Haskell for Grownups - harrisonwl.github.io · Haskell for Grownups Bill Harrison February 8, 2019....

Post on 20-May-2020

31 views 0 download

Transcript of Haskell for Grownups - harrisonwl.github.io · Haskell for Grownups Bill Harrison February 8, 2019....

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