Personal tools

Let vs. Where

From HaskellWiki

(Difference between revisions)
Jump to: navigation, search
(where and eta expansion)
(Improved lay-out)
(4 intermediate revisions by 3 users not shown)
Line 1: Line 1:
 
Haskell programmers often wonder whether to use <hask>let</hask> or <hask>where</hask>.
 
Haskell programmers often wonder whether to use <hask>let</hask> or <hask>where</hask>.
This seems to be only a matter of taste in the sense of "[[Declaration vs. expression_style]]",
+
This seems to be only a matter of taste in the sense of "[[Declaration vs. expression style]]", however there is more to it.
however there is more about it.
 
   
It is important to know that <hask>let ... in ...</hask> is an expression,
+
It is important to know that <hask>let ... in ...</hask> is an expression, that is, it can be written wherever expressions are allowed. In contrast, <hask>where</hask> is bound to a surrounding syntactic construct, like the [[pattern matching]] line of a function definition.
that is, it can be written wherever expressions are allowed.
 
In contrast to that, <hask>where</hask> is bound to a surrounding syntactic construct,
 
like the [[pattern matching]] line of a function definition.
 
   
 
== Advantages of let ==
 
== Advantages of let ==
   
Consider you have the function
+
Suppose you have the function
 
<haskell>
 
<haskell>
 
f :: s -> (a,s)
 
f :: s -> (a,s)
Line 56: Line 56:
 
= let a = w x
 
= let a = w x
 
in case () of
 
in case () of
_ | cond1 x = a
+
_ | cond1 x -> a
| cond2 x = g a
+
| cond2 x -> g a
| otherwise = f (h x a)
+
| otherwise -> f (h x a)
 
</haskell>
 
</haskell>
   
Line 129: Line 129:
 
fib' n = fib (n - 1) + fib (n - 2)
 
fib' n = fib (n - 1) + fib (n - 2)
 
</haskell>
 
</haskell>
you will notice, that the second one runs considerably slower than the first one.
+
you will notice, that the second one runs considerably slower than the first one. You may wonder, why just adding the explicit argument to <hask>fib</hask> (known as [[eta expansion]]) reduces the performance dramatically.
You may wonder, why just adding the explicit argument to <hask>fib</hask> (known as [[eta expansion]])
 
reduces the performance dramatically.
 
   
 
You might see the reason better, if you rewrite this code using <hask>let</hask>.
 
You might see the reason better, if you rewrite this code using <hask>let</hask>.
Line 148: Line 148:
 
in map fib' [0 ..] !! x
 
in map fib' [0 ..] !! x
 
</haskell>
 
</haskell>
. In the second case <hask>fib'</hask> is (re-)defined for every argument <hask>x</hask>,
+
. In the second case <hask>fib'</hask> is (re-)defined for every argument <hask>x</hask>, thus it cannot be floated out.<br><br>
thus it cannot be floated out.
+
 
In contrast to that, in the first case <hask>fib'</hask> can be moved to the top level by a compiler.
 
In contrast to that, in the first case <hask>fib'</hask> can be moved to the top level by a compiler.
 
The <hask>where</hask> clause hid this structure
 
The <hask>where</hask> clause hid this structure

Revision as of 06:11, 24 October 2012

Haskell programmers often wonder whether to use
let
or
where
.

This seems to be only a matter of taste in the sense of "Declaration vs. expression style", however there is more to it.

It is important to know that
let ... in ...
is an expression, that is, it can be written wherever expressions are allowed. In contrast,
where
is bound to a surrounding syntactic construct, like the pattern matching line of a function definition.

Contents

1 Advantages of let

Suppose you have the function

f :: s -> (a,s)
f x = y
   where y = ... x ...
and later you decide to put this into the
Control.Monad.State
monad.

However, transforming to

f :: State s a
f = State $ \x -> y
   where y = ... x ...
will not work, because
where
refers to the pattern matching
 f =
, where no
x
is in scope. In contrast, if you had started with
let
, then you wouldn't have trouble.
f :: s -> (a,s)
f x =
   let y = ... x ...
   in  y

This is easily transformed to:

f :: State s a
f = State $ \x ->
   let y = ... x ...
   in  y

2 Advantages of where

Because "where" blocks are bound to a syntactic construct, they can be used to share bindings between parts of a function that are not syntactically expressions. For example:

f x
  | cond1 x   = a
  | cond2 x   = g a
  | otherwise = f (h x a)
  where
    a = w x
In expression style, you might use an explicit
case
:
f x
  = let a = w x
    in case () of
        _ | cond1 x   -> a
          | cond2 x   -> g a
          | otherwise -> f (h x a)

or a functional equivalent:

f x =
   let a = w x
   in  select (f (h x a))
          [(cond1 x, a),
           (cond2 x, g a)]

or a series of if-then-else expressions:

f x
  = let a = w x
    in if cond1 x
       then a
       else if cond2 x
       then g a
       else f (h x a)
These alternatives are arguably less readable and hide the structure of the function more than simply using
where
.

3 Lambda Lifting

One other approach to consider is that let or where can often be implemented using lambda lifting and let floating, incurring at least the cost of introducing a new name. The above example:

f x
  | cond1 x   = a
  | cond2 x   = g a
  | otherwise = f (h x a)
  where
    a = w x

could be implemented as:

f x = f' (w x) x
 
f' a x
  | cond1 x   = a
  | cond2 x   = g a
  | otherwise = f (h x a)
The auxiliary definition can either be a top-level binding, or included in f using
let
or
where
.

4 Problems with where

If you run both

fib = (map fib' [0 ..] !!)
    where
      fib' 0 = 0
      fib' 1 = 1
      fib' n = fib (n - 1) + fib (n - 2)

and

fib x = map fib' [0 ..] !! x
    where
      fib' 0 = 0
      fib' 1 = 1
      fib' n = fib (n - 1) + fib (n - 2)
you will notice, that the second one runs considerably slower than the first one. You may wonder, why just adding the explicit argument to
fib
(known as eta expansion) reduces the performance dramatically. You might see the reason better, if you rewrite this code using
let
.

Compare

fib =
    let fib' 0 = 0
        fib' 1 = 1
        fib' n = fib (n - 1) + fib (n - 2)
    in  (map fib' [0 ..] !!)

and

fib x =
    let fib' 0 = 0
        fib' 1 = 1
        fib' n = fib (n - 1) + fib (n - 2)
    in  map fib' [0 ..] !! x
. In the second case
fib'
is (re-)defined for every argument
x
, thus it cannot be floated out.

In contrast to that, in the first case
fib'
can be moved to the top level by a compiler. The
where
clause hid this structure and made the application to
x
look like a plain eta expansion, which it is not.