Presence of <| operator in uses of elm-test's test function

I was looking at elm-test’s test function, which has the signature:

test : String -> (() -> Expectation) -> Test

In both the example in that documentation, and the uses of the function that I’ve seen in the wild, they use the <| operator before supplying the second argument. Why is that necessary?

In general, I think I understand what the <| operator does. But, in this case, it seems to me that because the expression to the right of the operator is just a value and not the application of a function, that operator is unnecessary. Similar to how it is unnecessary in the following:

f x y z =
  x * y + z
  
_ = f 10 2 <| 3

What am I missing?

1 Like

It’s not necessary! But, it is really handy.

In practice, <| means you don’t need to use parentheses. So these things are equivalent:

  1. Dict.get "a" (Dict.fromList [("a", 1), ("b", 2)])
  2. Dict.get "a" <| Dict.fromList [("a", 1), ("b", 2)]

The first example ends with )]), which can be confusing! And as expressions get more complex, it gets even worse. When you get to the level of a unit test, you get something like this:

test "getting an item from a dictionary"
    (\_ ->
        Expect.equal 1 (Dict.get "a" (Dict.fromList [("a", 1)]))
    )

Now we’re up to )]))! You may be better at this than I am, but the chances I’ll be able to modify this syntax correctly are quite low. Let’s refactor using <|:

test "getting an item from a dictionary" <|
    \_ ->
        Expect.equal 1 (Dict.get "a" (Dict.fromList [("a", 1)]))

Better already! What if we put another one to remove the parentheses around Dict.get?

test "getting an item from a dictionary" <|
    \_ ->
        Expect.equal 1 <| Dict.get "a" (Dict.fromList [("a", 1)])

And, another for the Dict.fromList?

test "getting an item from a dictionary" <|
    \_ ->
        Expect.equal 1 <| Dict.get "a" <| Dict.fromList [("a", 1)]

Nice! No parentheses. But, this is still pretty confusing. You have to read it backwards! So, let’s change this to use |>, which does the same thing as <| but with the arguments flipped:

test "getting an item from a dictionary" <|
    \_ ->
        Dict.fromList [("a", 1)]
            |> Dict.get "a"
            |> Expect.equal 1

Much better! In fact, the functions in Expect were designed with |> in mind. That’s why the expected values are consistently the first arguments, instead of the last ones.

Finally, there are some rules I like to use about <| so that things stay nice and clear:

  1. Don’t ever use <| more than once in an expression.
  2. Don’t ever mix <| and |> in the same expression (the test above is two separate expressions.)
  3. Don’t use <| in simple cases. add <| 1 <| 1 and add 1 1 both result in 2, but one is significantly clearer.

Oh, and a last tidbit: I’ve heard these called many things, but my favorite are “left pizza” and “right pizza” :joy:

Edit: I keep thinking of things! If you’re trying to find examples of this in other languages, <| is sometimes written $. So add $ 1 $ 1. IMO <| is nicer to use since it implies direction once you know what |> does.

I’ve also written about this previously, if you want more examples.

9 Likes

In addition to what Brian said, the reason why these are used in Elm-test examples is so that you can read the tests like this:

test <name of test> 
  <body of test>

Because of the way elm-tests are evaluated, we have to provide a thunk to the API. A thunk in this context essentially means a function that will be evaluated at a later point: that’s why each test starts with \_, as the test function itself takes () as an argument, to delay evaluation until needed.

4 Likes

Thank you @brian - great information there! I understand the pizza operators now, and get the gist of thunks @eeue56

The nature of the lambda syntax has me a little confused though.

We can use a lambda “literal” as a value like this:

sq = \x -> x*x

However, we cannot use one like that as an argument to another function:

nPlusFuncN : Int -> (Int -> Int) -> Int
nPlusFuncN n f =
  n + f n  
  
result = nPlusFuncN 5 \x -> x*x
--                    ^
--                    COMPILER ERROR

Alright - I assume using lambdas as arguments has potential for parsing ambiguities, so it must be adorned with parenthesis:

result = nPlusFuncN 5 (\x -> x*x)
-- OK

But, when using left-pizza, an unadorned lambda is in fact valid syntax?

result = nPlusFuncN 5 <| \x -> x*x
-- OK

I guess that’s consistent with what you said (the <| operator can be used to reduce parenthesis). But the way it’s explained seems always to be about the thing to the right of the <| operator being a function application. In my example, the thing to the right of the <| operator is just a literal value.

The thing on the left of a <| (pointy side) is a function. It may, in the case of nPlusFuncN 5 <| ..., be a partially-applied function. The thing to the right (bar side) is a value. In this case it’s a literal function expression.

I agree with Brian: use only one <| at a time, and read it as “everything from here to the end of the expression is in parentheses”. If you need multiple <|, change it to |>, which is chaining transformations together.

The great thing about pipe operators (as I like to call them) is that you can read code intuitively and it does what you think. But there’s a level of type safety that’s got your back, which you don’t have to fully understand at first.

The other place I will use multiple <| operators is when building up something with constructors where I want the value to be read from left to right even if we are evaluating from right to left — e.g., Ok <| Just <| someFunction withArgument. This doesn’t come up all that often but it comes up more often than I might have expected.

Mark

1 Like