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
johnsMoneyand says "johnsMoneywas set to 150", - then it sees
bensMoneyand says "bensMoneywas set to 300" - and finally, it substitutes those values in the place of the variables, turning
total = johnsMoney + bensMoneyinto
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:
- A function called
addTenwhich adds10to its parameter - A function called
sub15which subtracts15from its parameter - A function called
squarewhich multiplies the parameter with itself - A function called
doNothingwhich 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:
- Add another variable called
var4and useaddFiveto set it to6. - Add another variable called
var5and useaddFiveto set it to100.
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
letorwherebindings 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
valueis 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?