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). In general, the code editor should always start with the following lines

module Main where
import Prelude
  • Code blocks starting with the above lines should usually be safe to type/paste into the code editor without any worry of errors; just make sure to clear out the code editor before pasting new code in.
  • Code blocks without the above lines may or may not work when pasted into the code editor. Not all code examples are intended to be typed in the code editor, but I will do my best to ensure the intent on how to use the code samples throughout this book are clear.
  • Despite the previous bullet point, I encourage you to experiment where ever your curiosity is piqued. But try not to get too hung up on any particular section, you may miss the forest for the trees.

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.

Conventions

I want to keep the main conventions on how to follow along with this book clear and easily accessible. In order to achieve this goal, I will include the following section of tips at the beginning of each chapter:

  • 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

Chapter 1


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

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.

Before we start, clear out your code editor and paste the following code in

module Main where
import Prelude

Now, 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: Variable names must start with a lowercase letter (or an _) and spaces arent allowed.

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

Clear out the code editor and add the following lines:

module Main where
import Prelude

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. Once we've stored some variables in our program, we can get those values back by using those variable names later in the code. Whenever the computer sees one of those variables, it will substitute the variable with it's value. So in the line total = johnsMoney + bensMoney

  • First, the computer sees johnsMoney and says "johnsMoney was set to 150",
  • then it sees bensMoney and says "bensMoney was set to 300"
  • and finally, it substitutes those values in the place of the variables, turning
    total = johnsMoney + bensMoney into
    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.

module Main where
import Prelude

johnsMoney = 200
bensMoney = johnsMoney * 2

Ah, now no matter what value we give johnsMoney, bensMoney will always be double. If we were to decide to change johnsMoney to 400, bensMoney will still be double that. If we had done something like:

johnsMoney = 200
bensMoney = 400

or

johnsMoney = 200
bensMoney = 200 * 2

We'd have to change 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

Pitfall - Using text without quotes

Before we finish up our chapter, I wanted to call your attention to an issue you may run into. When you create a variable and you want it to hold some text, make sure that you put quotes around that text. If not, you're likely to encounter the errors listed below

1. Unknown value

Try out the following:

module Main where
import Prelude

name = james bond

Using the code above, you should see an error on the right side reading:

Unknown value james

What's happening here is that the compiler thinks you're trying to use a variable called james. It looks around your code for the variable declaration and when it doesn't find it, comes to the conclusion that you tried to use a variable that doesn't exist! Change the above to "james bond" and everything will work as intended.

2. Unknown Data Constructor

Here's another:

module Main where
import Prelude

name = John Smith

This code will give you the following error:

Unknown data constructor John

I won't venture to explain exactly what this means yet, but again, the program is looking for a declaration of something called John and not finding it. Changing the definition to "John Smith" will fix this error.


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!

Chapter 2


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

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.

module Main where
import Prelude

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

module Main where
import Prelude

{-
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 9 months ago, or reading code someone else wrote.

Chapter 3


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

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 + name

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

You can read the :: symbol to mean "has the type" or "is the type". So the above type declaration would be read as "coolNum has the type Int".

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


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 4


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

Functions

Now that we have a basic understanding of creating and using variables, we can move on to the next primary tool in our toolbox: Functions. Functions are the cornerstone of programming, so we'll dedicate the next couple of sections to the understanding of this concept. Everything else we learn in the latter chapters of this book builds on top of this, so make sure to take your time here.

If you haven't programmed before, the extent of your experience with functions may be limited to that of high school algebra. First, you were taught the equation of a line, y = mx + b; then, without warning, the math teachers decided to get cute with us and rewrite it as f(x) = mx + b.

Functions in purescript are akin to those we learned in math, but I think you'll find that they are much more helpful than the functions you were taught in algebra.

The repetition problem

In order to really get a grasp of the utility functions provide, let's start off with a motivating example. Let's say your friend comes up to you one day and says

I've figured out how to determine if someone will have good luck or not! Here's how it works: First, have them pick a random number between 1 and 1000, then add 17 to their number, multiply by 13, subtract 9 and finally divide by 2! If their number is odd, then they will have good luck that day!

Obviously your friend is off his rocker, but suppose we wanted to create some people in a program and see if they have bad luck or not, how would we do that? Let's create 4 imaginary people and give them some lucky numbers!

Put the following example in your code editor:

module Main where
import Prelude

-- Darby picks 350 as his number
darby :: Int
darby = (((350 + 17) * 13) - 9) / 2

-- Pablo picks lucky number 7!
pablo :: Int
pablo = (((7 + 17) * 13) - 9) / 2

-- Jason picks 449
jason :: Int
jason = (((449 + 17) * 13) - 9) / 2

-- Dan picks 123
dan :: Int
dan = (((123 + 17) * 13) - 9) / 2

So you go and show your friend that you've tried out his formula in a purescript program, but regretfully, it turns out he got the formula wrong!

Sorry, instead of adding 17 to their number, we're actually supposed to add 13; and instead of dividing by 2 at the end, we're really supposed to divide by 4!

Great! Now we have to go and fix the formula in our program! Go ahead and adjust your formula in the code editor; when you're finished, you can check against the answer here to see that you're changes match.

Fixed lucky number formula
module Main where
import Prelude

-- Darby picks 350 as his number
darby :: Int
darby = (((350 + 13) * 13) - 9) / 4

-- Pablo picks lucky number 7!
pablo :: Int
pablo = (((7 + 13) * 13) - 9) / 4

-- Jason picks 449
jason :: Int
jason = (((449 + 13) * 13) - 9) / 4

-- Dan picks 123
dan :: Int
dan = (((123 + 13) * 13) - 9) / 4

It's quite annoying to have to run through every line fixing up the formula. Each line is also exactly the same besides the random number picked by each person. Imagine if we had picked numbers for 10 or 20 different people, the possibility that we might forget to fix some of the lines or make a typo becomes increasingly likely! And here's where functions come to the rescue...

Functions in action

When we talked about variables, we talked about instructing the computer to memorize data for us. Functions are similar, except instead of memorizing values, they memorize calculations. We can pack entire calculations into a variable-like name, and then invoke it on command by simply using the name again later on in the code.
We can write a function to encode the lucky number formula our eccentric friend described to us up above, it goes like this:

getLuckyNumber num =
  (((num + 13) * 13) - 9) / 4

We won't dive into the specifics of creating functions just yet, but let's compare this with the calculation of darby's lucky number:

-- Darby's number - A variable declaration
darby =
  (((350 + 13) * 13) - 9) / 4

-- Lucky number formula - A function declaration
getLuckyNumber num =
  (((num + 13) * 13) - 9) / 4

Notice the symmetry between the these two declarations: They're nearly identical apart from substituting 350 with num. But what is num? num is what we call a function parameter, it's basically a variable that only exists within the function. We can set num to different values to change the functions calculation. We'll demonstrate how to do this below.

Paste the following function declaration at the bottom of your code editor

getLuckyNumber num =
  (((num + 13) * 13) - 9) / 4

Next, for each person's variable definition, replace the formula with getLuckyNumber followed by the random number they picked; we can try the first one together.

Go to Darby's variable definition (on line 6) and change it from this:

-- Darby picks 350 as his number
darby :: Int
darby = (((350 + 13) * 13) - 9) / 4 -- Change this line

to this:

-- Darby picks 350 as his number
darby :: Int
darby = getLuckyNumber 350  -- to this

What we've shown here is how num gets assigned its value. When we invoke getLuckyNumber, we add a number in front of the function (in the place where num was in the declaration), and that number gets assigned to num.

Once you've replaced every formula with getLuckyNumber, check that your code editor matches up with the section below. Resist the urge to look at the answer until you've given this your best try.

Code check

After following the above instructions, you're code editor should look like the following:

module Main where
import Prelude

-- Darby picks 350 as his number
darby :: Int
darby = getLuckyNumber 350

-- Pablo picks lucky number 7!
pablo :: Int
pablo = getLuckyNumber 7

-- Jason picks 449
jason :: Int
jason = getLuckyNumber 449

-- Dan picks 123
dan :: Int
dan = getLuckyNumber 123

getLuckyNumber num =
  (((num + 13) * 13) - 9) / 4

Notice what's happening here: Even though the lucky number formula is only written in one place, we're still able to use that formula to create each variable. Now suppose our friend comes back to us and says:

Oh jeez, sorry but it turns out the first formula I told you was actually the correct one!

Oh great! Now we have to go through and fix the formula again! But this time, things are different. We don't have to change that formula on 4 different lines anymore, we only have to fix it in the function itself. After making the fix, you should end up with this:

module Main where
import Prelude

-- Darby picks 350 as his number
darby :: Int
darby = getLuckyNumber 350

-- Pablo picks lucky number 7!
pablo :: Int
pablo = getLuckyNumber 7

-- Jason picks 449
jason :: Int
jason = getLuckyNumber 449

-- Dan picks 123
dan :: Int
dan = getLuckyNumber 123

getLuckyNumber num =
  (((num + 17) * 13) - 9) / 2

Conclusion

That concludes the first section on functions. Hopefully this first taste has given you a good sense of how useful functions can be. We'll skip the summary and questions for this section since this part was quite interactive anyways. In the next section, we'll go over how to create functions in greater detail.

Chapter 4.1


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

Function Declarations

Now that we've had the chance to play around with functions a bit, we'll take a closer look at how to create them.

When we learned how to create variables, I mentioned that the syntax to do this was:

name = value

The syntax for creating functions closely matches this, taking the form

name parameters = definition

That is, first we put the name of the function, then the names of its parameters, the = sign, and finally the actual calculation the function will perform.

For example:

module Main where
import Prelude

addFive num = num + 5

-- addFive is the function name
-- num (on the left side of the "=") is the parameter
-- num + 5 is the definition

We can name the parameters whatever we want; they use the same naming rules as variables (because function parameters are variables).

Next, lets try writing a function that multiplies its parameter by two. Paste the following into your code editor

module Main where
import Prelude

multByTwo theNumber = theNumber * 2

-- multByTwo is the function name
-- theNumber (on the left side of the "=") is the parameter
-- theNumber * 2 is the definition

Why not try a few on your own? Try writing the following functions:

  1. A function called addTen which adds 10 to its parameter
  2. A function called sub15 which subtracts 15 from its parameter
  3. A function called square which multiplies the parameter with itself
  4. A function called doNothing which simply returns the parameter without doing anything to it
Answers
module Main where
import Prelude

addTen num = num + 10

sub15 param = param - 15

square x = x * x

doNothing num = num

-- For `doNothing`, you also could have done the following:
-- doNothing num = num * 1
-- doNothing num = num + 0

So, how'd you do? Sorry if #4 seemed tricky, but kudos to you if you got it right! Also, don't worry if your parameter names don't match mine; remember, you can name them whatever you want, as long as they make sense.

Invoking Functions

Creating functions is great, but we won't be able to enjoy them unless we actually use them. The syntax for using a function looks like this:

name parameters

The syntax to use them is nearly the same as creating them, we just omit the = and the definition. Lets compare the create vs usage syntax:

-- Function creation
addFive num = num + 5

-- Function Usage
addFive 10

Why did we use num during the creation but 10 during the usage?

As mentioned previously, num is a function parameter, which is essentially a placeholder. The function wants to add 5 to something, but it lets whoever uses the function pick what that number should be. The function basically says "If you want to use me, you need to tell me what num is first." So then, when we use the function, we tell it to use 10 for num.

However, we can't just simply throw addFive around willy-nilly; functions return values, and values have to be stored somewhere. Therefore, we'll need to use a variable to hold the result of using the addFive function:

module Main where
import Prelude

-- Function Declaration
addFive num = num + 5

-- Using our function
myVar :: Int
myVar = addFive 10

-- myVar = 15

We can use addFive more than once, and we can also use different values for the num parameter without any issues:

module Main where
import Prelude

-- Function Declaration
addFive num = num + 5

-- Using our function
myVar :: Int
myVar = addFive 10
-- myVar = 15

var2 :: Int
var2 = addFive 20
-- var2 = 25

var3 :: Int
var3 = addFive 0
-- var3 = 5

Lets try out a few more exercises.. In the above example:

  1. Add another variable called var4 and use addFive to set it to 6.
  2. Add another variable called var5 and use addFive to set it to 100.
Answer
module Main where
import Prelude

module Main where
import Prelude

-- Function Declaration
addFive num = num + 5

-- Using our function
myVar :: Int
myVar = addFive 10
-- myVar = 15

var2 :: Int
var2 = addFive 20
-- var2 = 25

var3 :: Int
var3 = addFive 0
-- var3 = 5

var4 :: Int
var4 = addFive 1

var5 :: Int
var5 = addFive 95

If you got those right and everything is making sense, then nice work! If you're still feeling shaky on what's happening, you might want to go back through this chapter and the previous chapter from again. A second run through can do wonders to help solidify knowledge.

Summary

Functions are created using the syntax

name parameters = definition

Here's an example of a function that multiplies its parameter by 3

tripleNum num = num * 3

To use a function, we use the syntax:

name parameters

For example:

tripleNum 20

Values have to be stored in a variable, so because functions return values, we need to create a variable to store the result of our function. With that said, here's a full example of creating and using a function.

module Main where
import Prelude

-- Function Declaration
tripleNum num = num * 3

-- Using the function
myVar = tripleNum 20

Self Practice

Question 1.
Create a function that multiplies its parameter by 4, then create 2 variables that use the function

Question 2.
Using the code box below, create one variable using each of the functions

module Main where
import Prelude

addSeven num = num + 7

sub5 param = param - 5

square x = x * x

Question 3.
Create a function that cubes its parameter, then create 2 variables that use the function

Answers

Question 1.

module Main where
import Prelude

quadruple num = num * 4

money = quadruple 10

numberOfOnionRings = quadruple 100 -- Lets go!

Question 2.

module Main where
import Prelude

addSeven num = num + 7

sub5 param = param - 5

square x = x * x

-- Variables
num1 = addSeven 7

answer2 = sub5 30

anotherVar = square 10

Question 3.

module Main where
import Prelude

cubeTheNum y = y * y

-- Variables
answer1 = cubeTheNum 3

answer2 = cubeTheNum 10

Chapter 5


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

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


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

Chapter 7


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

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


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

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


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

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?