Does "<-" mean assigning a variable in Haskell?

vik santata picture vik santata · Feb 4, 2016 · Viewed 7.7k times · Source

Just started Haskell, it's said that everything in Haskell is "immutable" except IO package. So when I bind a name to something, it's always something immutable? Question, like below:

Prelude> let removeLower x=[c|c<-x, c `elem` ['A'..'Z']]
Prelude> removeLower "aseruiiUIUIdkf"
"UIUI"

So here:

1. “removeLower" is an immutable? Even it's a function object?
But I can still use "let" to assign something else to this name.

2. inside the function "c<-x" seems that "c" is a variable.
It is assigned by list x's values.

I'm using the word "variable" from C language, not sure how Haskell name all its names?

Thanks.

Answer

Luis Casillas picture Luis Casillas · Feb 4, 2016

If you're familiar with C, think of the distinction between declaring a variable and assigning a value to it. For example, you can declare a variable on its own and later assign to it:

int i;
i = 7;

Or you can declare a variable and assign initial value at the same time:

int i = 7;

And in either case, you can mutate the value of a variable by assigning to it once more after the first initialization or assignment:

int i = 7;  // Declaration and initial assignment
i = 5;      // Mutation

Assignment in Haskell works exclusively like the second example—declaration with initialization:

  1. You declare a variable;
  2. Haskell doesn't allow uninitialized variables, so you are required to supply a value in the declaration;
  3. There's no mutation, so the value given in the declaration will be the only value for that variable throughout its scope.

I bolded and hyperlinked "scope" because it's the second critical component here. This goes one of your questions:

“removeLower" is an immutable? Even it's a function object? But I can still use "let" to assign something else to this name.

After you bind removeLower to the function you define in your example, the name removeLower will always refer to that function within the scope of that definition. This is easy to demonstrate in the interpreter. First, let's define a function foo:

Prelude> let foo x = x + 2
Prelude> foo 4
6

Now we define an bar that uses foo:

Prelude> let bar x = foo (foo x)
Prelude> bar 4
8

And now we "redefine" foo to something different:

Prelude> let foo x = x + 3
Prelude> foo 4
7

Now what do you think happens to bar?

Prelude> bar 4
8

It remains the same! Because the "redefinition" of foo doesn't mutate anything—it just says that, in the new scope created by the "redefinition", the name foo stands for the function that adds three. The definition of bar was made in the earlier scope where foo x = x + 2, so that's the meaning that the name foo has in that definition of bar. The original value of foo was not destroyed or mutated by the "redefinition."

In a Haskell program as much as in a C program, the same name can still refer to different values in different scopes of the program. This is what makes "variables" variable. The difference is that in Haskell you can never mutate the value of a variable within one scope. You can shadow a definition, however—uses of a variable will refer to the "nearest" definition of that name in some sense. (In the case of the interpreter, the most recent let declaration for that variable.)


Now, with that out of the way, here are the syntaxes that exist in Haskell for variable binding ("assignment"). First, there's top-level declarations in a module:

module MyLibrary (addTwo) where

addTwo :: Int -> Int
addTwo x = x + 2

Here the name addTwo is declared with the given function as its value. A top level declaration can have private, auxiliary declarations in a where block:

addSquares :: Integer -> Integer
addSquares x y = squareOfX + squareOfY
  where square z = z * z
        squareOfX = square x
        squareOfY = square y

Then there's the let ... in ... expression, that allows you to declare a local variable for any expression:

addSquares :: Integer -> Integer
addSquares x y = 
  let square z = z * z
      squareOfX = square x
      squareOfY = square y
  in squareOfX + squareOfY

Then there's the do-notation that has its own syntax for declaring variables:

example :: IO ()
example = do
  putStrLn "Enter your first name:"
  firstName <- getLine

  putStrLn "Enter your lasst name:"
  lastName <- getLine

  let fullName = firstName ++ " " ++ lastName
  putStrLn ("Hello, " ++ fullName ++ "!")

The var <- action assigns a value that is produced by an action (e.g., reading a line from standard input), while let var = expr assigns a value that is produced by a function (e.g., concatenating some strings). Note that the let in a do block is not the same thing as the let ... in ... from above!

And finally, in list comprehension you get the same assignment syntax as in do-notation.