A more useful bind() for Javascript en

By .oisyn on Wednesday 30 January 2013 23:53 - Comments (4)
Category: Random code farts, Views: 2.214

bind or similar functionality has been a part of many javascript codebases and has made its official introduction in Javascript 1.8.5. Coming from a C++ background myself, where std::bind made its way through the TR1 library into standard C++11 after years of perculating within the boost library, I found that Javascript's version is lacking in certain features.

Recently this blog post was brought to my attention. Disregarding the underlying message of the post for the moment, the issue at hand was that the following Javascript expression:

JavaScript:
1
['10','10','10','10','10'].map(parseInt);

resulted in:

JavaScript:
1
[10NaN234]


While this might not be very intuitive for the average non-javascript developer, the actual problem lies in the fact that Array.prototype.map calls the supplied callback with 3 arguments (respectively: value, index and the array being mapped), and that parseInt accepts 2 - value and radix - with the latter being optional. Thusly, the array indices 0 through 4 are being interpreted as radix, causing the string '10' to be parsed for all 5 radices.

API design considerations aside, it would've been nice if we somehow were able to express: hey, I have this function accepting 2 argument, and I'd like to bind the second argument to a fixed value. Sounds like a job for... (queue drumroll)... Function.prototype.bind()! Well, it sounds like that, except that it really isn't. It would've suited the job perfectly if you wanted to fix the first argument, but the second? Come on! Been smoking crack lately?

And of course, some might say that simply using a lambda would suffice:

JavaScript:
1
['10','10','10','10','10'].map(function(x) { return parseInt(x10); });

and some would be right. But then what is the point of bind's existence, really? Would it not have been cool if you could simply use bind to say: pass the first argument through, but set the second argument to 10. For example, using this syntax:

JavaScript:
1
2
var f = parseInt.bind(null$_10);
f('34'); // calls parseInt('34', 10)


Well, you can now :). Inspired by C++'s std::bind, I've implemented a new version, Function.prototype.bind2(). Specifically, one that also accepts the following placeholders as its arguments:
  • $this - represents the 'this' that is used to call the function with
  • $1..$9 - represent the arguments 1 through 9
  • $_ - represents the argument at that location
  • $end - a special placeholder to indicate that further arguments are to be ignored (can only be used as the last argument to bind2().
Here are some examples:

JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
['1''2''3''4'].map(parseInt.bind2(null$_10)); // [1, 2, 3 ,4]

foo.bind2(null$2$1)("hello""world"); // foo("world", "hello")

foo.bind2(null$4$_$1)(1234567); // foo(4, 2, 1, 3, 5, 6, 7)

foo.bind2(null$4$_$1$end)(1234567); // foo(4, 2, 1)

foo.bind2(null$2$299$2)(123); // foo(2, 2, 99, 2, 1, 3)

foo.bind2($2$1)(12); // foo.call(2, 1), in other words: (2).foo(1)

String.prototype.output = alert.bind2(null$this);
"meow".output(); // alert("meow")

Number.prototype.concatTo = Array.prototype.concat.bind2($1$this);
(3).concatTo([12]); // [1, 2].concat(3) => [1, 2, 3]



Source code:

JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
function Placeholder(i) { if (ithis.index = i; }
$this = new Placeholder();
$_ = new Placeholder();
$end = new Placeholder();
for (var i = 1i <= 9i++)
    window['$' + i] = new Placeholder(i);

Function.prototype.bind2 = function()
{
    var func = this;
    var remap = [];
    var bound = [];
    var used = [];
    var numArgs = arguments.length;
    var hasEnd = false;

    for (var i in arguments)
    {
        i = +i// make sure it's an int
        var a = arguments[i];
        
        if (a === $end)
        {
            if (i != arguments.length-1)
                throw new TypeError("bind2 - only allowed to use $end als last parameter");
            hasEnd = true;
            numArgs = +i;
        }
        else if (a instanceof Placeholder)
        {
            var idx;
            if (a === $this)
                idx = 0;
            else if (a === $_)
                idx = i;
            else
                idx = a.index;
            
            remap[i] = idx;
            numArgs = Math.max(numArgsidx + 1);
            used[idx] = true;
        }
        else
        {
            bound[i] = a;
        }
    }
    
    var firstFree = 0;
    while(used[++firstFree]);
    if (!hasEnd)
    {
        for (var i = arguments.lengthi < numArgsi++)
        {
            remap[i] = firstFree;
            while(used[++firstFree]);
        }
    }
    
    var r = function()
    {
        var outThis = (0 in bound) ? bound[0] : remap[0] == 0 ? this : arguments[remap[0] - 1];
        var outArgs = [];
        for (var i = 1i < numArgsi++)
            outArgs[i-1] = (i in bound) ? bound[i] : remap[i] == 0 ? this : arguments[remap[i] - 1];
            
        if (!hasEnd)
            outArgs = outArgs.concat(Array.prototype.splice.call(argumentsfirstFree - 1));

        return func.apply(outThisoutArgs);
    };
    
    var nop = function() { };
    nop.prototype = func.prototype;
    r.prototype = new nop();
    return r;
}


Disclaimer: I'm not a javascript afficionado. I know above code polutes the global namespace (in particular with the Placeholder constructor), and you might not want that. You could wrap it in a local scope if you want...

Volgende: Overloading lambda's in C++11 02-'13 Overloading lambda's in C++11

Comments


By Tweakers user FlowinG, Thursday 31 January 2013 10:35

Om eigenwijs toch in het Nederlands te antwoorden:

Ik denk dat je het op zich een slimme manier is om een C++-programmeerstijl te vertalen naar een Javascript-stijl. Hiermij maak je denk ik wel een klassieke fout voor iets in deze taal te willen oplossen wat niet echt probleem is.

Het voordeel van de officiŽle bind, is dat je gedwongen wordt om een functie methode mee te geven, wat debuggen vergemakkelijkt. De opzet met verschillende argumenten die kriskras door elkaar gezet worden vereist een groot 'werkgeheugen' van de ontwikkelaar om te volgen wat precies in de code staat. Het gebruik van een lambda of functie maakt het mogelijk om tijdens debuggen deze functie binnen te stappen.

Mocht je dit 'probleem' toch willen oplossen, dan is dit ook niet helemaal de goede weg. Voornamelijk - en je geeft het zelf al aan - door het plaatsen van variabelen in de globale namespace. Je kan dit dan wel oplossen door het in een closure te plaatsen, maar dan kan je vanuit een andere closure weer niet de bind2 methode gebruiken met deze variabelen, tenzij je die weer opnieuw declareert.

Een meer Javascript manier zou zijn om als tweede argument van de bind2-functie een object mee te geven waarin je definieert wat de functie moet doen. Bijvoorbeeld bij onderstaande functie zou de parseInt als optie krijgen om als tweede argument de optie 10 mee te geven. (Je kan nog nadenken of je de indexer op 0 of 1 wilt laten beginnen ivm het gedrag van de thisArg).

code:
1
parseInt.bind(null, { 2: 10})


Ander (klein) puntje: bij vergelijken van waardes kan je beter gebruik maken van de strict operators (=== ipv ==). Dit voorkomt eventuele typeconversie en is daardoor sneller en safer. https://developer.mozilla...tors/Comparison_Operators

By Tweakers user .oisyn, Thursday 31 January 2013 10:50

Ik denk dat je het op zich een slimme manier is om een C++-programmeerstijl te vertalen naar een Javascript-stijl
Wat is er specifiek C++-stijl aan dan? C++ kent ook gewoon closures. Er is een reden dat bind() bestaat, namelijk dat de code ietwat cleaner wordt dan overal closures gebruiken. Daarnaast is het in standaard Javascript niet straightforward om de overige parameters ook meegestuurd te krijgen in een closure. Ook kom je dan zo nu en dan in de knoop met het gebruik van gerefereerde variabelen uit de omringende scope (wat referenties zijn en geen kopieŽn), waardoor je in het geval van een closures daar omheen ook nog eens een closures moet maken. Het gebruik van bind is in die gevallen veel praktischer.


JavaScript:
1
2
3
4
5
6
function foo(x)
{
    var f = function(y) { bar(yx); }
    x = 5;
    f(); // oops
}


Je kunt regel 3 dan wel vervangen door:

JavaScript:
3
var f = function(_x) { return function(y) { bar(y_x); } }(x);

Maar daar wordt het ook niet echt leesbaarder van. Dit is dan veel duidelijker:

JavaScript:
3
var f = bar.bind(null$_x);
Het voordeel van de officiŽle bind, is dat je gedwongen wordt om een functie methode mee te geven, wat debuggen vergemakkelijkt
Ik snap niet helemaal wat je hiermee bedoelt. Wat is een "functie methode" (ik neem aan dat je gewoon functies danwel methodes bedoelt), en hoe geef je die mee aan de officiele bind dan? Ben je niet in de war met een 3rd party implementatie van bind() die niet op Function.prototype zit en daardoor een functie verwacht als eerste argument? Mijn bind2() werkt op precies dezelfde manier als de officiele bind(), behalve dat hij daarnaast ook nog eens deze placeholders ondersteunt.
Voornamelijk - en je geeft het zelf al aan - door het plaatsen van variabelen in de globale namespace
Ik had het over de Placeholder identifier, die is voor de gebruiker nutteloos. Uiteraard is het van belang dat die $1 en dergelijke wel gewoon beschikbaar zijn. Je zou ze in een object kunnen onderbrengen als je daar vrolijker van wordt.
Een meer Javascript manier zou zijn om als tweede argument van de bind2-functie een object mee te geven waarin je definieert wat de functie moet doen
Totaal niet compatible met de officiele bind(). Ben je Łberhaupt bekend met Function.prototype.bind? Na aanleiding van deze post heb ik het idee van niet, namelijk.

Ik vind je API daarnaast niet heel erg praktisch. Ik moet dus nadenken over het feit dat het de tweede parameter is, wat in mijn implementatie niet hoeft (gewoon parseInt.bind2(null, $_, 10)
Daarnaast is het met jouw API niet mogelijk om argumenten om te draaien.
Ander (klein) puntje: bij vergelijken van waardes kan je beter gebruik maken van de strict operators (=== ipv ==)
Good point, al is typeconversie hier niet helemaal relevant :)

[Comment edited on Thursday 31 January 2013 11:33]


By Tweakers user Zerotorescue, Friday 01 February 2013 18:20

Wat is er mis met de anonymous function?

JavaScript:
1
['10','10','10','10','10'].map(function(x) { return parseInt(x10); });


Deze oplossing bedenk je binnen een paar seconden en heb je binnen 10 seconden uitgetypt; op naar het volgende probleem. Om op een werkende alternatieve oplossing - zoals jou bind2 - te komen ben je al snel een uur bezig, dat brengt je productiviteit omlaag en je deadline sneller in de buurt. Ik zou vooral niet te lang over zulke simpel oplosbare problemen nadenken.

Oja, niet te vergeten dat de anonymous function oplossing veel duidelijk is. Iedere JS developer kent deze manier en hoeft niet in bind(2) te duiken.

[Comment edited on Friday 01 February 2013 18:22]


By Tweakers user .oisyn, Saturday 02 February 2013 00:09

Wat is er mis met de anonymous function?
Dat heb ik gewoon in mijn blog behandeld.
Om op een werkende alternatieve oplossing - zoals jou bind2 - te komen ben je al snel een uur bezig
Dan ga je er dus vanuit dat je
• dergelijke oplossingen altijd zťlf verzint (not invented here syndrome)
• en dan ook alleen op je werk.

Sommigen maken frameworks, en sommigen doen dat als hobby. Natuurlijk ga je mijn oplossing niet zitten te bedenken als je druk aan een project bezig bent. Ikzelf daarentegen klop zo goed als geen professioneel javascript (ik ben gamedeveloper), dus als ik het doe dan doe ik dat vanuit de hobbysfeer. Daarnaast vind ik het leuk om dit soort theoretische foefjes te bedenken. Aan de andere kant zijn er ook zat Javascript frameworks waar dit soort dingetjes inzitten. Verklaar jij hen ook allemaal voor gek omdat ze tijd hebben gespendeerd om die oplossingen te bedenken?

En het punt is dat jij het nu niet meer hoeft te verzinnen, want de oplossing heb ik gepresenteerd. Je kan de code gewoon letterlijk copypasten en gebruiken (rechtentechnische aspecten aside - het zal mij een worst wezen of je mijn code gebruikt maar ik had natuurlijk officieel nog geen afstand gedaan). En wellicht zit het over een paar jaar gewoon standaard in Javascript. Net zoals bind tegenwoordig gewoon standaard in javascript zit. Ook die is begonnen met een blogpost niet zo heel verschillend van deze.
Oja, niet te vergeten dat de anonymous function oplossing veel duidelijk is. Iedere JS developer kent deze manier en hoeft niet in bind(2) te duiken.
Dat is natuurlijk een enorm nonargument. Als developers niet bereid zijn te leren dan staat de boel gewoon stil. Talen - en paradigma's - evolueren ook. 10 jaar geleden kende Javascript geen bind(). 10 jaar geleden was er ook nog geen jQuery. Nu verklaart iedereen je voor gek als je alle Dom manipulation in native javascript op gaat zitten lossen. Maar dat doe jij natuurlijk wel, want je kent het immers niet dus je past het nooit toe.

[Comment edited on Saturday 02 February 2013 00:39]


Comments are closed