Is there a reason why `this` is nullified in Crockford's `curry` method?

后端 未结 4 628
遇见更好的自我
遇见更好的自我 2021-01-31 20:18

In Douglas Crockford\'s book \"Javascript: The Good Parts\" he provides code for a curry method which takes a function and arguments and returns that function with

4条回答
  •  一生所求
    2021-01-31 20:42

    Reader beware, you're in for a scare.

    There's a lot to talk about when it comes to currying, functions, partial application and object-orientation in JavaScript. I'll try to keep this answer as short as possible but there's a lot to discuss. Hence I have structured my article into several sections and at the end of each I have summarized each section for those of you who are too impatient to read it all.


    1. To curry or not to curry

    Let's talk about Haskell. In Haskell every function is curried by default. For example we could create an add function in Haskell as follows:

    add :: Int -> Int -> Int
    add a b = a + b
    

    Notice the type signature Int -> Int -> Int? It means that add takes an Int and returns a function of type Int -> Int which in turn takes an Int and returns an Int. This allows you to partially apply functions in Haskell easily:

    add2 :: Int -> Int
    add2 = add 2
    

    The same function in JavaScript would look ugly:

    function add(a) {
        return function (b) {
            return a + b;
        };
    }
    
    var add2 = add(2);
    

    The problem here is that functions in JavaScript are not curried by default. You need to manually curry them and that's a pain. Hence we use partial application (aka bind) instead.

    Lesson 1: Currying is used to make it easier to partially apply functions. However it's only effective in languages in which functions are curried by default (e.g. Haskell). If you have to manually curry functions then it's better to use partial application instead.


    2. The structure of a function

    Uncurried functions also exist in Haskell. They look like functions in "normal" programming languages:

    main = print $ add(2, 3)
    
    add :: (Int, Int) -> Int
    add(a, b) = a + b
    

    You can convert a function in its curried form to its uncurried form and vice versa using the uncurry and curry functions in Haskell respectively. An uncurried function in Haskell still takes only one argument. However that argument is a product of multiple values (i.e. a product type).

    In the same vein functions in JavaScript also take only a single argument (it just doesn't know it yet). That argument is a product type. The arguments value inside a function is a manifestation of that product type. This is exemplified by the apply method in JavaScript which takes a product type and applies a function to it. For example:

    print(add.apply(null, [2, 3]));
    

    Can you see the similarity between the above line in JavaScript and the following line in Haskell?

    main = print $ add(2, 3)
    

    Ignore the assignment to main if you don't know what it's for. It's irrelevant apropos to the topic at hand. The important thing is that the tuple (2, 3) in Haskell is isomorphic to the array [2, 3] in JavaScript. What do we learn from this?

    The apply function in JavaScript is the same as function application (or $) in Haskell:

    ($) :: (a -> b) -> a -> b
    f $ a = f a
    

    We take a function of type a -> b and apply it to a value of type a to get a value of type b. However since all functions in JavaScript are uncurried by default the apply function always takes a product type (i.e. an array) as its second argument. That is to say that the value of type a is actually a product type in JavaScript.

    Lesson 2: All functions in JavaScript only take a single argument which is a product type (i.e. the arguments value). Whether this was intended or happenstance is a matter of speculation. However the important point is that you understand that mathematically every function only takes a single argument.

    Mathematically a function is defined as a morphism: a -> b. It takes a value of type a and returns a value of type b. A morphism can only have one argument. If you want multiple arguments then you could either:

    1. Return another morphism (i.e. b is another morphism). This is currying. Haskell does this.
    2. Define a to be a product of multiple types (i.e. a is a product type). JavaScript does this.

    Out of the two I prefer curried functions as they make partial application trivial. Partial application of "uncurried" functions is more complicated. Not difficult, mind you, but just more complicated. This is one of the reasons why I like Haskell more than JavaScript: functions are curried by default.


    3. Why OOP matters not

    Let's take a look at some object-oriented code in JavaScript. For example:

    var oddities = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].filter(odd).length;
    
    function odd(n) {
        return n % 2 !== 0;
    }
    

    Now you might wonder how is this object-oriented. It looks more like functional code. After all you could do the same thing in Haskell:

    oddities = length . filter odd $ [0..9]
    

    Nevertheless the above code is object-oriented. The array literal is an object which has a method filter which returns a new array object. Then we simply access the length of the new array object.

    What do we learn from this? Chaining operations in object-oriented languages is the same as composing functions in functional languages. The only difference is that the functional code reads backwards. Let's see why.

    In JavaScript the this parameter is special. It's separate from the formal parameters of the function which is why you need to specify a value for it separately in the apply method. Because this comes before the formal parameters, methods are chained from left-to-right.

    add.apply(null, [2, 3]); // this comes before the formal parameters
    

    If this were to come after the formal parameters the above code would probably read as:

    var oddities = length.filter(odd).[0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
    
    apply([2, 3], null).add; // this comes after the formal parameters
    

    Not very nice is it? Then why do functions in Haskell read backwards? The answer is currying. You see functions in Haskell also have a "this" parameter. However unlike in JavaScript the this parameter in Haskell is not special. In addition it comes at the end of the argument list. For example:

    filter :: (a -> Bool) -> [a] -> [a]
    

    The filter function takes a predicate function and a this list and returns a new list with only the filtered elements. So why is the this parameter last? It makes partial application easier. For example:

    filterOdd = filter odd
    oddities = length . filterOdd $ [0..9]
    

    In JavaScript you would write:

    Array.prototype.filterOdd = [].filter.myCurry(odd);
    var oddities = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].filterOdd().length;
    

    Now which one would you choose? If you're still complaining about reading backwards then I have news for you. You can make Haskell code read forwards using "backward application" and "backward composition" as follows:

    ($>) :: a -> (a -> b) -> b
    a $> f = f a
    
    (>>>) :: (a -> b) -> (b -> c) -> (a -> c)
    f >>> g = g . f
    
    oddities = [0..9] $> filter odd >>> length
    

    Now you have the best of both worlds. Your code reads forwards and you get all the benefits of currying.

    There are a lot of problems with this that don't occur in functional languages:

    1. The this parameter is specialized. Unlike other parameters you can't simply set it to an arbitrary object. Hence you need to use call to specify a different value for this.
    2. If you want to partially apply functions in JavaScript then you need to specify null as the first parameter of bind. Similarly for call and apply.

    Object-oriented programming has nothing to do with this. In fact you can write object-oriented code in Haskell as well. I would go as far as to say that Haskell is in fact an object-oriented programming language, and a far better one at that than Java or C++.

    Lesson 3: Functional programming languages are more object-oriented than most mainstream object-oriented programming languages. In fact object-oriented code in JavaScript would be better (although admittedly less readable) if written in a functional style.

    The problem with object-oriented code in JavaScript is the this parameter. In my humble opinion the this parameter shouldn't be treated any differently than formal parameters (Lua got this right). The problem with this is that:

    1. There's no way to set this like other formal parameters. You have to use call instead.
    2. You have to set this to null in bind if you wish to only partially apply a function.

    On a side note I just realized that every section of this article is becoming longer than the preceding section. Hence I promise to keep the next (and final) section as short as possible.


    4. In defense of Douglas Crockford

    By now you must have picked up that I think that most of JavaScript is broken and that you should shift to Haskell instead. I like to believe that Douglas Crockford is a functional programmer too and that he is trying to fix JavaScript.

    How do I know that he's a functional programmer? He's the guy that:

    1. Popularized the functional equivalent of the new keyword (a.k.a Object.create). If you don't already do then you should stop using the new keyword.
    2. Attempted to explain the concept of monads and gonads to the JavaScript community.

    Anyway, I think Crockford nullified this in the curry function because he knows how bad this is. It would be sacrilege to set it to anything other than null in a book entitled "JavaScript: The Good Parts". I think he's making the world a better place one feature at a time.

    By nullifying this Crockford is forcing you to stop relying on it.


    Edit: As Bergi requested I'll describe a more functional way to write your object-oriented Calculator code. We will use Crockford's curry method. Let's start with the multiply and back functions:

    function multiply(a, b, history) {
        return [a * b, [a + " * " + b].concat(history)];
    }
    
    function back(history) {
        return [history[0], history.slice(1)];
    }
    

    As you can see the multiply and back functions don't belong to any object. Hence you can use them on any array. In particular your Calculator class is just a wrapper for list of strings. Hence you don't even need to create a different data type for it. Hence:

    var myCalc = [];
    

    Now you can use Crockford's curry method for partial application:

    var multiplyPi = multiply.curry(Math.PI);
    

    Next we'll create a test function to multiplyPi by one and to go back to the previous state:

    var test = bindState(multiplyPi.curry(1), function (prod) {
        alert(prod);
        return back;
    });
    

    If you don't like the syntax then you could switch to LiveScript:

    test = do
        prod <- bindState multiplyPi.curry 1
        alert prod
        back
    

    The bindState function is the bind function of the state monad. It's defined as follows:

    function bindState(g, f) {
        return function (s) {
            var a = g(s);
            return f(a[0])(a[1]);
        };
    }
    

    So let's put it to the test:

    alert(test(myCalc)[0]);
    

    See the demo here: http://jsfiddle.net/5h5R9/

    BTW this entire program would have been more succinct if written in LiveScript as follows:

    multiply = (a, b, history) --> [a * b, [a + " * " + b] ++ history]
    
    back = ([top, ...history]) -> [top, history]
    
    myCalc = []
    
    multiplyPi = multiply Math.PI
    
    bindState = (g, f, s) -->
        [a, t] = g s
        (f a) t
    
    test = do
        prod <- bindState multiplyPi 1
        alert prod
        back
    
    alert (test myCalc .0)
    

    See the demo of the compiled LiveScript code: http://jsfiddle.net/5h5R9/1/

    So how is this code object oriented? Wikipedia defines object-oriented programming as:

    Object-oriented programming (OOP) is a programming paradigm that represents concepts as "objects" that have data fields (attributes that describe the object) and associated procedures known as methods. Objects, which are usually instances of classes, are used to interact with one another to design applications and computer programs.

    According to this definition functional programming languages like Haskell are object-oriented because:

    1. In Haskell we represent concepts as algebraic data types which are essentially "objects on steroids". An ADT has one or more constructors which may have zero or more data fields.
    2. ADTs in Haskell have associated functions. However unlike in mainstream object-oriented programming languages ADTs don't own the functions. Instead the functions specialize upon the ADTs. This is actually a good thing as ADTs are open to adding more methods. In traditional OOP languages like Java and C++ they are closed.
    3. ADTs can be made instances of typeclasses which are similar to interfaces in Java. Hence you still do have inheritance, variance and subtype polymorphism but in a much less intrusive form. For example Functor is a superclass of Applicative.

    The above code is also object-oriented. The object in this case is myCalc which is simply an array. It has two functions associated with it: multiply and back. However it doesn't own these functions. As you can see the "functional" object-oriented code has the following advantages:

    1. Objects don't own methods. Hence it's easy to associate new functions to objects.
    2. Partial application is made simple via currying.
    3. It promotes generic programming.

    So I hope that helped.

提交回复
热议问题