5

I've been thinking about language design lately, and reading over some of the new things in Haskell (always a nice source of inspiration). I'm struck by the many odd uses of the left <- and right -> arrow operators.

I guess the many different usages comes from prior art in math and other languages regarding arrow syntax, but is there some other reason not to try and make the usage more consistent or clear? Maybe I'm just not seeing the big picture?

The right arrow gets used as a type constructor for functions, the separator between argument and body of lambda expressions, separator for case statements, and it's used in pattern views, which have the form (e -> p).

The left arrow gets used in do notation as something similar to variable binding, in list comprehensions for the same (I'm assuming they are the same, as list comprehensions look like condensed do blocks), and in pattern guards, which have the form (p <- e).

Now the last examples for each arrow are just silly! I understand that guards and views serve different purposes, but they have almost identical form except that one is the mirror of the other! I've also always found it kind of odd that regular functions are defined with = but lambdas with ->. Why not use the arrow for both? Or the equals for both?

It also gets pretty odd when you consider that for comparing some calculated value against a constant, there's nearly a half-dozen ways to do it:

test x = x == 4

f     x = if test x then g x else h x

f'    4 = g 4
f'    x = h x

f''   x@(test -> true) = g x
f''   _ = h x

f'''  x | true <- test x = g x
        | otherwise = h x

f'''' x = case (x) of 
          4 -> g x
          _ -> h x

Variety is the spice of source code though, right?

  • 1
    I think the <- in comprehensions is meant to resemble (meaning "element of") not . – detly Aug 29 '14 at 5:51
  • 2
    @detly I think it's because list comprehensions are meant to resemble do notation – Benjamin Hodgson Aug 29 '14 at 10:38
  • 1
    @Carcigenicate: Actually, you can think of list comprehensions as just a different syntax for do-notation. In fact, they used to be "monad comprehensions" by default, and now you can still enable that functionality with an extension: Monad Comprehensions. – Tikhon Jelvis Nov 7 '14 at 22:24
  • 1
    "Why not use the arrow for both? Or the equals for both?" The equality symbol makes sense for named function definitions because you're saying that applying the named function to its arguments is equivalent to the right hand side of the equation. Defining lambdas that way makes no sense notationally: x = x + 1 implies x is equal to its successor, which can't be true. – Doval Mar 11 '15 at 12:07
  • 2
    The use of the arrow in list comprehension predates the "do" notation by over a decade. I'd guess the use of an arrow over a ∈ was probably because the former isn't really appropriate for lists and also so that generators don't look like guards. (See: D.A. Turner, 1981, "The semantic elegance of applicative languages.") The use of -> only came with Haskell; I don't think either KRC nor Miranda had lambdas. ML uses => for both lambda expressions and case expressions. The use of = comes from ML and KRC (which was first?) and fits well with equational reasoning about programs. – Theodore Norvell Apr 18 '15 at 22:48
5

I've also always found it kind of odd that regular functions are defined with = but lambdas with ->. Why not use the arrow for both? Or the equals for both?

The equality symbol makes sense for named function definitions because it states that applying the named function to its arguments is equivalent to evaluating the right hand side of the equation. Defining lambdas that way makes no sense notationally: x = x + 1 implies x is equal to its successor, which can't be true.

The left arrow gets used in do notation as something similar to variable binding, in list comprehensions for the same (I'm assuming they are the same, as list comprehensions look like condensed do blocks), and in pattern guards, which have the form (p <- e).

All of those constructs bind a new variable. Using = here would not make much sense either, because a statement such as x = x + 1 implies that x refers to the same thing on both sides. A variable binding introduces a new x that's distinct from any x that may occur in the right hand side.

One alternative would've been to just use -> and place the variable on the right, but I believe placing it on the left is objectively more readable in cultures that read left to right. You scan text by moving your eyes along the left margin and thus placing all the variables on the left makes them easier to find. And although having two different symbols for variable bindings may not seem consistent, there's consistency in the position of the variable that gets bound; it's always on the left.

So that just leaves the question of why <- as opposed to some other symbol like :=. I don't know, but I can guess. Look at what desugared monadic code looks like:

[1, 2, 3] >>= \xs -> map (*2) xs

Considering that >>= with its arguments swapped is called =<<, it probably seemed natural to use a left-facing arrow.

1

The operator <- is used in two ways as described in Haskell operators:

  1. List comprehension generator;
  2. Single assignment operator in do-constr.

List comprehensions are syntactic sugar like the expression

import Data.Char (toUpper)

[toUpper c | c <- s]

where s :: String is a string such as "Hello". Strings in Haskell are lists of characters; the generator c <- sfeeds each character of s in turn to the left-hand expression toUpper c, building a new list. The result of this list comprehension is "HELLO". (Of course, in this simple example you would just write map toUpper s.)

In the first versions of Haskell, the comprehension syntax was available for all monads. Later the comprehension syntax was restricted to lists. Since lists are an instance of monads, you can get list comprehension in terms of the do-notation.

Because of this, several Haskell programmers consider the list comprehension unnecessary now.

The example from above can be translated to list monad as follows:

do c <- s return (toUpper c)

For more info, see List comprehension.

  • TBH, I first really deemed that @CodexArcanum asks about "logic behind" ->, <-. So, my answer actually is good for a school exam.Though, I hope it will be helpful for someone. – Anton Danilov Mar 11 '15 at 15:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.