Chapter 6


Tips ⚠️️

  • Use try.purescript.org to test out the code examples in this book.
  • Whenever the example code starts with module Main where, make sure to clear out the code editor on try.purescript.org before pasting new code in. This will help to avoid unncessary errors

Variables in functions

We've talked about creating variables in chapter 1, but these variables were created at the top level. Although variables declared at the top level can be useful in their own right, variables are most commonly used within functions. There are two ways to declare variables within our functions...

let bindings

The first way to create a variable in a function is using the let keyword. Try out this example:

module Main where
import Prelude

addThenMult :: Int -> Int -> Int
addThenMult a b =
  let
    sum = a + b
  in
    sum * 2

As you can see above, the general form for declaring let variables is as follows

function x =
  let
    name1 = value1
    name2 = value2
  in
    name1 + name2

That is, first we type let, followed by our variable declarations (we can have as many declarations here as we want), followed by the in keyword and finally the rest of the function. What's happening here is that the variables declared in the let statement are assigned values, and then once we move to the lines past the in keyword, we can then use those variables in our final calculation.

Let's look at a more motivating example.

Complex functions

Can you tell me what this function does?

mysteryFunction :: Number -> Number
mysteryFunction p =
  p - (p * 0.24) - (p * 0.05)

The above function just looks like random number crunching! Even if you could deduce exactly what its doing, we haven't done ourselves any favors by typing it out this way. If we had used variables, the intent of this function would have been much clearer. Let's try that again..

lessMysteriousFunction :: Number -> Number
lessMysteriousFunction p =
  let
    price = p
    couponDiscount = price * 0.24
    memberDiscount = price * 0.05
  in
    price - couponDiscount - memberDiscount

Ah! Now we have a much better idea of what this function does. Its calculating the new price for some item after applying a coupon and a membership discount. (If you consider 5% to be a good discount that is; I don't. I think you all know which store im looking at here...)

As we can see above, variables used within functions help us to clarify what our functions do.

We have another way of declaring local variables for our functions, lets look at those next.

where bindings

The second way to create variables in functions is using the where keyword. Lets try out the first example again, but using where this time.

module Main where
import Prelude

addThenMult :: Int -> Int -> Int
addThenMult a b =
  sum * 2
  where
  sum = a + b

where statements are a little bit simpler than the let version. They use the general form

function x =
  name1 + name2
  where
  name1 = value1
  name2 = value2

And revisiting our discount calculating function, we would have the following

lessMysteriousFunction :: Number -> Number
lessMysteriousFunction p =
  price - couponDiscount - memberDiscount
  where
  price = p
  couponDiscount = price * 0.24
  memberDiscount = price * 0.05

Local Variable Type Declarations

Variables declared within functions using let or where can be given type declarations if you want to add them. They're not required, but are often helpful to clarify the intent of your code. You add type declarations to local variables in the usual way:

let statement type declarations

module Main where
import Prelude

calculateDiscount :: Number -> Number
calculateDiscount price =
  let
    couponDiscount :: Number
    couponDiscount = price * 0.24

    memberDiscount :: Number
    memberDiscount = price * 0.05
  in
    price - couponDiscount - memberDiscount

where statement type declarations

module Main where
import Prelude

calculateDiscount :: Number -> Number
calculateDiscount price =
  price - couponDiscount - memberDiscount
  where
  couponDiscount :: Number
  couponDiscount = price * 0.24

  memberDiscount :: Number
  memberDiscount = price * 0.05

Nested Functions!

A very inquisitive person may realize that these let and where variable declarations don't look a whole lot different from our function declarations, and they'd be right. We can use let and where statements to create functions inside of our functions too! The act of creating variables inside of variables or functions inside of functions is called nesting. Using let and where, we can do just that.

Lets modify our calculateDiscount function to add a helper function that does the actual percent off calculations for us:

module Main where
import Prelude

calculateDiscount :: Number -> Number
calculateDiscount price =
  price - couponDiscount - memberDiscount
  where
  couponDiscount :: Number
  couponDiscount = percentOff 0.24

  memberDiscount :: Number
  memberDiscount = percentOff 0.05

  percentOff :: Number -> Number
  percentOff percent = price * percent

Details and Caveats

Superficially, one difference between let and where is that let variables are declared at the top of the function and require the in keyword to specify how the variables will be used. The where statement always goes at the end of the function, and the in keyword isn't used.

I also wanted to take a second to mention that let and where variables aren't restricted to only the use of functions, you can also use them in variable declarations in the exact same way!

There are a couple of other strange little behaviors with regards to how exactly let and where variables work. I don't think its necessarily important to be bogged down with these details, but I will leave this section here in case you're interested.

The details

As I mentioned above, there are a couple of weird little behaviors around let and where variables, particularly with regards to scope. By that I mean, there are different rules for which variables we can use when creating these local variables. This is sounding confusing, let's just dive into the different cases.

1. Function Parameters

When creating local variables using both let and where statements, you can use the parent functions parameters as part of the definition.

Example:

module Main where
import Prelude

fn1 :: Int -> Int
fn1 theParameter =
  let
    -- We can use `theParameter` here in the definition of `result`
    result = theParameter + 10
  in
    result

fn2 :: Int -> Int
fn2 theParameter =
  result
  where
  -- Here too!
  result = theParamter + 10

2. let and where variables can refer to each other

You can define let variables using other let variables, and the same goes for where variables.

module Main where
import Prelude

testFunc :: Int -> Int
testFunc x =
  let
    a = x + 10

    -- b uses the variable `c` in its definition - this is ok
    b = c + 10

    -- c uses the variable `a` in its definition - this is also ok
    c = a * 20
  in
    b/2

testFunc2 :: Int -> Int
testFunc2 x =
  b/2
  where
  a = x + 10

  -- b uses the variable `c` in its definition - this is ok
  b = c + 10

  -- c uses the variable `a` in its definition - this is also ok
  c = a * 20

3. You can use let and where variables at the same time

Yep, there's nothing stopping us from using both types of local variables, and this happens relatively often I'd say...

module Main where
import Prelude

someFunc :: Int -> Int
someFunc x =
  let
    a = x + 10
  in
    a + b
  where
  b = 100

4. Where variables are higher scope than let

You can define let variables using where variables, but not vice-versa. This is because variables created using let are only available for use within the in statement. Since where variables aren't part of the in statement, they can't see or use the let variables.

module Main where
import Prelude

-- This function is ok
goodFunction :: Int -> Int
goodFunction x =
  let
    a = y + 10
  in
    a * 2
  where
  y = x * 10

-- This function won't compile because `a` doesn't exist when we try to create `y`
badFunction :: Int -> Int
badFunction x =
  let
    a = x + 10
  in
    y * 2
  where
  y = a * 10

5. let and where statements can be nested

We can use let and where statements in the definitions of our let and where variables. You can also use let statements inside of in statements. That may either sound confusing or outlandish, but these scenarios do occur as well. As usual, I will demonstrate what I mean with examples.

module Main where
import Prelude

letInLet :: Int -> Int
letInLet x =
  let
    a =
      let
        -- This `b` only exists here in the definition of `a`
        b = 10
      in
        b + x
  in
    -- `b` doesn't exist here. Trying to use it would cause an error
    a + 10

letInWhere :: Int -> Int
letInWhere x =
  -- `b` doesn't exist here. Trying to use it would cause an error
  a + 10
  where
  a =
    let
      -- This `b` only exists here in the definition of `a`
      b = 10
    in
      b + x

whereInWhere :: Int -> Int
whereInWhere x =
  -- `b` doesn't exist here. Trying to use it would cause an error
  a + 10
  where
  a =
    -- This `b` only exists here in the definition of `a`
    b + x
    where
    b = 10

whereInLet :: Int -> Int
whereInLet x =
  let
    a =
      -- This `b` only exists here in the definition of `a`
      b + x
      where
      b = 10
  in
    -- `b` doesn't exist here. Trying to use it would cause an error
    a + 10

letInInStatement :: Int -> Int
letInInStatement x =
  let
    -- `b` doesn't exist here. Trying to use it would cause an error
    a = x + 10
  in
    -- This `b` only exists here within the `in` statement
    let
      b = 20
    in
      a + b

I believe creating let variables within in statements actually is outlandish. I don't think this is used all that often, but you are technically "allowed" to do it. I would like to make it a point to say, you don't have to do any of these things if you don't want to. Stick with what makes sense. You may likely use nested where or lets at some point, but don't try to use them unncessarily; you'll know when you need to use this type of thing.

Summary

You can declare variables and functions within your functions using let and where statements. let variables require the use of the in keyword to get the final result of the let statement. You can also add type declarations to let and where variables/functions just like any other top-level declaration. Here's an example of each

module Main where
import Prelude

letExample :: Int -> Int
letExample x =
  let
    a :: Int
    a = x * 20

    fn :: Int -> Int
    fn num = num * 2
  in
    fn (a + 10)

whereExample :: Int -> Int
whereExample x =
  fn (a + 10)
  where
  a :: Int
  a = x * 20

  fn :: Int -> Int
  fn num = num * 2

Using local variables within your functions helps to clarify what the code does, so be sure to use them!


Self Practice

1. The perimeter of a rectangle is calculated using the formula

2*L + 2*W

Write a function that takes 2 parameters, length and width, and declares two local variables using let; use one of these let variables to hold the result of 2 * length and the next to hold the result of 2 * width. Finally, use those two variables to compute the perimeter and return the result
(Note: Be careful, variable names can't begin with numbers)

2. Repeat question 1, but use where variables instead


Answers

Question 1.

module Main where
import Prelude

calculatePerimeter :: Int -> Int -> Int
calculatePerimeter length width =
  let
    twoL = 2 * length
    twoW = 2 * width
  in
    twoL + twoW

Question 2.

module Main where
import Prelude

calculatePerimeter :: Int -> Int -> Int
calculatePerimeter length width =
  twoL + twoW
  where
  twoL = 2 * length
  twoW = 2 * width