What does the `<<` operator mean in elm?

前端 未结 5 1475
悲&欢浪女
悲&欢浪女 2021-01-30 12:24

In the following code taken from Elm Form Example, line 122, what does the << operator mean?

Field.field Field.defaultStyle (Signal.send updat         


        
5条回答
  •  一整个雨季
    2021-01-30 13:21

    << is a function composition operator, defined in core library Basics. All functions from Basics are imported into Elm projects unqualified.

    Elm's type system

    Let's recall basics of Elm type system.

    Elm is statically typed. This means that in Elm every variable or function has a type, and this type never changes. Examples of types in Elm are:

    • Int
    • String
    • Maybe Bool
    • { name : String, age : Int }
    • Int -> Int
    • Int -> String -> Maybe Char.

    Static typing means that compiler ensures types of all functions and variables are correct during compilation, so you don't have runtime type errors. In other words, you'll never have a function of type String -> String receiving or returning Int, code that allows this won't even compile.

    You can also make your functions polymorphic by replacing a concrete type such as String or Maybe Int with a type variable, which is an arbitrary lowercase string, such as a. Many Elm core functions are type polymorphic, for example List.isEmpty has the type List a -> Bool. It takes a List of some type and returns a value of type Bool.

    If you see the same type variable again, then instances of this type variable must be of same type. For example List.reverse has type List a -> List a. So if you apply List.reverse to a list of integers (i.e. to something that has type List Int), it will return a list of integers back. No way such function can take a list of integers, but return a list of strings. This is guaranteed by the compiler.

    All functions in Elm are curried by default. This means that if you have a function of 2 arguments, it is transformed into a function of 1 argument that returns a function of 1 argument. That's why you function application syntax of Elm is so different from function application in other languages such as Java, C++, C#, Python, etc. There's no reason to write someFunction(arg1, arg2), when you can write someFunction arg1 arg2. Why? Because in reality someFunction arg1 arg2 is actually ((someFunction arg1) arg2).

    Currying makes partial application possible. Suppose you want to partially apply List.member. List.member has a type a -> List a -> Bool. We can read the type as “List.member takes 2 arguments, of type a and type List a”. But we can also read the type as “List.member takes 1 argument of type a. It returns a function of type List a -> Bool”. Therefore we can create a function isOneMemberOf = List.member 1, which will have the type List Int -> Bool.

    This means that -> in type annotations of functions is right-associative. In other words, a -> List a -> Bool is the same as a -> (List a -> Bool).

    Infix and prefix notation

    Any infix operator is actually an ordinary function behind the curtains. It's just when a function name consists solely of non-alphanumeric symbols (such as $, <|, <<, etc), it is placed between 2 arguments, not in front of them (like ordinary functions).

    But you still can put a binary operator like + in front of the 2 arguments, by enclosing it in parentheses, so the 2 function applications below are equivalent:

    2 + 3 -- returns 5
    (+) 2 3 -- returns 5, just like the previous one
    

    Infix operators are just ordinary functions. There's nothing special about them. You can partially apply them just like any other function:

    addTwo : Int -> Int
    addTwo = (+) 2
    
    addTwo 3 -- returns 5
    

    Function composition

    (<<) is a function composition operator, defined in core library Basics. All functions from basics are imported into Elm projects unqualified, meaning you don't have to write import Basics exposing (..), it is already done by default.

    So just like any other operator, (<<) is just a function, like any other. What is its type?

    (<<) : (b -> c) -> (a -> b) -> a -> c
    

    Because -> is right-associative, this is equivalent to:

    (<<) : (b -> c) -> (a -> b) -> (a -> c)
    

    In other words, (<<) takes 2 functions of types b -> c and a -> b respectively, and returns a function of type a -> c. It composes 2 functions into one. How does that work? Let's look at a contrived example for simplicity's sake. Suppose we have 2 simple functions:

    addOne = (+) 1
    multTwo = (*) 2
    

    Suppose we don't have (+), only addOne, how would we create a function that adds 3, not 1? Very simple, we would compose addOne together 3 times:

    addThree : Int -> Int
    addThree = addOne << addOne << addOne
    

    What if we want to create a function that adds 2, then multiples by 4?

    ourFunction : Int -> Int
    ourFunction = multTwo << multTwo << addOne << addOne
    

    (<<) composes functions from right-to-left. But the above example is simple, because all the types are the same. How would we find a sum of all even cubes of the list?

    isEven : Int -> Bool
    isEven n = n % 2 == 0
    
    cube : Int -> Int
    cube n = n * n * n
    
    ourFunction2 : List Int -> Int
    ourFunction2 = List.sum << filter isEven << map cube
    

    (>>) is the same function, but with arguments flipped, so we can write the same composition from left to right instead:

    ourFunction2 = map cube >> filter isEven >> List.sum
    

    Recap

    When you see something like h << g << f, then you know that f, g, h are functions. When this construct h << g << f is applied to a value x, then you know:

    • Elm first applies f to x
    • then applies g to the result of the previous step
    • then applies h to the result of the previous step

    Therefore (negate << (*) 10 << sqrt) 25 equals -50.0, because you first take a square root of 25 and get 5, then you multiply 5 by 10 and get 50, then you negate 50 and get -50.

    Why << and not .

    Before Elm 0.13 (see announcement) function composition operator was (.), and its behavior was identical to current (<<). (<<) was adopted in Elm 0.13 from F# language (see Github issue). Elm 0.13 also added (>>) as equivalent to flip (<<), and (<|) as replacement for function application operator ($), and (|>) as equivalent to flip (<|).

    Infix function call

    You might be wondering if you can turn an ordinary alphanumeric function name into an infix binary operator. Before Elm 0.18 you'd use backticks to make a function infix, so below 2 would be equivalent:

    max 1 2 -- returns 2
    1 `max` 2 -- returns 2
    

    Elm 0.18 removed this feature. You can't do it in Elm anymore, but languages like Haskell and PureScript still have it.

提交回复
热议问题