Practical Programming

by Vance Palacio


Welcome to my Programming book!

I've written this book as a bit of a thought experiment but my hope is that I might tear down the "wall of intimidation" regarding this field we affectionately call Software Engineering.

Programming isn't hard! Sure, there are some feats we attempt in programming that are quite tough, but the act of programming in itself is not hard. Programming just requires a new way of thinking, and adapting to this takes time and careful analysis on the part of the student.

I believe the perceived difficulty people experience when approaching a new concept is due to our teachers moving too quick. If we want to learn something at a deep level, we need to take time and study the examples; and the examples need be full and complete. I've attempted to do this here in my book, and I hope you will find it to be a easily digestable in comparison to any past experiences you may have had.

This book is aimed towards non-programmers and programmers alike. Move through the book at whatever pace feels comfortable for you, but keep in mind it's imperative that you fully understand the examples if you wish to make swift progress. If you try to gloss over the examples without fully understanding them, this may make a quick end to your learning experience. If the examples are just too hard to understand, then I have done something wrong, and you should contact me so that I may clarify these sections of the book!

I wish you the best in your learning!


P.S

Feedback is welcome! If you have any suggestions/concerns/difficulties, feel free to contact me at book@vanceism7.ml

Thanks again!




Practical Programming © 2020 by Vance Palacio is licensed under CC BY-NC-SA 4.0. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/

Chapter 0

What is Programming?

To answer this question, it's probably helpful to answer another: What is a computer? Practically speaking, a computer is a data tool. Computers store vast amounts of data (such as music, photos, email, homework, social media profiles, etc) and allow us to retrieve it later. They also provide safeguards to ensure we interact with our data correctly. Chances are, if you had to (and could) manually upload a photo on instagram without the app, it would take you a lot longer than 20 seconds to do it.

But what is Programming? Programming is feeding instructions to a computer to make it do something. Need to tell your phone to wake you up at 5:30 AM? Or do you wish you had a calculator to do your algebra homework for you? Do you need to launch the missiles? Any time you convince a device to carry out a task for you, you are in effect, programming. Some tasks are more difficult than others, but with enough diligence and creativity, it can probably be done.

So how do we harness this power? It all comes down to data. At the heart of programming, we have two main tools at our disposal: Input and Output. Know something the computer doesn't (like your Gmail username)? Tell the computer what it is (input) and it'll be able to use that. Do you need info from the computer? Maybe a new email came in and you want to read it? Ask it to retrieve that data and it will show you (output). Computers act on input and respond with output.

Wait a second, you might say, setting an alarm clock is not really "programming!" Ah, the principal difference between setting an alarm clock and programming is only in the choice of inputs you are allowed. An alarm clock allows only a few specific inputs: The time it should ring, which days it should ring, alarm volume, which alarm sound should be used, etc. When we write software, the variations of input are much greater, and also much more primitive. We don't deal with alarm times, or days of the week; we deal with numbers, letters, and symbols. This may sound very limiting, but like legos, we can build some very interesting things from some very simplistic parts.

Following Along

You can follow along on your computer at try.purescript.org (this doesn't work on phones unfortunately). When the website loads, delete everything in the textbox on the left side of the page except the top three lines. Your code page should read the following:

module Main where

import Prelude

As you follow along, notice that any time you type anything, there are various symbols appearing on the left. If you move your mouse over these symbols, you'll see different warnings and errors from the compiler. These messages are here to help and guide you.
What's a compiler? It's a tool that translates your code (the text you type) into instructions that can be run by the computer. Writing code can be tough, and sometimes we'll make mistakes; whenever the compiler detects errors in your code, it will give you a description of the error along with which line the error has occurred. It will also show warnings as an ⚠️ symbol. You may safely ignore those for the time being. Don’t feel compelled to type down every codebox in this book, I will inform you of which ones should be typed. However, feel free to experiment as you see fit.

Chapter 1

Computers memorize things

When we set an alarm clock, we pick our inputs and the alarm remembers them and uses them; when we program, the same principles apply. Let's try out a few examples.

Let's say we want to instruct the computer to remember a number for us. Maybe it's our lucky number. To do this, type the following on a blank line:

luckyNumber = 74

That's it. Whenever this program runs, if you ask it what luckyNumber is, it will say 74. How about our name?

name = "Scrooge McDuck"

There we go, now we are Scrooge McDuck! Notice the quotes above. Names aren't numbers (usually), but a string of text instead. Whenever we want the computer to remember some text, we put that text in quotes.

Variable Declarations

These pieces of data we're storing in our program are called variables. Variables have a name and a value. The act of creating a variable is called a Variable Declaration. You declare them using the general form:

name = value

The left side of the equals sign is the name of the variable, and the right side of the equals is the value given to it. Like our examples, you'll want to name your variables based on what they represent. If you want to store the name of your college for example, collegeName might be a good variable name to use.

collegeName = "Smart People University"

If you want to store the name of your favorite food, favoriteFood might be good. Note: Spaces arent allowed in variable names

favoriteFood = "Super Burrito"

But hmm, what's the point of all of this? You could just as easily type this up in a text document! The difference is, once the computer has some variables in memory, we can do things with them… Let's try some math then!

Addng Variables

Add the following to your code box on a blank line:

johnsMoney = 150
bensMoney = 300
total = johnsMoney + bensMoney

We create variables using the name = value syntax as stated above. This assigns the value to that variable name. If we use these variable names later in the code, the computer substitutes the variable with it's value. So in the line total = johnsMoney + bensMoney

this literally translates to total = 150 + 300

We're also creating a new variable called total here, and its assigned value will be the result of 150 + 300, i.e 450

Why not just write total = 300 + 150? Have you ever read one of those math problems that says something like "Ben has twice as much money as john."? Using variables (and *, the multiplication operator), we can express this more clearly

johnsMoney = 200
bensMoney = johnsMoney * 2

Ah, now no matter what value we give johnsMoney, bensMoney will always be double. Change johnsMoney to 400 and bensMoney will also be changed. If we had done something like

johnsMoney = 200
bensMoney = 400

or

johnsMoney = 200
bensMoney = 200 * 2

We'd have to update bensMoney anytime we changed johnsMoney to keep accurate with the original math problem. The relationship between John and Ben's money would be less apparent as well.

Immutability

By the way, I wanted to take a second to note that once a variable has been assigned a value, you can't change it later. For example, you can't do the following:

module Main where
import Prelude

johnsMoney = 100
johnsMoney = 400

The inability to update a variable's value is called Immutability. It might seem limiting but it helps us avoid a lot of problems down the road. But enough about that, lets move forward


Summary

Variables store data for us.

Variables are declared using the general syntax name = value

Heres an example of declaring some variables

myVariable = 100

otherVar = "hello"

Self Practice

Create 3 variables to store your name, age, and your most hated dessert! Once you're finished, expand the example answer below to see if what you have looks similar. Who knows, maybe we even hate the same dessert!

Answer
module Main where
import Prelude

name = "Vance Palacio"
age = 32
topHatedDessert = "Mint Chocolate"

If what you've done looks similar to the above, and you have no compile errors, then good job! It's ok if you named your variables differently, as long as they make sense!

Let's try a more interesting example

Chapter 2

Types

Erase everything in your code box and then type the following into your program:

module Main where
import Prelude

luckyNumber = 74
name = "Scrooge McDuck"
answer = luckyNumber + myName

Ah wait, what happened? In the above example, we told the computer to do 74 + "Scrooge McDuck". But as I'm sure you're well aware, you can't add numbers and words. This is because they aren't the same types of things. When we create variables, each one has a specific type. The type given to the variable is mostly based on the value you give it. Different types of values will have different types. Numbers will have types like Int (as in Integer) or Number, text will usually have the type String. Some actions, like addition, only work when both operands are numeric types. In the above example, the compiler helps us by informing us that the types of our variables aren't right for addition. The compiler will be your best friend when writing programs, it makes sure that the things you tell the computer to do make sense. For example, if you replace your definition of luckyNumber with the following, the compiler will complain:

luckyNumber :: Int
luckyNumber = "Hello Fred"

Oh whoa wait up now! What does this weird luckyNumber :: Int line mean? This is called a Type Declaration, they go above the variable definition. It’s basically saying “Hey compiler, I want you to know that luckyNumber should be an Int. You can read the symbol :: to mean “has the type” or “is the type”. So luckyNumber :: Int can be literally read as “luckyNumber has the type Int”. Most of the time, the compiler can infer the types of your variables based on the value you give it, but often it's helpful to inform the compiler of the types you intend to use explicitly. The more often you use type declarations, the more clear error messages will be.

The primitive types

There's a basic set of types you will find in just about every programming language. Here's a list of our primitives in purescript:


Int is the type of non-decimal numbers, like 10 or 450

integer :: Int
integer = 45

Number is the type of fractional/decimal numbers, like 3.14, or 45.734

decimal :: Number
decimal = 34.5

String is the type of text

greetingMessage :: String
greetingMessage = "Welcome to my glorious mansion"

Char is the type of single character text. (I dont think this type is all that useful) To create a char, you use single quotes ' instead of double "

favoriteLetter :: Char
favoriteLetter = 'H'

Boolean is the type to represent true and false.

areYouCool :: Boolean
areYouCool = true -- I sure think so ^_^

Unit is a bit different of a type. It contains only a single value. It's sort of like a Boolean but instead of having a true and false, it only has a true. That doesn't sound very useful! Well the point of it is that we use it in cases when we don't care what our value is. You'll learn more about this when we begin taking a deeper look at functions.

The single value belonging to the type Unit is unit. Here's an example of a Unit variable:

uselessValue :: Unit
uselessValue = unit

Void is a type which contains no values at all! You can't create a Void because there's no values we can use to create it. Although this is starting to sound very weird (even in comparison to Unit), it does have some legitimate uses.

I would show an example of a Void variable but since we can't create them, there's nothing to show. (There is technically a way to create them, but this is too early to introduce those concepts)


There are other more complex types aside from the primitives, we'll introduce those in the up and coming chapters

Summary

A Type Declaration informs the compiler of what type you intend your variables and functions to have (We'll get to functions soon).

Type declarations go above the variable's definition.

Heres an example type declaration

coolNum :: Int -- Type declaration
coolNum = 5 -- variable definition

The primitive types are Int, Number, String, Char, and Boolean.


Self Practice

Add type declarations to the variables below

module Main where
import Prelude

coolNumber = 50

collegeSavings = 4000

personsName = "John Wiggly"

When you think you've got it, check what you put against the answer below

Answer
module Main where
import Prelude

coolNumber :: Int
coolNumber = 50

collegeSavings :: Int
collegeSavings = 4000

personsName :: String
personsName = "John Wiggly"

Chapter 3

Comments

Before we continue onward, I wanted to take a brief detour to talk about comments. Comments are code that's completely ignored. You can use them to describe code in your program. This helps other readers (and yourself) to more easily understand the purpose of your code. You create comments by typing a --. Anything following the -- will be ignored by the compiler.

-- This is a comment.
-- You can type anything here,
-- it won't break the program

-- In the equation of a line, m is slope
m = 2

-- Equation of a line y = mx + b
-- where x = 6, b = 12
y = (m * 6) + 12

z = 15 -- The third dimension

These are examples of single line comments. They only effect the line they occur on and only everything after the --.

We also have multi-line comments. They start with the {- characters and end with -}. Everything you type between them is counted as comment Here's an example:

{-
This comment spans
multiple lines.

Set x to 70, the amount of
calories in an egg
-}
x = 70

A simple concept but very useful, especially when you find yourself looking back on code you wrote 8 months ago, or reading code someone else wrote.

Chapter 4

Functions

Using the example from earlier, what if we wanted a way to double anyone's money? Imagine this scenario: we have 4 different people with money in the bank

module Main where
import Prelude

johnBank = 100
benBank = 150
jerryBank = 700
tonyBank = 15 -- tony is poor :(

Now we want to have a way to ask the computer to double the money in each of their accounts. We could do this:

john2x = johnBank * 2
ben2x = benBank * 2
-- etc etc...

But this is tedious and painful. Often when writing programs, we will have hundreds or thousands of pieces of data. Surely we don't want to create by hand a double amount for every one. We're doing the same operation every time, there must be a better way! Functions my friend. Here's an example of a function declaration (i.e creating a function)

doubleTheBank money = money * 2

This looks similar to a variable declaration, but a little different as well. Let's compare them side by side

-- Variable declaration
theBank = 400

-- Function Declaration
doubleTheBank money = money * 2

doubleTheBank appears to have a second name, money, appearing before the equals sign, what is this? It's called a function parameter, you can think of it as a placeholder; let me explain. When you create a variable like theBank = 400, we immediately assign the value 400 to it. But in doubleTheBank money = money * 2, money doesn’t have a value. Without a value assigned to money, we can’t use doubleTheBank!

So how do we assign it a value? When we use the function, we put a value or variable in the spot where the name money was; money gets assigned that value, then our function comes to life!

-- If we have this
   doubleTheBank money = money * 2
-- then,
-- doubleTheBank 400   = 400   * 2
-- doubleTheBank 20    = 20    * 2
-- doubleTheBank 600   = 600   * 2

Putting different numbers in the place of money assigns that value to it! So then, with all of that said, let us show a complete example of using our function

doubleTheBank money = money * 2
-- Below, we use doubleTheBank, putting 400 in the spot where money was
answer = doubleTheBank 400
-- answer = 400 * 2 = 800

The value that comes back from a function is called it's return value. Functions aren't named values the way variables are, but they return a value back to you based on it's definition.

Here are a few more examples of ways we could use doubleTheBank

doubleTheBank money = money * 2
lunchMoney = 20
answer = doubleTheBank lunchMoney
-- answer = lunchMoney * 2 = 20 * 2 = 40

extraCash = doubleTheBank (50+10)
-- extraCash = (50+10) * 2 = 60 * 2 = 120

quadruple = doubleTheBank (doubleTheBank 200)
-- quadruple = (doubleTheBank 200) * 2 = (200*2) * 2 = 400 * 2 = 800

What's really happening here? It turns out money is actually a variable! But it only exists within the scope of the doubleTheBank function. What’s scope? Let's talk about that.

Scope

Scope is the boundaries in which a variable exists. For example

module Main where
import Prelude

coolNumber = 20
otherNum = coolNumber + 10

multNumber num = num * coolNumber

In the above code block, coolNumber exists within the entire Main module. Notice we can use it in other places such as when defining otherNum and the multNumber function. However, the function parameter num only exists within the multNumber function. If we tried to use num outside of multNumber, we'll get an error

E.g:

doubleTheMoney money =
  -- money exists here
  money * 2

-- money doesn't exist here
answer = money * 2

Scope also gives us some naming rules. Each variable name can only be used once within it's scope. So for example, once we create the variable money within our module, we can't use that same name again.

-- declare the variable money
money = 400

-- try to declare it again.
money = 250

-- The compiler will think you're trying to change the value of money and complain.
-- Don't play with the value of money, it's bad for the economy

But because function parameters only exists within the scope of the function, you can reuse parameter names as often as you wish

-- This someNum only exists in add10
add10 someNum = someNum + 10

-- This is ok because it's a different scope from add10
multiply15 someNum = someNum * 15

And one final note on the topic. Some scopes are more universal than others. Module scope declarations (often referred to as top level) exist in more places than function scope. This means if you create a variable in the module scope, and then use that same name for a function parameter, the compiler will yelp a little bit. This situation is called shadowing.

module Main where
import Prelude

coolNum = 400

-- coolNum parameter shadows the coolNum variable declared above
addFunc coolNum = coolNum + 32

When shadowing occurs, the function will use it's parameter value over the variable declared at the top level. Shadowing is allowed but somewhat discouraged. The compiler will give you warnings not to do it.

Types of Functions

We talked about types before when declaring variables, and we can declare types for our functions as well. What's the type of our doubleTheBank function? Let's see

doubleTheBank :: Int -> Int
doubleTheBank money = money * 2

That -> is new. Whatever is to the right of the last -> is the type of the functions return value. Everything to the left of the -> are the types of the function's parameters. The types help us to make sure the function receives the correct types of parameters and uses the correct return type.

module Main where
import Prelude

-- This breaks because the function returns a String instead of an Int
brokenFunc :: Int -> Int
brokenFunc num = "hello!"

-- This definition is ok...
brokenParam :: Int -> Int
brokenParam num = num + 4

-- But this breaks because we used a string parameter instead of an Int
answer = brokenParam "cool beans!"

Once again, it's considered a best practice to add type declarations for as many of your variables and functions as is practical. Indeed, the compiler will issue warnings for any top level variables or functions that don't have accompanying type declarations.

Multi-parameter functions

Everything to the left of the -> are the types of the function's parameters.

What does this mean? It means we can use more than one function parameter!

addNums x y = x + y

What is the type of addNums? Wager a guess..?

addNums :: Int -> Int -> Int
addNums x y = x + y

How about a triple adder!?

tripleAdder :: Int -> Int -> Int -> Int
tripleAdder x y z = x + y + z

So here you see that we can essentially use as many parameters for our functions as we want! However, it's recommended to not use too many parameters for a function, otherwise the function becomes difficult to use!


Summary

Functions give us a way to run a bunch of code without having to rewrite that code over and over again.

Functions can use parameters to run the same code against different values.

Function types use the -> to separate parameters. Whatever type appears to the right of the last -> is the type of the function's return value. Everything to the left of the final -> is a parameter.

The parameters of the function only exist within the function definition, this is called scope.

Heres an example of a function declaration

coolFunc :: Int -> Int -> Int
coolFunc num1 num2 =
  num1 + num2 + 100

Self Practice

  1. Create a function that subtracts two numbers, x and y, and then multiplies them by the first number x. You subtract numbers using the - operator. Just like math, you can use ( ) to enforce order of operations

  2. Create a function that computes the square of a number

  3. Create a function that takes a Int parameter and returns a String. Hint: You dont have to do anything with the Int parameter, you can simply ignore it

Note: If you named your function or parameters differently, thats fine, as long as the function definitions are essentially the same.


Answers

Question 1:

subMult :: Int -> Int -> Int
subMult x y = (x - y) * x

Question 2:

squareNum :: Int -> Int
squareNum x = x * x

Question 3:

ignoreNum :: Int -> String
ignoreNum num = "hello!"

Chapter 5

Output

So up to this point, we've typed a lot of code, but we've never seen it really do anything yet. Programming is all about input and output, and writing code could be considered input in its own right. But we're not going to have any fun until we get some output. I held off on the output aspect because I didn't want introduce things out of order, but no more! We're ready, sort of!

The Console

Now that we have functions, we can start displaying our data in the console. The console is a program that allows us to run applications via typing commands; it also provides us a quick and easy way to get input from the user and write output on the screen. We'll use it for output in our case.

If you're using try.purescript.org, you can bring up the console right in your browser by pressing ctrl shift J on linux/windows, or cmd option J on mac. This should work on any chrome-like browsers (Chrome, Chromium, Brave, Edge, etc) and firefox.

Easy output with spy

To get our first taste of writing output to the console, we're going to use a function called spy. This isn't the recommended way to really write output, but it will suffice for the time being.

For our first example, we'll start by writing the values of simple variables to the console.

module Main where
import Prelude
import Debug.Trace

coolInt :: Int
coolInt = spy "Cool num" 24

dumbBool :: Boolean
dumbBool = spy "Dumb!" true

Enter the above code into your code window and then open up the dev console. You should see

Cool num: 24
Dumb!: true

Imports

So whats happening here? We have two new things we haven't really seen before. First, we have the line

import Debug.Trace

We haven't talked about imports yet, but heres a quick rundown. People all over the world are writing code. If they publish that code online, we can pull it into our own program. The code we pull in are called libraries. Libraries are comprised of 1 or more modules.

Here, we import the Debug.Trace module from the library purescript-debug. (A listing of most published libraries can be found at pursuit.purescript.org). We'll take a deeper look into imports in a later chapter.

Polymorphism and the spy function

The next line of interest is:

coolInt = spy "Cool num" 24

This isn't totally weird, whats happening here? As mentioned above spy is a function. Lets look at it's type declaration

spy :: forall a. String -> a -> a

Ah! Another new thing! Whats this weird forall a. line? This is called polymorphism, the term sounds scary, but the concept isnt. forall a. is a sort of type declaration that says: "We have some type called a, and we dont care what it is! You can use anything for a and we'll be ok!"

What does it mean that a can be anything? Lets look at another example. Lets look at a function called identity

identity :: forall a. a -> a
identity x = x

Can you guess what identity does? Its a function that takes one parameter. This parameter can be anything, and all it does is return that parameter back to you.

Lets try it out!

module Main where
import Prelude

coolNum = identity 40
-- coolNum = 40

someText = identity "hello"
-- someText = "hello"

youCanDoIt = identity true
-- youCanDoIt = true

That doesn't seem very useful... Well, I won't go into the explanation of how identity can be useful, but this gives a good demonstration of how polymorphism works. We used three different types of values on the identity function: An Int 40, a String "hello", and a Boolean true; and it happily accepted them. Contrast this with the following

module Main where
import Prelude

numIdentity :: Int -> Int
numIdentity x = x

-- Compile error here
someText = numIdentity "Hello"

The above doesn't work because numIdentity needs it's first parameter to be an Int. This isn't the case with identity, identity says "You can give me anything and I'll still work"! We'll take a closer look at polymorphism later as well.

Back to the spy

So let's look at the type of spy again

spy :: forall a. String -> a -> a

I won't show the actual definition of spy, but essentially, it takes a String and an a (which can be anything); writes them to the console, and then returns the a parameter. So its sort of like identity, but with some hidden side effects.

So hopefully our original block of code makes a little more sense now

module Main where
import Prelude
import Debug.Trace

coolInt :: Int
coolInt = spy "Cool num" 24

dumbBool :: Boolean
dumbBool = spy "Dumb!" true

Spy the functions

The way we used spy up above is not how its typically used. There's no need to use spy on plain old variables, it's more useful in functions.

module Main where
import Prelude
import Debug.Trace

doubleTheBank :: Int -> Int
doubleTheBank money =
  spy "double the money is" (money * 2)

johnBank :: Int
johnBank = doubleTheBank 100

tobyBank :: Int
tobyBank = doubleTheBank (400)

Now we'll see in the console

double the money is: 200
double the money is: 800

Each time doubleTheMoney is called, we'll get some output in the console.

So there we go, here's our first taste of output. There's more official ways to get output, but we need to learn a bit more before we can tackle that task.

Summary

We can use the spy function to output the values of variables to the console.

In the web browser, you can open the comsole by pressing ctrl shift J on linux/windows, or cmd option J on mac (for chrome based browsers and firefox)

In order to use spy, you must import the module Debug.Trace

spy uses a polymorphic parameter, meaning you can use any value for that parameter.

Heres the type declaration for spy

spy :: forall a. String -> a -> a

Heres an example using spy

module Main where

import Prelude
import Debug.Trace

doubleNumber x =
  (spy "Doubling" x) * 2


jakesAge = doubleNumber 20
bensMoney = doubleNumber 4

The above example will output

Doubling: 20
Doubling: 4

Self Practice

Lets write a greeting function. Write a function that takes a string parameter and says hello to it before returning it. For example, if the string is "Mr.X", then in the console, we should see

Hello: Mr.X
Answer
greeting :: String -> String
greeting name =
  spy "Hello" name

Chapter 6

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 discoint 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

Chapter 7

Sum Types

At the beginning of this book, we talked about the two big categories of tools we have at our disposal when writing programs: input and output. When we write code for a program, we're working with a very primitive yet powerful form of input. We create variables to store our data, yet the type of data we can store is restricted to text, numbers, and symbols. We've worked with text via the String type and numbers via the Int and Number types, but now we move on by taking a look at symbols. I want to apologize now because what I've been calling symbols is more formally known as sum types (also ADTs, short for Algebraic Data Types).

What are sum types?

Sum Types are a bit different from the other types we've worked with so far. Unlike String or Int, these types align more closely with real world concepts. For example, perhaps we want a variable that stores the current day of the week, or the grade you got on your last math test; with sum types, we can create these types easily. Let's try it out.

The "Days of the Week" type

module Main where
import Prelude

data DayOfTheWeek
  = Monday
  | Tuesday
  | Wednesday
  | Thursday
  | Friday
  | Saturday
  | Sunday

-- Create a variable to store the current day of the week
currentDay :: DayOfTheWeek
currentDay = Sunday

Alright! We got some new syntax to look at. Let's break this down a bit.

We create new types using the keyword data, followed by the name of our type.
Warning: Type names always start with a capital letter. Like variables and functions, spaces aren't allowed either.

Here's an example:

module Main where
import Prelude

data CoolType

data NotCoolType

This alone is valid syntax and creates the types CoolType and NotCoolType, but if we stop here, what we've created is our own version of Void types - types with no values. To add values to our types, we add an = after our type name followed by a list of values.

module Main where
import Prelude

data CoolType
  = Cool

data NotCoolType
  = UnCool

Both CoolType and NotCoolType now have one valid value. At this point we've moved from creating our own version of Void to creating our own version of Unit! In order to move on to more useful types, we need one more piece of syntax; the | character. The | character could be read as the word "or"; we use it to give our types more than one value. Let's use it to give our types a few more useful values.

module Main where
import Prelude

data CoolType
  = Cool
  | VeryCool
  | SuperCool

data NotCoolType
  = UnCool
  | VeryUnCool
  | AbsolutelyLame
  | UnthinkablyLame

Finally, our types have become much more interesting now. Our type CoolType has three valid values to choose from, and NotCoolType has four. With that explanation behind us, we should be able to understand our original DayOfTheWeek example now:

module Main where
import Prelude

data DayOfTheWeek
  = Monday
  | Tuesday
  | Wednesday
  | Thursday
  | Friday
  | Saturday
  | Sunday

-- Create a variable to store the current day of the week
currentDay :: DayOfTheWeek
currentDay = Sunday

As demonstrated above, to create a variable using that type, we give it a type declaration using the types name. When we assign it a value, we use one of the values listed after the types = sign:

currentDay :: DayOfTheWeek -- Type declaration using the name we gave our type
currentDay = Wednesday -- Value from the list of values

bestDayEver :: DayOfTheWeek
bestDayEver = Friday

humpDay :: DayOfTheWeek
humpDay = Wednesday

Why go through the trouble?

We could use Strings or Ints to do this, but that would be making life hard on ourselves. For example, we could implement our DayOfTheWeek type using Strings like this:

module Main where
import Prelude

currentDay :: String
currentDay = "Sunday"

-- This doesn't make sense
tomorrow :: String
tomorrow = "PlutoDay"

-- What..?
yesterday :: String
yesterday = "Yo yo yo!"

The problem is, nothing is preventing us from entering values that have nothing to do with the days of the week. On the converse, if we use our official DayOfTheWeek type, the compiler protects us from such silly errors:

module Main where
import Prelude

data DayOfTheWeek
  = Monday
  | Tuesday
  | Wednesday
  | Thursday
  | Friday
  | Saturday
  | Sunday

-- Create a variable to store the current day of the week
currentDay :: DayOfTheWeek
currentDay = Sunday

-- This will cause an error: "Unknown type PlutoDay"
tomorrow :: DayOfTheWeek
tomorrow = PlutoDay

-- This will cause an error: "Can't match String with DayOfTheWeek
yesterday :: DayOfTheWeek
yesterday = "Yo yo yo!"

As you can see from the above examples, handling this manually using the primtive types would be taking on a lot of mental burden that the compiler would otherwise automatically take care of for us.

Summary

We can create our own types using the data keyword. The general syntax used to create types using data is as follows:

data YourTypesName
  = Value1
  | Value2

-- And create a variable to use them like this:
someVariable :: YourTypesName
someVariable = Value1

Types created in this way are called sum types. We can use these types to create variables that run closely with real world concepts, such as days of the week, school grades, etc.

We haven't seen everything sum types have to offer just yet, but we'll visit that at a later time.

Self Practice

1. Create a data type to represent school grades and then create a variable giving yourself the best grade you remember getting on a Math test. (Or any other test if you really don't like math).

2. Create a data type for your top five desserts, then create a variable that stores your favorite one. (Use spy to output your answer to the console for extra points!)


Answers

Question 1.

module Main where
import Prelude

data LetterGrade
  = A
  | B
  | C
  | D
  | F

lastMathGrade :: LetterGrade
lastMathGrade = C -- Don't judge, it was a hard class :'(

Question 2.

module Main where
import Prelude
import Debug.Trace

data BestDessert
  = Brownie
  | CinnamonMuffin
  | CinnamonDonut
  | CheeseCake
  | DanishPastry

favoriteDessert :: BestDessert
favoriteDessert = spy "My favorite dessert is" CinnamonMuffin -- The ones from costco man...

Chapter 8

Case statements

It's time to introduce a very important concept; we'll call it decision making. Up to this point, we've worked with various types of input, but we haven't really done anything interesting with that data. Our programs run straight through from start to finish with the same behavior regardless of the data it gets. What we really want is to have a program that does different things based on what we give it!
For example, we might write a program that greets the user differently based on their age, saying "Hello child!" to ages below 18 and "How do you do?" to those above. Another example might be a prize received for acheiving a certain grade in school. Perhaps C grades and lower earn pencils, B grades earns you a candy, and A grades will get you a hamburger! We can write programs that do the above using case statements, let's look at some examples.

To start, we'll stay simple and use our CoolType defined in the previous chapter. Using this type, we'll output a different string based on which CoolType value we're using.

module Main where
import Prelude
import Debug

data CoolType
  = Cool
  | VeryCool
  | SuperCool

-- | Returns a cool string message based on the `CoolType` value
getCoolMessage :: CoolType -> String
getCoolMessage coolVar = case coolVar of
  Cool -> spy "msg" "Cool..."
  VeryCool -> spy "msg" "That seems... very cool..!"
  SuperCool -> spy "msg" "You couldn't be cooler"

message1 :: String
message1 = getCoolMessage VeryCool

message2 :: String
message2 = getCoolMessage SuperCool

If you check the developer console, you should see the following output:

msg: That seems... very cool..!
msg: You couldn't be cooler

The above example shows nearly everything there is to see regarding the basics of case statements. Your intuition may be enough to understand what's happening, but before we break down the example further, let's look at the case statement a little more closely.

Case Basics: The case variable

The syntax for our case statement is as follows

case value of
  case1 -> response1
  case2 -> response2
  ...

The first part is case value of. We can put anything with a value between the case and the of keywords (taking the place of the word value above), for example:

  • A top-level variable
  • A function parameter
  • An actual function call which returns a value
  • Any let or where bindings which yeild a value.
module Main where
import Prelude

data Decision
  = This
  | That

-- 1. Top-level variable
someDecision :: Decision
someDecision = This

topLevelCase :: Int
topLevelCase = case someDecision of
  This -> 10
  That -> 20

-- 2. A function parameter
caseFunction :: Decision -> String
caseFunction decision = case decision of
  This -> "This thing"
  That -> "That thing"

-- 3. A function call
-- We ignore the `num` parameter and just return a `That`
someFunction :: Int -> Decision
someFunction num = That

functionCallCase :: Boolean
functionCallCase = case someFunction 20 of
  This -> false
  That -> true

-- 4. Let/Where bindings in a case
letCase :: String
letCase =
  let
    letVar = That
  in
    case letVar of
      This -> "Hello"
      That -> "Goodbye"

whereCase :: Int
whereCase = case whereVar of
  This -> 20
  That -> 100
  where
  whereVar = This

Case Basics: The case response

The next part to turn our attention to is:

case1 -> response1
case2 -> response2

That is: some value, followed by an -> followed by some sort of response value. What are the values case1, case2, etc? These correspond to the values associated with our type; we'll have as many cases as we do values for that type.

module Main where
import Prelude

--
-- 1. A single value type --
data Boring
  = Boo

-- There's only one case value to work with here: `Boo`.
ex1 :: Boring -> Boolean
ex1 var = case var of
  Boo -> true

--
-- 2. A type with 2 valid values --
data Decision
  = This
  | That

-- We have two valid values for the Decision type, so we have two cases to deal with
ex2 :: Decision -> Boolean
ex2 decision = case decision of
  This -> true
  That -> false

--
-- 3. A Type with three valid values
data CoolType
  = Cool
  | VeryCool
  | SuperCool

-- Our Original CoolType has 3 valid values, so we have three cases to deal with now
ex3 :: CoolType -> Int
ex3 coolVal = case coolVal of
  Cool -> 10
  VeryCool -> 90
  SuperCool -> 9001 -- It's over 9000!

Back to the examples

Ah so now that we've been through a basic run down of case statements, we return to our original example.

module Main where

import Prelude
import Debug

data CoolType
  = Cool
  | VeryCool
  | SuperCool

-- | Returns a cool string message based on the `CoolType` value
getCoolMessage :: CoolType -> String
getCoolMessage coolVar = case coolVar of
  Cool -> spy "msg" "Cool..."
  VeryCool -> spy "msg" "That seems... very cool..!"
  SuperCool -> spy "msg" "You couldn't be cooler"

Hopefully reading this makes a lot more sense now. We check the three possible values we could encounter for the CoolType type, and use spy to output the appropriate message to the console.

We can also use case statements on the primitive types.

module Main where

import Prelude
import Debug

-- | Write to the console a string and return it based on a Boolean (true/false)
areYouCool :: Boolean -> String
areYouCool bool = case bool of
  true -> spy "msg" "I knew you were!"
  false -> spy "msg" "Aw cmon, you're cooler than you think!"

It's also possible to use case statements on bigger types, like Int or String, but we aren't equipped to deal with that just yet. Case statements rely on Pattern Matching to inspect the values, and we've only used the simplest of pattern matches here. In the next chapter, we'll go into a deeper exploration of Pattern Matching to learn how to handle a couple of the more complicated and common scenarios.

Finally, one last thing to take note of: Because case statements yield a value just like a variable or a function, all the different response values of our case statement must be the same type. If you try to use a different type in each response, you'll be met with an error.

module Main where
import Prelude

-- This function won't compile
badCaseFunc :: Boolean -> String
badCaseFunc bool = case bool of
  true -> "Hello!"
  false -> 54

The above is wrong because the case statement tries to return a String in the true case but an Int in the false case (Also because badCaseFunc should return a String in general).

Summary

We use case statements to inspect the values of a type, allowing us to react in different ways to the different possibilites.

The syntax of a case statement takes the general form

case value of
  case1 -> response1
  case2 -> response2
  ...

Where

  • value is some value (like a variable)
  • Each case covers a different possible value for value
  • Each response following the -> gives us a way act differently based on each case.
  • And each response must be of the same type

Here's an example of a simple case statement

module Main where
import Prelude

data Greeting
  = Hello
  | Hi
  | Yo

responseToGreeting :: Greeting -> String
responseToGreeting greeting = case greeting of
  Hello -> "Hello to you"
  Hi -> "Hey!"
  Yo -> "Yo yo yo!"

Self Practice

1. Using the following type:

data LetterGrade
  = A
  | B
  | C
  | D
  | F

Write a function called prize which outputs a string to the console telling the user what prize they have won for their efforts. Create 2 variables and assign them the result of our prize function so you can see it work in the console.

2. Using the following type:

data BetterBool
  = Yes
  | No

Create a function called upgradeBool which takes a Boolean and turns it into a BetterBool. Next, create a function called downgradeBool which takes a BetterBool and turns it back into a Boolean.


Answers

Question 1.

module Main where
import Prelude
import Debug

data LetterGrade
  = A
  | B
  | C
  | D
  | F

prize :: LetterGrade -> String
prize grade = case grade of
  A -> spy "A Prize" "In N Out"
  B -> spy "B Prize" "Little Ceasars"
  C -> spy "C Prize" "2oz cup frozen yogurt"
  D -> spy "D Prize" "Jolly Rancher"
  F -> spy "F Prize" "One potato chip"

reward1 :: String
reward1 = prize B

reward2 :: String
reward2 = prize F

Question 2.

module Main where
import Prelude

data BetterBool
  = Yes
  | No

upgradeBool :: Boolean -> BetterBool
upgradeBool bool = case bool of
  true -> Yes
  false -> No

downgradeBool :: BetterBool -> Boolean
downgradeBool better = case better of
  Yes -> true
  No -> false

Chapter 9

Pattern Matching

As you can see from above, the more values our type has, the more cases we have to deal with! What if we have a type with 10 different cases, and we only want to do something different for one of them? For example, maybe we have a guessing game, where the correct answer returns a "You got it!", but all the wrong answers return a "Sorry, better luck next time!". How do we handle such a scenario?

You might be tempted to try the following:

module Main where

import Prelude

data Food
  = Donuts
  | Brownies
  | Pizza
  | Chicken
  | GummyBears
  | Bread
  | ChocolateCake
  | Speghetti
  | Sandwich
  | CheeseBurger

whatsMyFavoriteFood :: Food -> String
whatsMyFavoriteFood food = case food of
  Speghetti -> "You got it!"

The problem with the above (and you'll see the error for this as well), is that case statements require every value for your type to be accounted for; you can't ignore some cases unless you're willing to do some ugly things in your program (which we won't go over here). Also, without accommodating the other scenarios, we couldn't return back our "Sorry, better luck next time!" message.

So then the next thing you might think is "Ok, guess I gotta bite the bullet", and do the following:

module Main where

import Prelude

data Food
  = Donuts
  | Brownies
  | Pizza
  | Chicken
  | GummyBears
  | Bread
  | ChocolateCake
  | Speghetti
  | Sandwich
  | CheeseBurger

whatsMyFavoriteFood :: Food -> String
whatsMyFavoriteFood food = case food of
  Speghetti -> "You got it!"
  Donuts -> wrongAnswer
  Brownies -> wrongAnswer
  Chicken -> wrongAnswer
  GummyBears -> wrongAnswer
  Bread -> wrongAnswer
  ChocolateCake -> wrongAnswer
  Speghetti -> wrongAnswer
  Sandwich -> wrongAnswer
  CheeseBurger -> wrongAnswer
  where
  wrongAnswer :: String
  wrongAnswer = "Sorry, better luck next time!"

That's just no fun at all! We don't want to have to go over every single case, that's gonna cause us a big headache. Luckily, there's a better way.

module Main where

import Prelude

data Food
  = Donuts
  | Brownies
  | Pizza
  | Chicken
  | GummyBears
  | Bread
  | ChocolateCake
  | Speghetti
  | Sandwich
  | CheeseBurger

whatsMyFavoriteFood :: Food -> String
whatsMyFavoriteFood food = case food of
  Speghetti -> "You got it!"
  otherAnswers -> "Sorry, better luck next time!"

Ah, beautiful! We just used a "catch-all" to accommodate all the other scenarios in one go! How does that work?