Haskell notes
Haskell notes
:{ print (take 20 [0..23]) :}
def greeting(): return "hello" print(1 + 2)
The above was just a test for seeing if org-babel works.
:{ putStrLn "Hello, everybody!" :}
The :{:}
are necessary because that's how ghci knows that they are to be executed together.
Haskell
- First class functions.
- No side effects.
- Evaluate exprs rather than execute instructions.
Haskell also happens to be lazily evaluated. This means that things are only evaluated when their results are needed. This makes things like infinite data structures possible.
It's also statically typed.
-- comment lmao fun () = do { let y = 3 in print (y + 3) }; -- finally!! fun()
Types
y = 1+1 print(y)
Haskell can infer types on its own.
This has no bounds however
let n :: Integer = 2^(2^(2^(2^(2)))) print(n)
A little caveat to take care of is the fact that not doing 3 * (-3)
will probably make the
compiler scream at you.
import Text.Printf let d :: Double = 4.538799999999999 let b :: Bool = True let c :: Char = 'ダ' -- unicode print(d, b, c)
Also has the regular business with booleans. Inequality checked with /=
Lists in haskell
Very powerful and very helpful.
let a = [4,8,15] print(a)
Concatenation is done with ++
print([1,2,3,4] ++ [5,6,7,8])
Strings are also lists of characters
print (['j', 'e', 'l', 'l', 'o']) print ("jello")
- Nil and Cons
[]
is called nil and denotes an empty list:
is called cons that is a constructor for a value and another list of the same type They are enums. One thing to note here is that the constructor cons is also a binary operator so it can be applied partially like other binary operators. This is true for any data operator that starts with:
.print ([]) print (1 : []) print (1: 2 : 3 : []) -- eval'd from right to left
- Concat (++)
print ([1] ++ [2, 3]) print (1 : [2, 3]) -- better time complexity wise
The concat or
++
operator is linear in the size of the list on the left side. - Indexing
You can get the n'th element of the list by using the
!!
operator on the list. However, check for bounds else there's a runtime error.print("Its a me mario" !! 4) -- print([1] !! 3) -- error
- Other basic functions
head
gives the first element of the listtail
removes the first element and returns a new listlast
gives the last element of the listinit
returns the list with everything except for the last elementlength
returns the lengthnull
checks if the list is emptyreverse
reverses a listtake
takes the first k elements of the list and returns that as the new list. If it is larger than the list's length then the entire list is returned.drop
drops the first k elements of the list. If larger than the list's size, return an empty list.maximum
,minimum
,sum
andproduct
do what you think they do.elem
takes a list and an element and returns if it was there or not. Usually written infix
All of them fail on an empty list
print(head [1, 2, 3, 4]) print(tail [1, 2]) print(last [1, 3]) print(init [1, 3, 4]) print(take 1 [6, 6, 6]) print(if 4 `elem` [1, 2, 3] then "present" else "Lol")
- Ranges and infinite lists
print([1..20])
-- define a step here print([1, 3..20])
However, be wary of using it on floating point numbers because of precision. Preferably don't use them at all.
print([0.1, 0.3 .. 1])
To make an infinite list, do not specify an upper limit. Haskell won't complain unless you try to evaluate all of it.
print(take 10 [1, 3..])
cycle
repeats a list forever, so you can use it to repeat something in a loopprint(take 10 (cycle [1, 2, 3]))
repeat
takes an element and repeats it to give an infinite listprint(take 10 (repeat 1))
replicate
takes a number and an element and creates a finite list of that argument that many timesprint(replicate 3 'a')
- List Comprehensions in haskell
Basically a method to build a list from another range/list/whatever
print([x * 2| x <- [1..10]])
Apart from this we can also specify a predicate to filter out values
print([x | x<- [50..100], x `mod` 7 == 3])
We can also use any expression in the final result that depends on the list value, such as
print([if x < 10 then "Foo" else "bar" | x<- [1..20], odd x])
We can also specify several predicates and the value has to satisfy all of them.
We can also specify multiple lists and the comprehension will combine them in all manners.
[x*y | x <- [2,3,5], y<- [5,7,8]]
Tuples in haskell
let x = (1, 3) print(x)
They are not extensible like lists but they can contain different types of data such as a string and an int, etc. They can also be 3, 4 or even more.
If Else
They also work as expressions.
:{ doubleSmall x = if x > 100 then x else x * 2 :} doubleSmall 101
Functions in haskell
Function calls are simple, and they take precedence over anything else.
min 9 10
succ 9 * 10 -- succ 9 eval'd first
succ (9*10)
`fun`
makes a function infix, as shown below. We can also define them this way!
print (16 `div` 3, 16 `rem` 3)
print (9 `max` 10)
Every operator is also an infix function.
defining functions
Functions can't begin with uppercase letters.
doubleMe x = x * 2 print(doubleMe 8.3, doubleMe 10)
:{ doubleUs x y = doubleMe x + doubleMe y doubleMe x = x + x :} print(doubleUs 10 11.1)
Function types
removeNonUppercase :: [Char] -> [Char] removeNonUppercase st = [ c | c <- st, c `elem` ['A'..'Z']]
It has the same semantics as other functional programming languages, so multi-argument functions are basically single argument functions that return another function. This also implies partial application of functions.
Pattern matching on multiple functions
We can do a switch case style pattern matching on multiple functions as follows:
:{ myLength :: (Num b) => [a] -> b myLength [] = 0 myLength (_:xs) = 1 + myLength xs :} print(myLength [1])
We can also bind a variable to patterns by using the following syntax:
:{ capital "" = "Empty string, Whoops!" capital all@(x:xs) = "The first letter of " ++ all ++ " is " ++ [x] :} print(capital "foo")
It also has the concept of guards, similar to match guards in Rust.
:{ bmiTell bmi | bmi <= 18.5 = "You're underweight, you emo, you!" | bmi <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!" | bmi <= 30.0 = "You're fat! Lose some weight, fatty!" | otherwise = "You're a whale, congratulations!" -- same as catch all :} print(bmiTell 21)
We can also use the where
keyword to reduce the repetition happening
:{ bmiTell weight height | bmi <= 18.5 = "You're underweight, you emo, you!" | bmi <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!" | bmi <= 30.0 = "You're fat! Lose some weight, fatty!" | otherwise = "You're a whale, congratulations!" where bmi = weight / height ^ 2 :} print(bmiTell 70 1.8)
Add more context with the where
keyword:
:{ bmiTell weight height | bmi <= skinny = "You're underweight, you emo, you!" | bmi <= normal = "You're supposedly normal. Pffft, I bet you're ugly!" | bmi <= fat = "You're fat! Lose some weight, fatty!" | otherwise = "You're a whale, congratulations!" where bmi = weight / height ^ 2 skinny = 18.5 normal = 25.0 fat = 30.0 :} print(bmiTell 60 1.34)
You can also do pattern matching in where bindings.
Type variables
Some functions like head
, fst
etc have the following types
fst :: (a, b) -> a head :: [a] -> a
Typeclasses
A typeclass defines a behaviour, and also a constraint.
(==) :: (Eq a) => a -> a -> Bool
The Eq a
part is a class constraint. It basically says, it takes any two values that are of
the same type and returns a Bool
. Also, the two types must belong to the Eq
class.
Some common typeclasses are:
Eq
: types that have some notion of equality. Members implement both- (
==)
and (/=)
- (
Ord
: Some notion of ordering. To be a member of this you need to be a member ofEq
. Implements basically all the boolean checks. Also implements thecompare
function, that returns an enumOrdering
which is eitherGT
,LT
orEQ
.Show
: Shows the type as a string. Most used function isshow
Read
: Opposite ofShow
.read
reads a string and returns the type which is inferred. If no type can be inferred then it throws an error.Enum
: enumerated basically. We can use list ranges,succ
andpred
etc. The unit type falls in this class. Also,Ordering
,Int
, etc.Bounded
: have upper and lower bounds. Important functions includeminBound
andmaxBound
. Their types are interesting because they do not take an argument, just give an output. So you need to annotate their output type.Num
: numeric. Basically anything that's a number.Integral
andFloating
: obviously
A useful function to deal with numbers is fromIntegral :: (Num b, Integral a) => a -> b
.
We can see that the input is any integer type and the output is a generic num type.
Let bindings
Same as OCaml/Rust, scoped and act as expressions.
There are three situations of using let
:
- In the body of do (which binds to the IO monad)
:{ do let x = 1 x :}
- In list comprehensions (similar to above)
- As an expression (
let x in y
) similar to OCaml
Case expressions
Same as switch case in C/match in OCaml/Rust.
:{ head' :: [a] -> a head' xs = case xs of [] -> error "No head for empty lists!" (x:_) -> x :} print(head' [1,2])
Recursion and Higher Order Functions
Recursion is what it sounds like. Write a recursive function. Since haskell is lazily evaluated, writing infinite recursion is fine. Similar to OCaml, its functions only take in one parameter. And return a function that takes in the rest of the parameters. So partial application aka currying is possible in this scenario.
Similar to OCaml, there is the provision of passing functions as arguments.
Binary operators are special. Since they take two arguments, there are syntactic sugars to resolve which operand you're binding with. This is called sectioning
applyTwice f x = f (f x) applyTwice (++ " HAHA") "HEY" applyTwice ("HAHA " ++) "HEY"
It also has the classic map and filter from other languages. We also have takeWhile
that
does what it sounds like, take while a predicate is true.
Needless to say, it also has anonymous functions/lambdas, which are written like this:
(\x -> x^2) 2
They can also have pattern matching but only of one pattern! If the pattern fails to match it's a runtime failure.
Needless to say, foldl
and foldr
do what they are supposed to. Unlike OCaml, they are smarter
and have the same function signature. There's also foldl1
and foldr1
that automatically take the
first value of the list to be the initial value.
scanl
and scanr
are functions that behave like the two above except they report all the
intermediate accumulator states as a list. Similarly there's scanl1
and scanr1
as well.
Function application operator and composition
We've all come across the classic problem of f(g(h(x)))
. Rust solves this using method chaining,
OCaml using pipes, and haskell using the $
operator. It can be equivalently written as
f $ g $ h x
.
As we have composition in mathematics, we also have composition in haskell, where
\(\left(f \cdot g\right)\left(x\right)\) is written using the dot operator f . g
.
Using the above two features, we can often elide the arguments to a function that's written as a combination of other functions. This is called as points free style of writing functions.
Modules
import Data.List -- for selective imports -- import Data.List (nub, sort) -- for selective rejections -- import Data.List hiding (nub, sort) numUniques = length . nub numUniques [1, 1, 2, 2, 4, 5, 4]
For qualified imports,
import qualified Data.Map as M -- allows for usage of M.filter