Intro to Haskell
(Note: no strange imports this time. Plain old ghci
should load this file just fine.)
1 Playing With Some Building Blocks
As with most languages, Haskell lets us give names to values:
= 42
x = 'A'
y = False oppositeOfTrue
Let’s play with these in ghci
:
y:type y
Check the other variables’ types and values as well.
(Exercise!)
Let’s explore one more type:
= "<insert fun phrase here>"
s = s ++ s doubleS
Strings in Haskell are lists of characters (written [Char]
).1
1.1 Definition or Assignment?
Remember that a Haskell program is a big expression, not a series of instructions (statements) to execute.
In Java x = 5
is assignment, meaning “put the new value 5
into x
”.
In Haskell, x = 5
is definition: define x
to be 5
.
So, what if we give a new value to x
?
Try uncommenting this (remove the {-
and -}
) and loading (or reloading: :reload
) the file:2
{- x = 525600 -}
Why did this happen? (Exercise!)
1.2 Values or Expressions
(We will probably skip this part in lecture. If so, and if you have trouble finding expressions that cause errors, consider trying the built-in functions div
or head
.)
Now, in Haskell, do we give names to expressions or to values? Let’s try these:
= 10 + 2
n = 25 `div` 3 -- equivalent to div 25 3
m = 25 / 3 o
Does it matter? What’s the difference?
Take a few minutes to define some expressions, show their values, and show their types. Try specifically to define an expression that causes no error but then when we evaluate we get an error.
(Exercise!)
1.3 Type Inference
Haskell figures out the types of your expressions. (It performs type inference.) We’ll explore the details of that later, with connections to “unification”.
For now, think of it as detective work: Investigate each element of an expression for its type “constraints”: what do you know for sure about its type? Puzzle together those constraints until you know the overall type.
(There’s a challenging, very quiz-like exercise on this, which is for out-of-class. Try it then, and ask questions on Piazza!)
2 Building Up Functions
We’ve used a handful of built-in arithmetic functions, and you’ll see more in the reading.
Let’s define a few of our own simple functions.
2.1 Define a function that adds one to an Int
add1 :: Int -> Int
= undefined -- Fill me in! add1 n
There’s actually a built-in function that does the same. We could also just put its value into our function:
add1' :: Int -> Int
= succ add1'
2.2 Finding the n
th Odd Number
Now, define a function that produces the n
th odd number:
-- >>> nthOdd 1
-- 1
--
-- >>> nthOdd 2
-- 3
nthOdd :: Int -> Int
= undefined nthOdd n
(Exercise!)
(Hint: if you double a number n
, it gives you the n
th even number.)
2.3 Define an exactly-one-true function
Define a function that determines if exactly one of three Boolean values is true. You’ll want to use the &&
(and), ||
(or), and not
functions.
oneTrue :: Bool -> Bool -> Bool -> Bool
= undefined oneTrue b1 b2 b3
2.4 Building Farther Using Cases
One of the central mechanisms Haskell uses to make decisions and break down data is pattern-matching by cases.3
In fact, built-in functions like head
and tail
that break up data structures are implemented in terms of pattern-matching:
myHead :: [a] -> a
:_) = x myHead (x
(_:_)
matches a non-empty list. (x:_)
does the same, but defines x
’s value to be the head of the list.
(Guards are handy also. Learn about those from the readings!)
We can use any type of data in our cases, like bools:
True = False
myNot False = True myNot
Now, redefine oneTrue
(as oneTrue'
) except by cases instead:
oneTrue' :: Bool -> Bool -> Bool -> Bool
= undefined oneTrue' _ _ _
2.5 Exercise
Now, we’ll try an exercise where we interpret lists of Bool
s as if they were single Bool
values. Go try it out as an exercise!
It may help to know that you can use patterns like:
[]
: the empty list(x:xs)
: a non-empty list with the headx
and tailxs
,[True, False]
: a length-two list withTrue
as its first value, andFalse
as its second.
3 Lazy Evaluation, Referential Transparency, and Control Structures
We’ve mentioned before that Haskell uses “lazy evaluation”, meaning loosely that it avoids evaluating expressions until forced to.
Let’s use that to define our own if
expression:
myIf :: Bool -> a -> a -> a
= undefined myIf condition thenArg elseArg
Would this work in Java?
public static int myIf(bool condition, int thenArg, int elseArg) {
if (condition)
return thenArg;
else
return elseArg;
}
myIf(a != 0, b / a, 0);
Let’s try it in Haskell!
= 0
a = 3
b = myIf (a /= 0) (b / a) 0 result
Now type result
in ghci
.
3.1 Referential Transparency and No Side Effects
But wait. If we don’t even know when an expression will be evaluated… if expressions can be evaluated “out of order” with the way we expect them to go… then what happens with code like x++
?4
x++
could change x
’s value at some unpredictable time in a Haskell program. Or it could never change x
’s value, if it happened never to get evaluated. How can we possibly increment a variable’s value given all that?
Haskell’s answer: We can’t. Haskell disallows side-effects: effects your code has besides computing a value, like changing the value of a variable.5
That means Haskell also offers something called referential transparency: once you know an expression’s value6, you know that the value and expression mean the same thing. So:
- you can freely substitute the value for the expression
- you can evaluate the same expression again, and you will get the same result
- you can substitute in a different expression if it also has the same value
That’s tremendously handy for reasoning about your programs (like when you’re testing, for example!).
4 Recursive Functions
We’ll use recursion frequently in defining our functions (at least at first!).
Fortunately, most recursive functions we create do just what Haskell is good at: break down processing of data into cases based on the structure of the data, and then define the result of each case based on the data from those structures.
So, let’s write a couple of our own recursive functions. First, we’ll double each element in a list. Let’s break it into cases, and then figure out the cases:
-- If we doubleAll an empty list, that's still just an empty list.
-- If we doubleAll on a list with x at the head and xs at the tail,
-- we should get 2*x as the head and
-- the result of doubleAll on xs as the tail.
doubleAll :: [Int] -> [Int]
= undefined doubleAll
Now, let’s try to intersperse a new letter between each pair of letters in a string. For example, intersperse c [letter1, letter2, letter3]
is [letter1, c, letter2, c, letter3]
.
intersperse :: Char -> [Char] -> [Char]
= undefined intersperse c s
Over in the exercises! we have a mystery recursive function for you to evaluate and a recursive function for you to define.
There’s actually a more robust text type available and GHC support for “polymorphic” strings, in much the way that the number
5
came out with the typeNum p => p
, meaning “some typep
, wherep
is an instance of theNum
type class, i.e., is numeric”.↩︎You’ll get a different result if you run
x = 525600
at the REPL prompt inghci
, which exposes some interesting techniques used to make the REPL work!↩︎In fact,
ghc
compiles Haskell to a Haskell-lite intermediate language called Core, which lacksif
expressions and even cases in function definitions and boils them all down to an explicitcase
expression construct that uses pattern-matching by cases.↩︎“We don’t even know when an expression will be evaluated. Expressions might be evaluated out of order.” Does that sound a bit like the hazards of parallelism and concurrency to you? I wonder if strictly-functional programming is hugely valuable in modern programming because of the growing importance of parallelism and concurrency.↩︎
In fact, with a very small number of “functions” you are strongly encouraged never to use, it is possible to cause side effects. Doing so is a mess in a Haskell program for the reasons we talked about! There are also some side-effects you can’t disallow in the real world. For example, how long a piece of code takes to run on your computer. On the other hand, Haskell has brilliant solutions for side effects like reading input and displaying output that do not violate the no-side-effects rule within your program!↩︎
Within a particular context, that is. For example
x + 1
in your program may be very different from in mine, andx + 1
in one call to a function withx
as a parameter may be very different from in a different call to that function.↩︎