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