- Haskell
- Prolog
- OCaml
- Strongly-typed with polymorphism and generic types
- Higher-order functions
- Pure functions (output depends solely on the input; no side effects; no updates to global variables)
- Lazy (only evaluates what is needed)
- Expressive Type classes (Monads, Arrows, etc.)
putStrLn "Hello World!"
putStrLn :: String -> IO()
A pure function which takes as input a String
, and returns an IO
monad.
-- named function
let inc1 x = x + 1
-- lambda function
let inc2 = \ x -> x + 1
let inc3 = (+ 1)
All 3 lines above are different ways of writing a pure function which takes an integer argument x
, and returns a new integer.
Every expression has a type; use ascription (e :: t
) to force an expression e
to have a given type t
let tuple = ("hello", 2, 3.5)
-- `:t tuple` will be inferred as ([Char], Num, Fractional)
- Type of
tuple
will be inferred without ascriptions (with the most general type)
let tuple = ("hello", 2, 3.5) :: (String, Int, Double)
- Type can be forced to a more specific type by using ascriptions
The void
type is just the empty tuple ()
.
:t ()
-- returns () :: ()
Primitive types in Haskell are boxed types. Unboxed types are built-in but seldom used (except when implementing the compiler).
Boxed/unboxed types are identical to object/primitive types in Java
Advantages of boxed types:
- Supports polymorphism
- Supports lazy evaluation
A more systematic way to capture union types:
let data Data = I Int | F Float | S String deriving Show
-- then, we can build objects of different values:
let v1 = I 3;
let v2 = F 4.5;
let v3 = S "CS2104";
- The
deriving
keyword can be thought of like theextends
keyword in Java, where the typeData
will also inherit the methods in the typeShow
(which is used for printing to stdout)
fact :: Int -> Integer
fact 0 = 1
fact n = (fromIntegral n) * fact(n - 1)
- The
fromIntegral
is necessary to convertn
of typeInt
toInteger
The following will all return an infinite list of 1
s:
xs :: [Int]
-- using recursive binding:
xs = 1:xs
-- using list range notation:
xs = [1,1..]
-- using list comprehension:
xs = [ys | ys <- [1,1..]]
foo =
let greeting = "hello" in
print (greeting ++ " world")
-- prints "hello world"
let
is similar to javascript'slet
, and binds the variable to the current local block scopefoo
in
goes along withlet
to name one or more local expressions in a pure function, and is what is returned to the caller
let quad x =
let add x y = x + y
double x = add x x
in (double x) + (double x)
add
anddouble
are local functionsquad
is a global declaration
What are "binders" in programming language?
- A way to attach identifiers to entities
- Identifiers: the name we give the entity
- Entity: data, objects, operations, functions, etc.
Why do you need binders?
- Organise identifiers/entities into scopes and to group computations
- Basically to give structure to code
How are binders normally implemented?
- In the lexing phase, lexemes are identified based on patterns
- Indentations matter (like Python/Ruby)
<space>
is an infix operator that is left associative (with high precedence)- Use brackets, or the
$
symbol to override to prefix behaviour
- Use brackets, or the
a b c
-- means ((a b) c)
foo n + 1
-- actually means (foo n) + 1
foo $ n + 1
-- now means foo (n + 1)
foo(n + 1)
-- also means foo (n + 1)
What is the "layout rule" syntactic mechanism in programming language?
- The use of indentation/whitespaces to construct a language into logical blocks
Why is this called a two-dimentional syntax mechanism?
- Both rows and columns are taken into consideration
Why would you want to design layout rule mechamism for a new PL?
- Do away with the need for
{}
and;
- Reduce verbosity and cleaner syntax
How are layout rules normally implemented?
- Lexers: classify symbols (lexemes) into meaningful tokens
- Parsers: classify tokens into an AST
What other PL uses layout rule mechanism? Can you compare and contrast their features?
Haskell:
- Same indentation: starts a new clause in the same block
- Greater indentation: continues a current clause
- Less indentation: start of a new block
Similar to set comprehension in Mathematics.
For a set that contains the first ten even natural numbers:
[x * 2 | x <- [1..10]]
>> [2, 4, 6, ..., 18, 20]
-- similar to map
map (\ x -> x * 2) [1..10]
>> [2, 4, 6, ..., 18, 20]
Using filters:
usingFilter xs = [ if x < 10 then x else x * 10 | x <- xs, odd x]
usingFilter [1..20]
>> [1,3,5,7,9,110,130,150,170,190]
x <- xs
is the list generatorodd x
is the filterif x < 10 then x else x * 10
is then mapped to each element in the list
[f x | x <- xs]
map (\ x -> f x) xs
------------------
[f x | x <- xs, x > 5]
map (\ x -> f x) (filter (\ x -> x > 5) xs)
------------------
-- concatMap is similar to JS flatmap
[(x, y) | x <- xs, y <- ys]
concatMap (\ x -> map (\ y -> (x, y)) ys) xs
:t array
>> array :: Ix i => (i, i) -> [(i, e)] -> Array i e
(i, i)
: the bounds of the array give in(low, high)
inclusive(i, e)
: wherei
is the index, ande
the element at that index
-- generate an array of 2s of length 5:
array (1,5) [(i,2) | i <- [1..5]]
>> array (1,5) [(1,2),(2,2),(3,2),(4,2),(5,2)]
-- access at index (follows value of `i` above):
arr ! 5
>> 2
A functor is a container of any generic type A
that, when subjected to a function that maps from A
to any generic type B
, yields the same container of type B
.
In haskell, a Functor
is a typeclass, and any Functor
type needs to have a concrete implementation of fmap
. In JavaScript, a Functor
is any object that has a concrete implementation of the map
function. For example, a JavaScript array is a Functor
because the Array
class has a well defined map
function.
-- when executing `:i Functor`
class Functor (f :: * -> *) where
fmap :: (a -> b) -> f a -> f b
Some types of common functors in Haskell include:
Maybe
[]
IO
- Any function
-- in the source code for the Maybe type:
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just a) = Just (f a)
fmap (+1) (Just 3) -- Maybe is a functor
>> Just 4
instance Functor [] where
fmap = map
map (+1) [1, 2, 3]
>> [2, 3, 4]
fmap (+1) [1, 2, 3] -- [] is a functor
>> [2, 3, 4]
map (+1) (Just 3) -- Maybe is not a list
>> Error!
Essentially, map
is just a specialised version of fmap
which only works on lists.
-- in the source code for the IO type:
instance Functor IO where
fmap f x = x >>= (pure . f)
-- in the source code for the ((->) r) type:
instance Functor ((->) r) where
fmap = (.)
plusFive = fmap (+3) (+2) -- similar to `(.) (+3) (+2)`
plusFive 1
>> 6
fmap
of 2 functions is a composition of those 2 functions
The sequential application <*>
takes a wrapped function and a wrapped value. The output is the result of unwrapping the function and value, applying the function to the value, and then wrapping the result.
(<*>) :: f (a -> b) -> f a -> f b
fmap (Just (+2)) (Just 5)
>> Error!
(Just (+2)) <*> (Just 5)
>> Just 7
Monads apply a function which returns a wrapped value to a wrapped value. It is essentially the functional way of achieving polymorphism, because the wrapped type of the function can be different from the wrapped type of the input value.
In JavaScript, a Monad is a Functor which also implements flatMap
(in addition to implementing map
).
Implementation:
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
-- laws of Monad class
(return a) >>= k = k a
m >>= return = m
-- associativity (order of application does not matter for monads)
-- k and h are monads
(m >>= (\a -> (k a) >>= (\b -> h b)) = m >>= (\a -> k a) >>= (\b -> h b)
Suppose half
is a function that only works on even numbers:
-- half returns a wrapped value (a Maybe type to be exact)
half x = if even x then
Just (div x 2)
else
Nothing
If we pass a wrapped value:
half (Just 4)
>> Error!
We can use the bind operator >>=
to apply half
to the wrapped value Just 4
:
Just 4 >>= half
>> Just 2
Just 5 >>= half
>> Nothing
We can also chain it (like UNIX pipes):
Just 20 >>= half >>= half >>= half
>> Nothing
Using the bind operator >>=
on a list xs
to a function f
is equivalent to using a concatMap
(or map
followed by concat
):
(xs >>= f) = (concatMap f xs) = (concat (map f xs))
list_of_list = [[1], [2], [3]]
return_self = \xs -> xs
concatMap return_self list_of_list
>> [1, 2, 3]
list_of_list >>= return_self
>> [1, 2, 3]
-- types: in ghci `:i >>=` and `:i >>`
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
m >>= k
: runm
, and then run functionk
on the result ofm
m >> n
: runm
, and then runn
, ignoring the result ofm
a >> b
is equivalent toa >>= \_ -> b
In haskell, return
is an example of type inference. Since Haskell is lazily evaluated, the value of an expression and hence its type isn't computed until it really needs to be. This return
is totally different from the return
statements in other languages like C/Java/JS.
return
: function which takes an argument and returns a Monad
with that type in it.
-- in this inc function, it takes a number `a` and returns a Monad `m a`
inc :: (Monad m, Num a) => a -> m a
inc num = return (num + 1)
-- since the list `[1,2,3]` is a Monad, and `inc` is a function which returns a Monad:
[1,2,3] >>= inc
>> [2,3,4]
-- since `Just 5` is a Monad, and `inc` is a function which returns a Monad:
Just 5 >>= inc
>> Just 6
-- `getChar` and `putChar` are both I/O operations in Haskell
echo :: IO ()
echo = do c <- getChar
putChar c
-- similar to using `<<=`:
echo :: IO ()
echo = getChar >>= (\c -> putChar c)
- User character input will be stored in
c
- Then variable
c
is printed to I/O
For a list (using previous example):
-- this bind operation:
[[1], [2], [3]] >>= \xs -> xs
-- can be translated to:
flattened_list = do
xs <- [[1], [2], [3]]
xs
-- both results in:
[1, 2, 3]
printHello :: IO ()
printHello = do putStr "Hello, "
putStrLn "world!"
-- is similar to using `>>`:
printHello = putStr "Hello, " >> putStrLn "world!"
-- is also similar to using `>>=`:
printHello = putStr "Hello, " >>= (\_ -> putStrLn "world!")
- List is an instance of monad
- List comprehension is an instance of do-comprehension
This list comprehension:
[(x,y) | x <- xs, test x, y <- ys]
is similar to this do-comprehension:
filter test = \ x -> if test x then return a else empty
do
x <- xs
filter test
y <- ys
return (a, b)
getLine :: IO String
getLine = do c <- getChar
if c == '\n' then
return ""
else
do l <- getLine
return (c:l)
- Untyped
- Atoms: start with lower-case letters are constants
- eg.
cat
,john
,5
,-1
- eg.
- Variables: start with upper-case letter or underscore
- eg.
X
,Y2
,_var
,_1
- eg.
- Compound term: composed of an atom called a "functor" and a number of "arguments", which are again terms
atom(X)
: succeed ifX
is an atom (or an empty list)atomic(X)
: succeed ifX
is an atom or numbernumber(X)
: succeed ifX
is a numberinteger(X)
: succeed ifX
is an integerfloat(X)
: succeed ifX
is a real numbervar(X)
: succeed ifX
is unbound (a non-instantiated variable)nonvar(X)
: succeed ifX
is boundX == Y
: succeed ifX
andY
are identical (but do not unify them)X \== Y
: succeed ifX
andY
are not identical
X is E
: evaluateE
and unify the result withX
X + Y
: when evaluated, yields the sum ofX
andY
X - Y
: when evaluated, yields the difference ofX
andY
X * Y
: when evaluated, yields the product ofX
andY
X / Y
: when evaluated, yields the quotient ofX
andY
X mod Y
: when evaluated, yields the remainder ofX
divided byY
X =:= Y
: evaluateX
andY
and compare them for equalityX =\= Y
: evaluateX
andY
and succeed if they are not equal
We can provide facts as relations. Ends with a period .
like so:
male(john).
: a fact which says that John is malefather(john, mark).
: a fact which says that Mark's father is John
?- father(X, mark).
: who is the father of Mark?
pred(...) :- pred1(), pred2(), ... predn().
has the logical meaning of pred1() AND pred2() AND ... AND predn()
implies pred(...)
.
sibling(X,Y) :- parent(Z,X), parent(Z,Y), X\==Y.
:X
andY
are siblings (where they are not the same person)
Also supports recursive clauses:
ancestor(X,Y) :- parent(X,Z), ancestor(Z,Y).
Computing a substitution for the variables so that two terms can be made equal:
a = X
: success,X = a
n(a, Y) = n(X, p(a))
: success,X = a
andY = p(a)
a = b
: fail, botha
andb
are atoms, not variablesn(a, X) = n(X, p(a))
: fail,X
cannot be two different things simultaneously
Lists are denoted by square bracket []
with its elements separated by comma ,
: [mary, [], n(A), [1,2,3], X]
.
The append relation can be executed in different ways:
- Appending lists:
?- append([1,2,3], [4,5], Z).
returnsZ = [1,2,3,4,5]
- Computing the difference:
?- append([1,2,3], Y, [1,2,3,4,5]).
returnsY = [4,5]
- Splitting a list:
?- append(X,Y,[1,2]).
returnsX=[], Y=[1,2]; X=[1], Y=[2]; X=[1,2], Y=[].
- Only something which can be proven is true
- Anything which cannot be proven is assumed to be false
Thus, we can use this clause: female(X) :- not(male(X)).
, which says if a person cannot be proven to be male, we shall assume the person is female.
Warning: negation as failure is sound only if the given clauses are complete.
# Assume we have the following rules:
male(kevin).
male(tom).
human(mary).
female(X) :- not(male(X)).
# query for all males:
?- male(X).
X = kevin ;
X = tom.
# query for all females:
?- female(X).
false.
# query if mary females:
?- female(mary).
true.
- When querying if
mary
isfemale
, we cannot prove thatmary
is a male, and as suchmale(mary)
is assumed to befalse.
- However querying for
female(X)
will not work becauseX
must be known
Similar to an if-then-else
statement.
pred :- X1, !, X2, X3, X4.
pred :- Y1, Y2.
- If
X1
is true, then proceed with checkingX2, X3, X4
only - If
X1
is false, backtrack to next statementY1, Y2
Via the clpfd
library, imported using :- use_module(library(clpfd)).
at the top of a prolog file.
Expr1 #+ Expr2
:Expr1
equalsExpr2
Expr1 #\= Expr2
:Expr1
is not equal toExpr2
Expr1 #>= Expr2
:Expr1
is greater than or equal toExpr2
Expr1 #=< Expr2
:Expr1
is less than or equal toExpr2
Expr1 #> Expr2
:Expr1
is greater thanExpr2
Expr1 #< Expr2
:Expr1
is less thanExpr2
In contrast to the primitive operators like in
and =:=
, the constraints can be used in any direction, and do not require the Expr
expressions to be instantiated.
- Purpose is to enumerate and try all possible values (instead of just giving range)
range(X) :-
X#>0,
X#<100.
?- range(X). -- gives the constraint only:
X in 1..99.
?- range(X), label([X]). -- enumerate all possible values of X:
X = 1 ;
X = 2 ;
...
X = 98 ;
X = 99 ;
unit
: consists of exactly one value, written as()
; used to represent type of expressions that return no value (ie. are evaluated for side-effects only)ref
: akin to memory location (C-style pointer); dereference theref
using the prefix!
operator
User defined type, similar to struct
in C.
(* record type must first be declared: *)
type account = {
first_name: string;
last_name: string;
mutable balance: int;
} ;;
(* then initialise a variable with the record type: *)
let acc = {
first_name = "John";
last_name = "Doe";
balance = 5
} ;;
(* access the variables using dot notation: *)
Printf.printf "Name: %s %s\n" acc.first_name acc.last_name ;; (* Name: John Doe *)
Printf.printf "Balance: %d\n" acc.balance ;; (* Balance: 5 *)
(* mutate a mutable variable: *)
acc.balance <- acc.balance * 2 ;;
Printf.printf "Balance: %d\n" acc.balance ;; (* Balance: 10 *)
class counter init =
object (name)
val mutable x = init
method inc = x <- x + 1
method get = x
method set y = x <- y
method print = () = Printf.printf "Count: %d" (name # get)
end
;;
counter
is a factory of the objectinit
is the constructor parametername
is the explicit name of the current object; analogous tothis
in Java- All fields are immutable by default (use
mutable
to make them mutable) - All fields (like
x
) are private and not accessible, and must be called through theget
/set
methods - Setting an object variable is similar to setting a record variable
let p = new counter 0 ;;
let q = new counter 5 ;;
p # print ;; (* Count: 0 *)
p # inc ;;
p # inc ;;
p # print ;; (* Count: 2 *)
q # print ;; (* Count: 5 *)
q # set 10 ;;
q # print ;; (* Count: 10 *)
Inherit fields and methods of superclass, and support method overriding.
class counter_step init step =
object (s)
inherit counter init
method inc = x <- x + step (* override counter's inc method *)
end
;;
let r = new counter_step 0 4 ;;
r # print ;; (* Count: 0 *)
r # inc ;;
r # inc ;;
r # print ;; (* Count: 8 *)
Based on structure of the set of visible methods (since fields are always private). Thus, using counter
example above:
counter : <get : 'a; inc : unit; set : 'a -> unit; print : unit>
'a
refers to a generic type- Two classes are of the same type if they are structurally equivalent to each other based on their visible methods
foo : < get : 'b; .. > -> 'b
foo
will unify with any type with aget
method in its class type- The matched type need not be structurally equivalent
Similar to an abstract
class, a class can be defined with some undefined/virtual
methods:
class virtual ['a] buffer_eq init =
object (this)
val mutable value :'a = init
method get = value
method virtual eq : 'a buffer_eq -> bool
method neq b = not(this # eq b)
end
;;
class ['a] buffer init =
object (this)
inherit ['a] buffer_eq init
(* concrete classes must give concrete implementation of eq: *)
method eq that = this # get = that # get
method set n = value <- n
end
;;
let b = new buffer 5 (* ok *)
let c = new buffer_eq 5 (* error; virtual classes cannot be instantiated *)
- Objects of virtual classes cannot be instantiated
- Concrete subclasses should give definitions for the virtual methods
Used to group types, values, functions, exceptions, and other modules together.
An example of a functional stack implemented using OCaml lists:
module FunctionalStack = struct
type number = Int of int | Float of float
let init () : number list = []
let is_empty xs = (xs = [])
let push xs x = x :: xs
let peek = function
| [] -> failwith "Empty"
| x::_ -> x
let pop = function
| [] -> failwith "Empty"
| _::xs -> xs
let number_to_string = function
| Int x -> string_of_int x
| Float x -> Float.to_string x
let to_string xs = String.concat ", " (List.map number_to_string xs)
let print xs = Printf.printf "[%s]" (to_string xs)
end ;;
Use dot notation to access the definitions within the module:
let stack = FunctionalStack.init () ;;
let stack = FunctionalStack.push stack (Int 1) ;;
let stack = FunctionalStack.push stack (Float 4.5) ;;
let stack = FunctionalStack.push stack (Int 9) ;;
FunctionalStack.print stack ;; (* [9, 4.5, 1] *)
Use open
to expose the definitions within the module (ie. no need to use dot notation):
open FunctionalStack ;;
let stack = init () ;;
let stack = push stack (Int 1) ;;
let stack = push stack (Float 4.5) ;;
let stack = push stack (Int 9) ;;
print stack ;; (* [9, 4.5, 1] *)
Each module has a type signature that may be explicitly declared. For example, we can create the following type signature:
module type STACK = sig
type number = Int of int | Float of float
val init : unit -> number list
val is_empty : number list -> bool
val push : number list -> number -> number list
val peek : number list -> number
val pop : number list -> number list
val print : number list -> unit
end
And associate the original FunctionalStack
with the STACK
signature like so:
module FunctionalStack : STACK = struct
(* ... same code as above ... *)
end
Notice that STACK
does not have definitions for number_to_string
and to_string
. As such, information hiding can be implemented (ie. make some fields private and not callable).
open FunctionalStack ;;
let stack = init () ;;
number_to_string (Int 5) ;; (* error: unbound value number_to_string *)
to_string stack ;; (* error: unbound value to_string *)
print stack ;; (* ok! STACK has definition for print *)
A function from structures to structures. It is OCaml's way of creating parametrised structures.
As an example, we first create a module type NUMBER_TYPE
:
module type NUMBER_TYPE = sig
type t
val value : t ref
val inc : unit -> unit
val print : unit -> unit
end ;;
Then, we define the Number
module, a functor which takes as input another structure of type NUMBER_TYPE
:
module Number =
functor (Elt: NUMBER_TYPE) ->
struct
type t = Elt.t
let value : t ref = Elt.value
let inc = Elt.inc
let print = Elt.print
end
;;
Then, define the Integer
and Float
modules:
module Integer : NUMBER_TYPE = struct
type t = int
let value : t ref = ref 0
let inc () = value := !value + 1
let print () = Printf.printf "value: %s\n" (string_of_int !value)
end ;;
module Float : NUMBER_TYPE = struct
type t = float
let value : t ref = ref 0.0
let inc () = value := Float.add !value 0.1
let print () = Printf.printf "value: %s\n" (Float.to_string !value)
end ;;
Since Number
is a functor, we can create specific modules like so:
module INumber = Number(Integer) ;;
module FNumber = Number(Float) ;;
(* usage: *)
INumber.print () ;; (* value: 0 *)
FNumber.print () ;; (* value: 0. *)
INumber.inc () ;;
INumber.inc () ;;
FNumber.inc () ;;
INumber.print () ;; (* value: 2 *)
FNumber.print () ;; (* value: 0.1 *)