haskell for grownups - harrisonwl.github.io · haskell for grownups bill harrison february 8, 2019....
TRANSCRIPT
Haskell for Grownups
Bill Harrison
February 8, 2019
Table of Contents
IntroductionResources for HaskellHaskell vs. CTypes + Functions = Programs
PragmaticsModules are the basic unit of a Haskell programUsing GHCi
How to Write a Haskell Program
Haskell Basics
I Modern (pure) lazy functional language
I Statically typed, supports type inferenceI Compilers and interpreters:
I http://www.haskell.org/implementations.htmlI GHC CompilerI GHCi interpreter
I A peculiar language feature: indentation matters
I Also: capitalization matters
Some Reference Texts
I Programming in Haskell by Graham Hutton.This is an excellent, step-by-step introduction to Haskell.Graham also has a lot of online resources (slides, videos, etc.)to go along with the book.
I A Gentle Introduction to Haskell by Hudak, Peterson, andFasal.Available at http://www.haskell.org/tutorial/.
I Learn You a Haskell for Good by Miran Lipovaca.Highly amusing and informative; available online.
I Real World Haskell by Bryan O’Sullivan.Also available online (I believe). “Haskell for WorkingProgrammers”.
I Course notes and Slides by me.
I Google.
Question: What does this program do?
n = i;a = 1;while (n > 0) {
a = a * n;n = n - 1;
}
Functions in Mathematics
n! =
{1 if n = 0n ∗ (n − 1)! if n > 0
What does this have to do with that?
n = i;a = 1;while (n > 0) {
a = a * n;n = n - 1;
}
Functions in Mathematics
n! =
{1 if n = 0n ∗ (n − 1)! if n > 0
What does this have to do with that?
n = i;a = 1;while (n > 0) {
a = a * n;n = n - 1;
}
First Haskell Function
n! =
{1 if n = 0n ∗ (n − 1)! if n > 0
It’s relationship to this Haskell function is apparent:
fac :: Int -> Intfac 0 = 1fac n = n * fac (n-1)
First Haskell Function
n! =
{1 if n = 0n ∗ (n − 1)! if n > 0
It’s relationship to this Haskell function is apparent:
fac :: Int -> Intfac 0 = 1fac n = n * fac (n-1)
Hello World in C
#include <stdio.h>int main() {printf("hello world\n");
}
Hello World in Haskell
module HelloWorld wherehelloworld :: IO ()helloworld = print "Hello World"
Factorial Revisited
#include <stdio.h>int fac(int n) {if (n==0){ return 1; }
else{ return (n * fac (n-1)); }
}
int main() {printf("Factorial 5 = %d\n",fac(5));return 0;
}
Hello Factorial
#include <stdio.h>int fac(int n) {printf("hello world"); // newif (n==0){ return 1; }
else{ return (n * fac (n-1)); }
}...
(N.b., the type is the same)
int fac(int n) {...}
Hello Factorial
#include <stdio.h>int fac(int n) {printf("hello world"); // newif (n==0){ return 1; }
else{ return (n * fac (n-1)); }
}...
(N.b., the type is the same)
int fac(int n) {...}
Hello Factorial in Haskell
fac :: Int -> IO Int -- the type changedfac 0 = do print "hello world"
return 1fac n = do print "hello world"
i <- fac (n-1)return (n * i)
Data Types + Functions = Haskell ProgramsHaskell programming is both data type and functional programming!
I Arithmetic interpreterI data type:
data Exp = Const Int | Neg Exp | Add Exp Exp
I function:
interp :: Exp -> Intinterp (Const i) = iinterp (Neg e) = - (interp e)interp (Add e1 e2) = interp e1 + interp e2
I How do Haskell programs use data?I Patterns break data apart to access:
“interp (Neg e) =. . .”I Functions recombine into new data:
“interp e1 + interp e2”
Data Types + Functions = Haskell ProgramsHaskell programming is both data type and functional programming!
I Arithmetic interpreterI data type:
data Exp = Const Int | Neg Exp | Add Exp Exp
I function:
interp :: Exp -> Intinterp (Const i) = iinterp (Neg e) = - (interp e)interp (Add e1 e2) = interp e1 + interp e2
I How do Haskell programs use data?I Patterns break data apart to access:
“interp (Neg e) =. . .”I Functions recombine into new data:
“interp e1 + interp e2”
Type Synonym
Type synonym: new name for an existing type; e.g.,
type String = [Char]
String is a synonym for the type [Char].
Type synonyms can be used to make other types easier to read;e.g., given:
type Pos = (Int,Int)
origin :: Posorigin = (0,0)left :: Pos -> Posleft (x,y) = (x-1,y)
Parametric PolymorphismType synonyms can also have parameters
type Pair a = (a,a)
mult :: Pair Int -> Intmult (m,n) = m*n
copy :: a -> Pair acopy x = (x,x)
Nesting Type SynonymsType declarations can be nested
type Pos = (Int,Int) -- GOOD
type Trans = Pos -> Pos -- GOOD
However, they cannot be recursive:
type Tree = (Int,[Tree]) -- BAD
Data Declarations
A completely new type can be defined by specifying its valuesusing a data declaration.
data Bool = False | True
I Bool is a new type.
I False and True are called constructors for Bool.
I Type and constructor names begin with upper-case letters.
I Data declarations are similar to context free grammars.
Data Declarations
A completely new type can be defined by specifying its valuesusing a data declaration.
data Bool = False | True
I Bool is a new type.
I False and True are called constructors for Bool.
I Type and constructor names begin with upper-case letters.
I Data declarations are similar to context free grammars.
New types can be used in the same way as built-in types
For example, given
data Answer = Yes | No | Unknown
We can define:
answers :: [Answer]answers = [Yes,No,Unknown]
flip :: Answer -> Answerflip Yes = Noflip No = Yesflip Unknown = Unknown
New types can be used in the same way as built-in types
For example, given
data Answer = Yes | No | Unknown
We can define:
answers :: [Answer]answers = [Yes,No,Unknown]
flip :: Answer -> Answerflip Yes = Noflip No = Yesflip Unknown = Unknown
Constructors with ParametersThe constructors in a data declaration can also have parameters.For example, given
data Shape = Circle Float| Rect Float Float
we can define:
square :: Float -> Shapesquare n = Rect n narea :: Shape -> Floatarea (Circle r) = pi * rˆ2area (Rect x y) = x * y
Note:
I Shape has values of the form Circle r where r is a float, andRect x y where x and y are floats.
I Circle and Rect can be viewed as functions that constructvalues of type Shape:
-- Not a definitionCircle :: Float -> ShapeRect :: Float -> Float -> Shape
Not surprisingly, data declarations themselves can also haveparameters. For example, given
data Maybe a = Nothing | Just a
we can define:
safediv :: Int -> Int -> Maybe Intsafediv _ 0 = Nothingsafediv m n = Just (m ‘div‘ n)
safehead :: [a] -> Maybe asafehead [] = Nothingsafehead xs = Just (head xs)
Table of Contents
IntroductionResources for HaskellHaskell vs. CTypes + Functions = Programs
PragmaticsModules are the basic unit of a Haskell programUsing GHCi
How to Write a Haskell Program
General form of a Haskell module
module ModuleName whereimport L1 -- imports
...import Lk
data D1 = · · · -- type decls
...data Dn = · · ·
f1 = · · · -- fun decls
...fm = · · ·
I Order does not matter
I Modules, Types &constructors are alwayscapitalized
I Module ModuleName storedin file, ModuleName.hs
Starting GHCi
bash-3.2> ghciGHCi, version 7.6.3: http://www.haskell.org/ghc/ :? for helpLoading package ghc-prim ... linking ... done.Loading package integer-gmp ... linking ... done.Loading package base ... linking ... done.
*Prelude>
Loading a File into GHCi
*Prelude> :l ExtractList[1 of 1] Compiling ExtractList (ExtractList.hs, interpreted)Ok, modules loaded: ExtractList.
*ExtractList>
I :l is short for :load.
I Could type ghci ExtractList to load it at start up.
Checking Types in GHCi
*ExtractList> :t headhead :: [a] -> a
*ExtractList> :t tailtail :: [a] -> [a]
*ExtractList> :t (:)(:) :: a -> [a] -> [a]
I :t is short for :type.I Can check the type of any function definition
I The above are list functions defined in Prelude
Reloading and Quitting GHCi
*ExtractList> :rOk, modules loaded: ExtractList.
*ExtractList> :qLeaving GHCi.bash-3.2>
I :r is short for :reload.
I :q is short for :quit.
I Reload only recompiles the current module. Use it when youhave only made changes to the current module—it’s faster.
I Emacs Users: can use C-p, C-n, C-e, C-a, C-f at the GHCiprompt to cycle through and edit previous commands.
Table of Contents
IntroductionResources for HaskellHaskell vs. CTypes + Functions = Programs
PragmaticsModules are the basic unit of a Haskell programUsing GHCi
How to Write a Haskell Program
Type-Driven Programming in HaskellTypes first, then programs
I Writing a function with type A→ B, then you have a lot ofinformation to use for fleshing out the function.
I Why? Because the input type A — whatever it happens to be— has a particular form that determines a large part of thefunction itself.
I This is, in fact, the way that you should develop Haskellprograms.
Recursive Types
In Haskell, new types can be declared in terms of themselves. Thatis, types can be recursive.
data Nat = Zero | Succ Nat
Nat is a new type, with constructors
Zero :: NatSucc :: Nat -> Nat
Note:
I A value of type Nat is either Zero, or of the form Succ nwhere n :: Nat. That is, Nat contains the following infinitesequence of values:
ZeroSucc ZeroSucc (Succ Zero)
...
Note:
I We can think of values of type Nat as natural numbers, whereZero represents 0, and Succ represents the successorfunction 1+.
I For example, the value
Succ (Succ (Succ Zero))
represents the natural number
1 + (1 + (1 + 0))
Using recursion, it is easy to define functions that convert betweenvalues of type Nat and Int:
nat2int :: Nat -> Intnat2int Zero = 0nat2int (Succ n) = 1 + nat2int n
int2nat :: Int -> Natint2nat 0 = Zeroint2nat n = Succ (int2nat (n - 1))
Two naturals can be added by converting them to integers,adding, and then converting back:
add :: Nat -> Nat -> Natadd m n = int2nat (nat2int m + nat2int n)
However, using recursion the function add can be definedwithout the need for conversions:
add Zero n = nadd (Succ m) n = Succ (add m n)
The recursive definition for add corresponds to the laws
0 + n = n
and(1 + m) + n = 1 + (m + n)
The edit-compile-test-until-done paradigmI’m guessing that this is familar to you
When I was a student—the process of writing a C program tendedto follow these steps:
1. Create/edit a version of the whole program using a text editor.
2. Compile. If there were compilation errors, develop ahypothesis about what the causes were and start again at 1.
3. Run the program on some tests. Do I get what I expect? Ifso, then declare victory and stop; otherwise, develop ahypothesis about what the causes were and start again at 1.
An Exercise
I Write a function that
1. takes a list of items,2. takes a function that returns either True or False on those
items,3. and returns a list of all the items on which the function is true.
I This is called filter , and it’s a built-in function in Haskell, butlet me show you how I’d write it from scratch.
I I call the function I’m writing “myfilter” to avoid the nameclash with the built-in version.
Step 1. Figure out the type of the thing you’re writing
I Think about the type of filter and write it down as a typespecification in a Haskell module (called Sandboxthroughout).
I With what I’ve said about filter, it takes a list ofitems—i.e., something of type [a].
I It also takes a function that takes an item—an a thing—andreturns true or false—i.e., it returns a Bool. So, this functionwill have type a → Bool.
I ∴ the type should be:
myfilter :: [a] -> (a -> Bool) -> [a]
Step 2: Fill in the type template & Load the module.
I In this case, we have a function with two arguments. Thesecond argument of type a->Bool does not have amatchable form like the first argument.
I This leaves us with:myfilter :: [a] -> (a -> Bool) -> [a]myfilter [] f = undefinedmyfilter (x:xs) = undefined
I Reloading module reveals an error in the template as typed:> ghci Sandbox.hs[1 of 1] Compiling Sandbox ( Sandbox.hs, interpreted )Sandbox.hs:6:1:
Equations for ‘myfilter’ have different numbers of argumentsSandbox.hs:6:1-27Sandbox.hs:7:1-27
Step 2: Fill in the type template & Load the module.
I In this case, we have a function with two arguments. Thesecond argument of type a->Bool does not have amatchable form like the first argument.
I This leaves us with:myfilter :: [a] -> (a -> Bool) -> [a]myfilter [] f = undefinedmyfilter (x:xs) = undefined
I Reloading module reveals an error in the template as typed:> ghci Sandbox.hs[1 of 1] Compiling Sandbox ( Sandbox.hs, interpreted )Sandbox.hs:6:1:
Equations for ‘myfilter’ have different numbers of argumentsSandbox.hs:6:1-27Sandbox.hs:7:1-27
Step 2 (continued)Debugging via Type-checking!
I Fix the error NOW! Forgot 2nd f param. in 2nd clause.I Type error just committed can be fixed now.I Alternative: wait until I have “first draft” of the whole
program and check it.I Incremental approach: can check each new part of the
program as I create it so that I’m not surprised by errors.I Identifying the source of error easier—i.e., it is the line you
just typed.
I Fixing the error yields:
myfilter :: [a] -> (a -> Bool) -> [a]myfilter [] f = undefinedmyfilter (x:xs) f = undefined
Step 3: Fill in the clauses one-by-one reloading as you go.
The [] case is obvious because there is nothing to filter out:
myfilter :: [a] -> (a -> Bool) -> [a]myfilter [] f = []myfilter (x:xs) f = undefined
No problems with this last bit:
> ghci Sandbox.hs[1 of 1] Compiling SandboxOk, modules loaded: Sandbox.
*Sandbox>
Step 3 (continued).
I The second clause should only include x if f x is True; oneway to write that is with an if−then−else:
myfilter :: [a] -> (a -> Bool) -> [a]myfilter [] f = []myfilter (x:xs) f = if f x
then x : myfilter f xselse myfilter f xs
I Loading this into GHC reveals a problem:> ghci Sandbox.hs[1 of 1] Compiling Sandbox ( Sandbox.hs, interpreted )Sandbox.hs:8:46:
Couldn’t match expected type ‘[a]’ with actual type ‘a -> Bool’In the first argument of ‘myfilter’, namely ‘f’In the second argument of ‘(:)’, namely ‘myfilter f xs’In the expression: x : myfilter f xs
Failed, modules loaded: none.Prelude>
Step 3 (continued).
I This error occurs on line 8 of the module, which is the line“then x : myfilter f xs”. GHCi is telling us that itexpects that f would have type [a] but that it can see that fhas type a → Bool. After a moment’s pause, we can seethat the order of the arguments is incorrect in both recursivecalls. The corrected version works:
myfilter :: [a] -> (a -> Bool) -> [a]myfilter [] f = []myfilter (x:xs) f = if f x
then x : myfilter xs felse myfilter xs f