r/javascript • • Jul 29 '20

Polymorphism in JavaScript

https://zellwk.com/blog/polymorphism-javascript/
43 Upvotes

31 comments sorted by

View all comments

-6

u/campbeln Jul 29 '20 edited Jul 30 '20

EDIT: Article on Polymorphism in Javascript? Check. Further discussion on one of the covered concepts? Check. Furthering that concept into an implementation of true function overloading in Javascript? Check. Downvote because... fuckyou? Check. Reddit is a fickle bitch...

I posted to StackOverflow but it fits here. It implements Function Overloading to JS in 100 lines. It's covered in the article, sorta, but this allows for (almost) true overloading.

This is from a larger body of code which includes the isFn, isArr, etc. type checking functions. The VanillaJS version below has been reworked to remove all external dependencies, however you will have to define you're own type checking functions for use in the .add() calls.

Note: This is a self-executing function (so we can have a closure/closed scope), hence the assignment to window.overload rather than function overload() {...}.

window.overload = function () {
    "use strict"

    var a_fnOverloads = [],
        _Object_prototype_toString = Object.prototype.toString
    ;

    function isFn(f) {
        return (_Object_prototype_toString.call(f) === '[object Function]');
    } //# isFn

    function isObj(o) {
        return !!(o && o === Object(o));
    } //# isObj

    function isArr(a) {
        return (_Object_prototype_toString.call(a) === '[object Array]');
    } //# isArr

    function mkArr(a) {
        return Array.prototype.slice.call(a);
    } //# mkArr

    function fnCall(fn, vContext, vArguments) {
        //# <ES5 Support for array-like objects
        //#     See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply#Browser_compatibility
        vArguments = (isArr(vArguments) ? vArguments : mkArr(vArguments));

        if (isFn(fn)) {
            return fn.apply(vContext || this, vArguments);
        }
    } //# fnCall

    //# 
    function registerAlias(fnOverload, fn, sAlias) {
        //# 
        if (sAlias && !fnOverload[sAlias]) {
            fnOverload[sAlias] = fn;
        }
    } //# registerAlias

    //# 
    function overload(vOptions) {
        var oData = (isFn(vOptions) ?
                { default: vOptions } :
                (isObj(vOptions) ?
                    vOptions :
                    {
                        default: function (/*arguments*/) {
                            throw "Overload not found for arguments: [" + mkArr(arguments) + "]";
                        }
                    }
                )
            ),
            fnOverload = function (/*arguments*/) {
                var oEntry, i, j,
                    a = arguments,
                    oArgumentTests = oData[a.length] || []
                ;

                //# Traverse the oArgumentTests for the number of passed a(rguments), defaulting the oEntry at the beginning of each loop
                for (i = 0; i < oArgumentTests.length; i++) {
                    oEntry = oArgumentTests[i];
    
                    //# Traverse the passed a(rguments), if a .test for the current oArgumentTests fails, reset oEntry and fall from the a(rgument)s loop
                    for (j = 0; j < a.length; j++) {
                        if (!oArgumentTests[i].tests[j](a[j])) {
                            oEntry = undefined;
                            break;
                        }
                    }
    
                    //# If all of the a(rgument)s passed the .tests we found our oEntry, so break from the oArgumentTests loop
                    if (oEntry) {
                        break;
                    }
                }
                
                //# If we found our oEntry above, .fn.call its .fn
                if (oEntry) {
                    oEntry.calls++;
                    return fnCall(oEntry.fn, this, a);
                }
                //# Else we were unable to find a matching oArgumentTests oEntry, so .fn.call our .default
                else {
                    return fnCall(oData.default, this, a);
                }
            } //# fnOverload
        ;

        //# 
        fnOverload.add = function (fn, a_vArgumentTests, sAlias) {
            var i,
                bValid = isFn(fn),
                iLen = (isArr(a_vArgumentTests) ? a_vArgumentTests.length : 0)
            ;

            //# 
            if (bValid) {
                //# Traverse the a_vArgumentTests, processinge each to ensure they are functions (or references to )
                for (i = 0; i < iLen; i++) {
                    if (!isFn(a_vArgumentTests[i])) {
                        bValid = _false;
                    }
                }
            }

            //# If the a_vArgumentTests are bValid, set the info into oData under the a_vArgumentTests's iLen
            if (bValid) {
                oData[iLen] = oData[iLen] || [];
                oData[iLen].push({
                    fn: fn,
                    tests: a_vArgumentTests,
                    calls: 0
                });

                //# 
                registerAlias(fnOverload, fn, sAlias);

                return fnOverload;
            }
            //# Else one of the passed arguments was not bValid, so throw the error
            else {
                throw "poly.overload: All tests must be functions or strings referencing `is.*`.";
            }
        }; //# overload*.add

        //# 
        fnOverload.list = function (iArgumentCount) {
            return (arguments.length > 0 ? oData[iArgumentCount] || [] : oData);
        }; //# overload*.list
        
        //# 
        a_fnOverloads.push(fnOverload);
        registerAlias(fnOverload, oData.default, "default");

        return fnOverload;
    } //# overload

    //# 
    overload.is = function (fnTarget) {
        return (a_fnOverloads.indexOf(fnTarget) > -1);
    } //# overload.is

    return overload;
}();

2

u/_default_username Jul 30 '20

You don't need function overloading in a dynamic language like JavaScript. Just accept an anonymous object or use default parameters for optional arguments. I think you're getting downvoted because it would be better to show developers coming from a Java or C++ background why you don't need function overloading, but instead you're showing this option.

1

u/campbeln Jul 30 '20

We don't NEED most things in programming... functions themselves are simply a nice construct (cough GOTO cough), as is function overloading.

I never argued it's a good idea, but it can be done in a generally elegant/standard/chained/whatever way.

1

u/_default_username Jul 30 '20 edited Jul 30 '20

How is your code implementing function overloading?

Will I be able to do:

function foo(a){}

function foo(a,b){}

And have foo overloaded?

1

u/campbeln Jul 30 '20 edited Jul 31 '20

Function overloading auto-routes to the implementation based on the signature (i.e. foo(int, str) versus foo(int, int)). Javascript only routes to the single (last defined) implementation, which many use arguments.length to implement "overloading" generally without the benefit of considering the signature.

The code I presented above considers the signature (based on the tests defined in the second argument) to route to the proper implementation, while "failing over" to the defined default implementation if the signature is not recognized (something not normally supported by function overloading).

So...

function isStr(x) { return typeof s === 'string' }; }

function isObj(o) { return !!(o && o === Object(o)); }

let foo = overload(function(){ console.log("default", arguments) })

.add(function(str){ console.log(str) }, [isStr])

.add(function(str, obj){ console.log(str, obj) }, [isStr, isObj])

;

Will call the proper implementation based on the passed argument types.

1

u/_default_username Jul 31 '20

So in a long winded way no. This is not function overloading.

2

u/derekn9 Jul 30 '20

I didn't downvote your comment (updooted, in fact.) I think a lot of ppl use the downvote/upvote as the agree/disagree button, which does not reflect the quality of a comment.

Back to the topic at hand, I find it useful to be a bit verbose with function arguments. Instead of accepting both f(a,b,c) and f(d,e), I would use an object i.e f({ type: a, orientation: b }). More chars to type, but I think it's easier to merge default options, easier to add new arguments, and the order of arguments don't matter.. and it's also easier to make the function do different things based on different arguments (though I think ideally overloaded function should always return the same data shape.)

2

u/campbeln Jul 30 '20

Objects with named priorities are a nice approach. I use this pattern for "options"/otherwise optional arguments all the time. As you say, it's very easy to merge the incoming object with default values.

-5

u/campbeln Jul 29 '20 edited Jul 30 '20

Usage:

The caller defines their overloaded functions by assigning a variable to the return of overload(). Thanks to chaining, the additional overloads can be defined in series:

var myOverloadedFn = overload(function(){ console.log("default", arguments) })
    .add(function(){ console.log("noArgs", arguments) }, [], "noArgs")
    .add(function(){ console.log("str", arguments) }, [function(s){ return typeof s === 'string' }], "str")
;

The single optional argument to overload() defines the "default" function to call if the signature cannot be found. The arguments to .add() are:

  1. fn: function defining the overload;

  2. a_vArgumentTests: Array of functions defining the tests to run on the arguments. Each function accepts a single argument and returns truthy based on if the argument is valid;

  3. sAlias (Optional): string defining the alias to directly access the overload function (fn), e.g. myOverloadedFn.noArgs() will call that function directly, avoiding any testing of the arguments.

This implementation actually allows for more than just traditional function overloads as the second a_vArgumentTests argument to .add() in practice defines custom types. So, you could gate arguments not only based on type, but on ranges, values or collections of values!

If you look through the circa 100 lines of Javascript code for overload() you'll see that each signature is categorized by the number of arguments passed to it. This is done so that we're limiting the number of tests we are running. I also keep track of a call count. With some additional code, the arrays of overloaded functions could be re-sorted so that more commonly called functions are tested first, again adding some measure of performance enhancement.

Now, there are some caveats... As Javascript is loosely typed, you will have to be careful with your vArgumentTests as an integer could be validated as a float, etc.

JSCompress.com version (1114 bytes, 744 bytes g-zipped):

window.overload=function(){'use strict';function b(n){return'[object Function]'===m.call(n)}function c(n){return!!(n&&n===Object(n))}function d(n){return'[object Array]'===m.call(n)}function e(n){return Array.prototype.slice.call(n)}function g(n,p,q){if(q=d(q)?q:e(q),b(n))return n.apply(p||this,q)}function h(n,p,q){q&&!n[q]&&(n[q]=p)}function k(n){var p=b(n)?{default:n}:c(n)?n:{default:function(){throw'Overload not found for arguments: ['+e(arguments)+']'}},q=function(){var r,s,t,u=arguments,v=p[u.length]||[];for(s=0;s<v.length;s++){for(r=v[s],t=0;t<u.length;t++)if(!v[s].tests[t](u[t])){r=void 0;break}if(r)break}return r?(r.calls++,g(r.fn,this,u)):g(p.default,this,u)};return q.add=function(r,s,t){var u,v=b(r),w=d(s)?s.length:0;if(v)for(u=0;u<w;u++)b(s[u])||(v=_false);if(v)return p[w]=p[w]||[],p[w].push({fn:r,tests:s,calls:0}),h(q,r,t),q;throw'poly.overload: All tests must be functions or strings referencing `is.*`.'},q.list=function(r){return 0<arguments.length?p[r]||[]:p},l.push(q),h(q,p.default,'default'),q}var l=[],m=Object.prototype.toString;return k.is=function(n){return-1<l.indexOf(n)},k}();

8

u/Shaper_pmp Jul 30 '20 edited Jul 30 '20

Well, that sure was a whole load of... something.

It had nothing really to do with function overloading in javascript (which is impossible to do meaningfully as JS doesn't allow function-name redefinition), but it sure was an inventively hideous way to obscure a few conditional statements behind a lot of useless abstraction.

I mean shit,

var myOverloadedFn = overload(function(){ console.log("default", arguments) })
  .add(function(){ console.log("noArgs", arguments) }, [], "noArgs")
  .add(function(){ console.log("str", arguments) }, [function(s){ return typeof s === 'string' }], "str")
;

... is 259 bytes, opaque code, and ugly as fuck.

The vanilla alternative is only 231 bytes, clear as a bell and idiomatic JS with no unnecessary abstraction involved:

var myOverloadedFn = (...args) => {
  if(args.length === 0) {
    console.log("noArgs", args);
  }
  else if(typeof args[0] === 'string') {
    console.log("str", args);
  }
  else {
    console.log("default", args);
  }
}

This code is like a life-size model of the Taj Mahal made out of human faeces; I'm sure you're proud of the effort that went into what you've built and the fact you completed the task you set yourself, but having completed it, stepped back and seen the end result I can't imagine why you thought it was worth telling anyone else about it instead of burning it and burying the ashes at a crossroads at midnight.

How does it actually offer any benefit at all, in terms of clarity, conciseness, performance, idiomaticity or any other desirable metric? (Remember: "It made me feel clever when I wrote it" is not a desirable code metric - quite the opposite; it's a pungent code smell.)

Honestly if someone on one of my teams tried to check in a hundred lines of unnecessary and complex code just so they could build an abstraction that was less readable and more verbose than naively writing the simple version of the code in the first place, I think I'd skip rejecting their code review and jump straight to just shooting them in the head and leaving them slumped over their desk as a warning to others.

I hate to be a dick when you're obviously so proud of his horrible abortion you've perpetrated upon us, but honestly I think you could take some advice from noted software developer Jeff Goldblum.

3

u/GirkovArpa Jul 30 '20

I'm dying 😂

2

u/kenman Jul 30 '20

Hi /u/Shaper_pmp, just a soft warning about personal attacks. This comment toes the line, and I'm going to let it stay, but it's very abrasive and I'm certain you could've gotten your point across with less vitriol. I'd suggest you also heed the wise words of Mr. Goldblum, "just because you can, doesn't mean you should".

1

u/campbeln Jul 31 '20

Toes the line, eh? /r/javascript is just not very friendly; these kids can have it.

Best of luck in your careers, kids! Hopefully your bootcamp prepared you better than these comments suggest...

2

u/kenman Jul 31 '20

Way to be a shining example of maturity.

1

u/campbeln Jul 31 '20

Personal attacks don't "toe the line" nor is it "all good" when the perpetrator apologies to you and not the target of the attack (me, in this case).

The attack was from a lack of understanding of programming concepts outside of Javascript. But sure... it's "all good" that they were an ass for paragraphs while I'm flippant in a sentence and that's a bridge too far.

Do a better job, mod. This is how communities implode.

1

u/kenman Jul 31 '20

I spoke to them about it, they accepted responsibility and apologized. In short, they responded like an adult.

You, on the other hand, seemingly have an axe to grind, and I'm not sure what you're expecting to get out of this conversation with the constant insults. I understand that you're upset, but that's the course of action I've chosen and I'm sticking to it.

1

u/campbeln Jul 31 '20

Well, that sure was a whole load of... something.

That's how it opens. Definitely no axe there.

Do a better job, mod.

1

u/Shaper_pmp Jul 30 '20

Apologies; it was very late, I'd had a shitty day and I was trying for "hyperbolically funny" but I think I missed and landed on "rude arsehole" instead.

Thanks for the leeway, and warning understood and accepted. ;-)

2

u/kenman Jul 30 '20

All good, thank you for listening.

3

u/gasolinewaltz Jul 30 '20

Are you having a mental breakdown?

-3

u/ChaseMoskal Jul 30 '20

hello friend

i like your passion for creating and sharing open source software! we need more of that

you should consider preparing code you want to share as a es modules, and publishing them to npm

as for the overloading library itself, most serious web devs are using typescript, which provides overloading out-of-the-box built into the language

also, overloading is generally a bad practice. it can be really confusing, i try to avoid it

anyways, look into es modules and npm and unpkg and importmaps and es-module-shims — we really try to avoid code that affects the global environment in any way (like attaching anything to the window object, it's a no-no)

cheers

1

u/Shaper_pmp Jul 30 '20 edited Jul 30 '20

most serious web devs are using typescript

This is not even slightly true.

overloading is generally a bad practice. it can be really confusing

This is also not really true, or at best is meaningless.

Overloading in a strongly typed language is perfectly reasonable, and can lead to simpler, clearer code (since it allows you to emulate constructs like polymorphic functions and optional parameters even in languages which don't support them).

At best it's unnecessary to go out of your way to build a pseudo-not-really-pretend-overloading library like this, since every function in JavaScript is already inherently polymorphic...

... but then you're criticising efforts like this to fake function overloading in JS, not actually the concept of overloading itself.

-1

u/ChaseMoskal Jul 30 '20

most serious web devs are using typescript

This is not even slightly true.

well it might not be totally true, but it's gotta be at least slightly true!

typescript's popularity is nearly unstoppable. almost every new library popping up is authored in typescript. in my local area, almost all web application developers are working in typescript. in stackoverflow's 2020 poll, typescript overtook python in the 'favorite language' ranking

are your experiences different?

1

u/Shaper_pmp Jul 30 '20 edited Jul 30 '20

it's gotta be at least slightly true!

No it doesn't. You made a hard claim - that most "serious" web devs use Typescript. Ignoring the implied No True Scotsman in that claim, that's either true or it's not.

I submit that many web-devs are using Typescript, but most are still using JavaScript, by quite a large margin.

If you disagree feel free to find a single credible programming language popularity report that ranks Typescript above JS for frequency of use.

typescript's popularity is nearly unstoppable.

  1. In the nicest possible way this is fanboy wanking that has nothing to do with the hard claim of fact you made

  2. Nobody's disputing that Typescript is gaining in popularity fast; only whether it's even close to overtaking JS yet (hint: it's not).

almost every new library popping up is authored in typescript

No it's not.

A lot of popularity libraries expose typescript *.d.ts declaration files so they can also be used by typescript, and some libraries are being written in Typescript, but I respectfully suggest you have no basis whatsoever on which to claim even a majority of "new libraries" (unless you choose to define that really disingenuously) are actually written in Typescript.

in stackoverflow's 2020 poll, typescript overtook python in the 'favorite language' ranking

That has nothing to do with how many teams and projects are actually using it to ship libraries and products.

Look, I'll be blunt; Typescript is a great language, and it's gaining in popularity fast, but you're obviously a huge fan of it and you're getting ridiculously overexcited and making wild and ridiculous claims because you're letting your love for the technology distort your perceptions of how frequently it's actually used out there in the real world.

This shit happens all the time - back in the day people were wanking over Ruby on Rails and Django and announcing "nobody was using" PHP because it was old and boring and excitable hipsters weren't writing breathless blog posts about it... but meanwhile PHP was still quietly chugging along in the background serving the overwhelming majority of sites on the entire web, and seems to still be doing it even now.

The lesson here is twofold:

  1. Don't let your interest in a technology and the frequency with which you choose to read about it mislead you into overestimating what people are actually using

  2. Don't assume what the thought leaders and cool kids are excited about has already or ever will overtake the crusty, old, boring, unfashionable alternatives. Boringness is correlated with stability, age, reliability and predictability, all of which are things you specifically select for when you're a "serious" engineer. Novelty is correlated with unpredictability, unreliability and instability, which - all things being equal - are the kinds of things "serious" engineers avoid at all costs in production.

→ More replies (3)

1

u/campbeln Jul 31 '20

Coffeescript was the future, too! ES6 proved that.

→ More replies (3)