OK, this page is indeed beginner friendly because I'm also pretty new to Haskell.
But I guess by beginner I mean beginner to functional programming, NOT beginner to programming as a whole.
Environment Related
Steps to step to set up the env on a Mac with Intel chip: follow this guide
Several ways to compile and run:
the CML way: runghc -Wall hello.hs -fforce-recomp; run the code, with warning
the interactive way: ghci; load a file using load hello.hs
stack: I personally prefer this one; edit the code with highlighting in vscode then compile and run with
stack. Some commands:
stack new proj-name template-name: cannot have underscore in the project name;
new-template by default for template if you don't specify one
stack build: compiles and produces an executable proj-name-exe; this
executable is hidden in the document tree
stack exec proj-name-exe: run the executable
stack test: run the test suite
stack clean: clean compiler output (in .stack-work/dist)
stack purge: revert the project to a clean state; a throrough cleaning...
stack init: creating the stack.yaml file
document tree of stack
app/Main.hs: entrypoint
src/Lib.hs: libs
test/Spec.hs: test code
Setup.hs: configuration for cabal
helloworld.cabal: automatically updated for cabal
package.yaml: generates a cabal file, which you should NOT modify; specify dependencies
here
stack.yaml: configuration file for stack; resolver: compiler settings,
packages: local package path
README.md, .gitignore, ChangeLog.md and LICENSE
Principles
Principles of functional programming
function is first class: functions themselves are values
Haskell programs is centered around evaluating expressions rather than executing instructions
indentation matters: if you indent more it'll just be added to the previous line; if you indent
less the block is terminated
Principles of Haskell
everything is immutable; think of variables not as boxes
but as unique names for values
e.g. x=7 means "define x to be 7"
e.g. x=7 y=x+5 when you change the value of x, the value of y stays the same
no side effect so parallelism is possible
lazy: things are not evaluated until their results are
needed
statically typed: types must match for haskell
expressions; there is NO implicit type conversion in haskell "somestr"==5 yields an error
names in haskell can't start with uppercase letters; _ alone is a placeholder
Data types: Int, Integer, Bool, Double, Char, String: String is equivalent to
[Char]
Good practices:
top-level type declaration for every function: e.g. add :: Int -> Int -> Int
Haskell vs imperative languages
Declare things in term of WHAT it is instead of HOW to get it.
Basis
Lists
lists in haskell are singular linked lists. Only elements of the same data type are allowed to be in the same
list.
But this is allowed because it's a list of lists: [[1,2],[8,11,5],[4,5]]
create a list: note that when you create a list you always do a deep copy
mylist = [1,2,3]
using the cons operator: takes one element and a list
e.g. 1:2:3:[] yields [1,2,3]
using the ++ operator: takes two lists; think concat two linked list, the first
list is traversed
e.g. ["a","b","c"] ++ "de"
using list ranges ..: only works on integers and chars
e.g. [1..20]
e.g. [1..] this is an infinite list, don't print out the whole thing, use
take
create a cyclic list: cycle, repeat, replicate
e.g. cycle [1,2,3]
e.g. repeat 5 : you get an infinite list of 5s
e.g. replicate 15 6: you get fifteen 6s
operations on a list:
head, tail, init, last, length, reverse, take, drop, maximum, minimum, sum, product, elem
some of these operations are expensive don't use them
list comprehensions: these are pretty much like sets definition in math; can have multiple guards
and
generators
e.g. [x*2 | x <- [1..10]] this is equivalent to the set $\{x*2 \mid x \in \mathbb{N}
\text{ and } x \in [1,10]\}$
e.g. [ x | x <- [1..100], x `mod` 7 == 0, x `mod` 5 == 0 ] multiple guards
e.g. [ x + y | x <- [100,200..400], y <- [0..3] ] multiple generators, applies
right to
left note that if you use let bindings in list comprehension, whatever defined in
let
is only visible to things on the left hand side of it
Tuples
Tuples are not homogenous, meaning they can contain multiple data types.
create a tuple
mytuple = (1, "hello")
zip two lists: the shorter list dominates
e.g. zip "Stephen" [1..] yields
[('S',1),('t',2),('e',3),('p',4),('h',5),('e',6),('n',7)]
Types
Haskell is statically typed, meaning that the type of every expression is known at compile time.
Haskell uses type inference, so you don't have to explicitly specify the type of every expression.
For functions, the return value is the last type in its type declaration.
Type annotation: read "5" :: Int
Specify the type of an expression explicitly by adding :: after it.
Type variables: head :: [a] -> a
The a here is a type variable, meaning it can be of any type. Similarly there can be b
etc.
Within the same type signature, if you use the same type variable, it means that the two types are the same.
Functions that have type variables are called polymorphic functions.
Type classes:
Type classes are like interfaces in Java. (==) :: (Eq a) => a -> a -> Bool
Everything before => is called a class constraint.
You can regard it as some constraint imposed on the type variable a.
In our case, the type constraint specifies that a must be a type that belongs to the
Eq type class,
which is essentially an interface to test for equality.
Everything after => can be intepreted the same way as you did for function.
Some basic type classes:
Eq, Ord: type classes that allow you to check if two values are equal or if one is greater
than the other Note that Num isn't a subclass of Ord
Enum, Bounded: check if members in this collection can be sorted or if members in this
collection are bounded
Show, Read: similar to toString() and its opposite, e.g.
parseInt() in Java.
Numeric value related:
Num: all numeric values that allows you to perform arithmetics.
Integral: two types Int and Integer
Floating: two types Float and Double
splitAt: split at index
sort
group: group the same element into sublists
User-defined Types data Bool = False | True
Modules
Haskell modules;
Data.List
concat: flattens out a list
transpose: transpose a 2D matrix
any: if any element of the list satisfies the predicate
Data.Char
check whether a character is a letter/number...
change a character to upper/to lower...
Data.Map
key-value pairs
Data.Set
set
User-defined modules:
-- in Geometry.hs
module Geometry
( sphereVolume
, sphereArea
, squareArea
, squareVolume) where
-- define the functions here
-- in Sphere.hs
module Geometry.Sphere
( volume
, area) where
-- define the functions here
Functions
Function data type: with multiple arguments, functions are actually function that returns functions that returns
functions...each of these functions is a single argument function
Call a function
function calls have higher precedence than all other operations, it'll use the following values as the
argument once it receives it.
e.g. succ 9 + max 5 4 + 1 yields 16
backticks enables you to use function as an infix operator
e.g. 15 `max` 16 is the same as max 15 16
parenthesis enables you to use a binary operator as a function
e.g. (+) 15 16 is the same as 15 + 16
function application w/ $: to get rid of parenthesis because it has the lowest precedence.
e.g. sqrt (3 + 4 + 9) is the same as sqrt $ 3 + 4 + 9
function composition w/ .: pretty much like function composition in math
e.g. map (\x -> negate (abs x)) [5,-3,-6,7] is the same as map (negate .
abs) [5,-3,-6,7]
Define a function
not that there is no return statement in haskell; every function in haskell must return something
simple function
e.g. area a b = a * b
let...in function:
e.g.
cylinder r h = let sideArea = 2 * pi * r * h topArea = pi * r^2 in sideArea + 2 * topArea
where function:
e.g.
cylinder r h = sideArea + 2 * topArea where sideArea = 2 * pi * r * h topArea = pi * r^2 diff between let bindings and where bindings: let bindings are expressions
themselves. where bindings are syntactic constructs.
This means that let bindings can be used anywhere (they return a value), but where bindings can
only be used in the function they are defined in.
pattern matching: note that patterns are evaluated in order, so put specific first digitToStr :: Integral a => a -> String digitToStr 1 = "One" digitToStr _ = "Other numbers"
equivalently, you can do case matching digitToStr :: Integral a => a -> String digitToStr x = case x of 1 -> "One" _ -> "Other numbers"
factorial :: (Eq a, Num a) => a -> a
factorial 0 = 1
factorial n = n * factorial (n - 1)
Higher order functions
Higher order function refers to the fact that functions can be used as parameters and return values.
Example that function can be used as a return value:
max :: (Ord a) => a -> a -> a
-- essentially the same as
max :: (Ord a) => a -> (a -> a)
Two ways to interpret this:
1. max takes two parameters and returns a value of the same type
2. max takes one parameter and returns a function that takes one parameter and returns a value of
the same type -> function application happens to the closest argument
Example that function can be used as a parameter:
applyTwice :: (a -> a) -> a -> a
applyTwice f x = f (f x)
Notice the type declaration: (a -> a) is the type of a function. The parenthesis cannot be omitted
because -> is right associative.
Some standard prelude functions commonly used
filter :: (a -> Bool) -> [a] -> [a]:
takes a boolean function (called a predicate) and a list and returns a
list that is filtered based on the predicate.
takeWhile :: (a -> Bool) -> [a] -> [a]:
takes a predicate and a list and returns a list that is taken from the original list until the first
element that doesn't satisfy the predicate.
compare :: Ord a => a -> a -> Ordering:
takes two parameters that can be ordered(belong to the type class Ord) and return an ordering.
zip :: [a] -> [b] -> [(a, b)]:
takes two lists and combine the two lists into a new list made of tuples of elements from the two
lists.
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]:
takes three parameters: (1) a function
that takes two parameters a and b, (2) a list of a, and (3) a list of b. returns a list of c where each
c is the result acquired from applying the function on a and b.
map :: (a -> b) -> [a] -> [b]:
takes a function and a list and returns a list where each element is the result of applying the
function on the corresponding element in the original list.
fold is a common pattern in haskell.
It takes a binary function, a starting value, and a list to fold up. A binary function takes two
parameters. The binary function is applied to the starting value to acquire a new starting value, and
this process repeats until the list is exhausted.
foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b:
folds from the let. takes a function that takes two parameters and returns a function that
takes a list and returns a value.
scanl :: (b -> a -> b) -> b -> [a] -> [b]:
similar to foldl but returns a list of all the intermediate values.