404
+ +Page not found :(
+The requested page could not be found.
+diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/404.html b/404.html new file mode 100644 index 00000000..0c8c7584 --- /dev/null +++ b/404.html @@ -0,0 +1,121 @@ + +
+ + + +Page not found :(
+The requested page could not be found.
+These are course notes for a second-year Programming Paradigms course that focuses on introducing functional programming concepts, first in JavaScript, then introducing type systems through TypeScript, and finally diving in to Haskell.
+ +The Markdown source for these notes is available at GitHub. Pull-requests with corrections or suggested changes are most welcome.
+ + + ++ + + 45 + + min read
+FunctionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + Reactive Programming (specifically the ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming. +/Observer pattern) allows us to capture asynchronousOperations that occur independently of the main program flow, allowing the program to continue executing while waiting for the operation to complete. + actions like user interfaceA TypeScript construct that defines the shape of an object, specifying the types of its properties and methods. + events in streams. These allow us to “linearise” the flow of control, avoid deeply nested loops, and process the stream with pure, referentially transparent functions.
+ +As an example we will build a little “Asteroids” game using FRP. We’re going to use RxJS as our ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming. + implementation, and we are going to render it in HTMLHyper-Text Markup Language - the declarative language for specifying web page content. + using SVGScalable Vector Graphics - another part of the HTML standard for specifying images declaratively as sets of shapes and paths. +. +We’re also going to take some pains to make pure functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + code (and lots of beautiful curried lambda (arrow) functions). We’ll use typescript type annotationsA syntax in TypeScript where types are explicitly specified for variables, function parameters, and return types to ensure type safety and correctness. + to help us ensure that our data is indeed immutable and to guide us in plugging everything together without type errors into a nicely decoupled Model-View-Controller (MVC) architecture:
+ +If you’re the kind of person who likes to work backwards, you can jump straight to playing the final result and you can also live edit its code.
+ +We’ll build it up in several steps.
+ +First, we’ll just rotate the ship
+ + +Let’s start by making the svgScalable Vector Graphics - another part of the HTML standard for specifying images declaratively as sets of shapes and paths. + with a simple polygon for the ship. It will look like this:
+ +And here’s the snippet of htmlHyper-Text Markup Language - the declarative language for specifying web page content. + that creates the ship:
+ +<svg width="150" height="150" style="background-color:black">
+ <g id="ship" transform="translate(75,75)">
+ <polygon points="-15,20 15,20 0,-20"
+ style="fill:lightblue">
+ </polygon>
+ </g>
+</svg>
+
Note that the ship is rendered inside a transform group <g>
. We will be changing the transform
attribute to move the ship around.
To begin with we’ll make it possible for the player to rotate the ship with the arrow keys. First, by directly adding listeners to keyboard events. Then, by using events via ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming. + streams. Here’s a preview of what it’s going to look like (or you can play with it in a live editor):
+ + + +There are basically just two states, as sketched in the following state machine:
+ +The first event we assign a function to is the window load event. This function will not be invoked until the page is fully loaded, and therefore the SVGScalable Vector Graphics - another part of the HTML standard for specifying images declaratively as sets of shapes and paths. + objects will be available. Thus, our code begins:
+ +window.onload = function() {
+ const ship = document.getElementById("ship")!;
+ ...
+
So ship
will reference the SVGScalable Vector Graphics - another part of the HTML standard for specifying images declaratively as sets of shapes and paths.
+ <g>
whose transform attribute we will be manipulating to move it. To apply an incremental movement, such as rotating the ship by a certain angle relative to its current orientation, we will need to store that current location. We could read it out of the transform attribute stored in the SVGScalable Vector Graphics - another part of the HTML standard for specifying images declaratively as sets of shapes and paths.
+, but that requires some messy string parsing. We’ll just store the state in a local object, which we will keep up to date as we move the ship. For now, all we have is the ship’s position (x and y coordinates) and rotation angle:
...
+ const state = {
+ x:100, y:100, angle:0
+ }
+...
+
Next, we need to specify a function to be invoked on keydown events:
+ +...
+ document.onkeydown = function(d:KeyboardEvent) {
+...
+
Inside this function, we are only interested in left and right arrow keys. If the keys are held down, after a moment they may start repeating automatically (this is OS dependent) and will churn out continuous keydown events. We filter these out too by inspecting the KeyboardEvent.repeat property:
+ +...
+ if((d.key === "ArrowLeft" || d.key === "ArrowRight") && !d.repeat) {
+...
+
Let’s say we want a left- or right-arrow keydown event to start the ship rotating, and we want to keep rotating until the key is released. To achieve this, we use the builtin setInterval(f,i)
function, which invokes the function f
repeatedly with the specified interval i
delay (in milliseconds) between each invocation. setInterval
returns a numeric handle which we need to store so that we can clear the interval behaviour later.
const handle = setInterval(function() {
+ ship.setAttribute('transform',
+ `translate(${state.x},${state.y}) rotate(${state.angle+=d.key === "ArrowLeft" ? -1 : 1})`)
+ }, 10);
+
So as promised, this function is setting the transform
property on the ship, using the position and angle information stored in our local state
object. We compute the new position by deducting or removing 1 (degree) from the angle (for a left or right rotation respectively) and simultaneously update the state object with the new angle.
+Since we specify 10 milliseconds delay, the ship will rotate 100 times per second.
We’re not done yet. We have to stop the rotation on keyup by calling clearInterval
, for the specific interval we just created on keydown (using the handle
we stored). To do this, we’ll use document.addEventListener
to specify a separate keyup handler for each keydown event, and since we will be creating a new keyup listener for each keydown event, we will also have to cleanup after ourselves or we’ll have a memory (event) leak:
...
+ const keyupListener = function(u:KeyboardEvent) {
+ if(u.key === d.key) {
+ clearInterval(handle);
+ document.removeEventListener('keyup',keyupListener);
+ }
+ };
+ document.addEventListener("keyup",keyupListener);
+ }
+ }
+}
+
And finally we’re done. But it was surprisingly messy for what should be a relatively straightforward and commonplace interaction. Furthermore, the imperativeImperative programs are a sequence of statements that change a programs state. This is probably the dominant paradigm for programming languages today. Languages from Assembler to Python are built around this concept and most modern languages still allow you to program in this style.
+ style code above finished up deeply nested with function declarations inside function declarations, inside if
s and variables like d
, handle
and keyupListener
are referenced from inside these nested function scopes in ways that are difficult to read and make sense of. The state machine is relatively straightforward, but it’s tangled up by imperativeImperative programs are a sequence of statements that change a programs state. This is probably the dominant paradigm for programming languages today. Languages from Assembler to Python are built around this concept and most modern languages still allow you to program in this style.
+ code blocks.
ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ (we’ll use the implementation from RxJS) wraps common asynchronousOperations that occur independently of the main program flow, allowing the program to continue executing while waiting for the operation to complete.
+ actions like user events and intervals in streams, that we can process with a chain of ‘operators’ applied to the chain through a pipe
.
We start more or less the same as before, inside a function applied on window.onload
and we still need local variables for the ship visual and its position/angle:
window.onload = function() {
+ const
+ ship = document.getElementById("ship")!,
+ state = { x:100, y:100, angle:0 };
+...
+
But now we use the RxJS fromEvent
function to create an ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ keydown$
(the “$” is a convention indicating the variable is a stream),
const keydown$ = fromEvent<KeyboardEvent>(document, 'keydown');
+
The objects coming through the stream are of type KeyboardEvent
, meaning they have the key
and repeat
properties we used before. We can create a new stream which filters these out:
const arrowKeys$ = keydown$.pipe(
+ filter(({key})=>key === 'ArrowLeft' || key === 'ArrowRight'),
+ filter(({repeat})=>!repeat));
+
To duplicate the behaviour of our event driven version we need to rotate every 10ms. We can make a stream which fires every 10ms using interval(10)
, which we can “graft” onto our arrowKeys$
stream using mergeMap
. We use takeUntil
to terminate the interval on a 'keyup'
, filtered to ignore keys other than the one that initiated the 'keydown'
. At the end of the mergeMap
pipe
we use map
to return d
, the original keydown KeyboardEvent
object. Back at the top-level pipe
on arrowKeys$ we inspect this KeyboardEvent
object to see whether we need a left or right rotation (positive or negative angle). Thus, angle$
is just a stream of -1
and 1
.
const angle$ = arrowKeys$.pipe(
+ mergeMap(d=>interval(10).pipe(
+ takeUntil(fromEvent<KeyboardEvent>(document, 'keyup').pipe(
+ filter(({key})=>key === d.key)
+ )),
+ map(_=>d))
+ ),
+ map(d=>d.key==='ArrowLeft'?-1:1));
+
Finally, we subscribe
to the angle$
stream to perform our effectful code, updating state
and rotating ship
.
angle$.subscribe(a=>
+ ship.setAttribute('transform',
+ `translate(${state.x},${state.y}) rotate(${state.angle+=a})`)
+ )
+}
+
Arguably, the ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming. + code has many advantages over the event handling code:
+ +fromEvent
and interval
automatically clean up the underlying events and interval handles when the streams complete.pipe
s.subscribe
s) to them allows us to reuse and plug them together in powerful ways.A weakness of the above implementation using ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ streams, is that we still have global mutable state. Deep in the function passed to subscribe we alter the angle attribute on the state
object. Another ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ operator scan
, allows us to capture this state transformation inside the stream, using a pure functionA function that always produces the same output for the same input and has no side effects.
+ to transform the state, i.e. a function that takes an input state object and—rather than altering it in-place—creates a new output state object with whatever change is required.
We’ll start by altering the start of our code to define an interface
for State
with readonly
members, and we’ll place our initialState
in a const
variable that matches this interfaceA TypeScript construct that defines the shape of an object, specifying the types of its properties and methods.
+. You can also play with the code in a live editor.
window.onload = function() {
+ type State = Readonly<{
+ x: number;
+ y: number;
+ angle: number;
+ }>
+ const initialState: State = { x: 100, y: 100, angle: 0};
+
Now we’ll create a function that is a pure transformation of State
:
...
+ function rotate(s:State, angleDelta:number): State {
+ return { ...s, // copies the members of the input state for all but:
+ angle: s.angle + angleDelta // only the angle is different in the new State
+ }
+ }
+...
+
Next, we have another, completely self contained function to update the SVGScalable Vector Graphics - another part of the HTML standard for specifying images declaratively as sets of shapes and paths. + for a given state:
+ +...
+ function updateView(state:State): void {
+ const ship = document.getElementById("ship")!;
+ ship.setAttribute('transform',
+ `translate(${state.x},${state.y}) rotate(${state.angle})`)
+ }
+...
+
And now our main pipe
(collapsed into one) ends with a scan
which “transduces” (transforms and reduces) our state, and the subscribe
is a trivial call to updateView
:
...
+ fromEvent<KeyboardEvent>(document, 'keydown')
+ .pipe(
+ filter(({code})=>code === 'ArrowLeft' || code === 'ArrowRight'),
+ filter(({repeat})=>!repeat),
+ mergeMap(d=>interval(10).pipe(
+ takeUntil(fromEvent<KeyboardEvent>(document, 'keyup').pipe(
+ filter(({code})=>code === d.code)
+ )),
+ map(_=>d))
+ ),
+ map(({code})=>code==='ArrowLeft'?-1:1),
+ scan(rotate, initialState))
+ .subscribe(updateView)
+}
+
The code above is a bit longer than what we started with, but it’s starting to lay a more extensible framework for a more complete game. And it has some nice architectural properties, in particular we’ve completely decoupled our view code from our state management. We could swap out SVGScalable Vector Graphics - another part of the HTML standard for specifying images declaratively as sets of shapes and paths.
+ for a completely different UI by replacing the updateView
function.
Classic Asteroids is more of a space flight simulator in a weird toroidal topology than the “static” rotation that we’ve provided above. We will make our spaceship a freely floating body in space, with directional and rotational velocity. +We are going to need more inputs than just left and right arrow keys to pilot our ship too.
+ +Here’s a sneak preview of what this next stage will look like (click on the image to try it out in a live code editor):
+ + + +Let’s start with adding “thrust” in response to up arrow.
+With the code above, adding more and more intervals triggered by key down events would get increasingly messy.
+Rather than have streams triggered by key events, for the purposes of simulation it makes sense that our main ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ be a stream of discrete timesteps. Thus, we’ll be moving our interval(10)
to the top of our pipe.
Before we had just left and right rotations coming out of our stream. Our new stream is going to have multiple types of actions as payload:
+ +Tick
- a discrete timestep in our simulation, triggered by interval
.Rotate
- a ship rotation triggered by left or right arrows keysThrust
- fire the boosters! Using the up-arrow keyWe’ll create separate ObservablesA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming. + for each of the key events. There’s a repetitive pattern in creating each of these ObservablesA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming. +, for a given:
+ +Event
- keydown or keyupKey
- one of the arrows (for now)It sounds like something we can model with a nice reusable function:
+ + type Event = 'keydown' | 'keyup'
+ type Key = 'ArrowLeft' | 'ArrowRight' | 'ArrowUp'
+ const observeKey = <T>(eventName:Event, k:Key, result:()=>T)=>
+ fromEvent<KeyboardEvent>(document,eventName)
+ .pipe(
+ filter(({code})=>code === k),
+ filter(({repeat})=>!repeat),
+ map(result)),
+
Now we have all the pieces to create a whole slew of input streams (the definitions for Rotate
and Thrust
classes are below):
const
+ startLeftRotate = observeKey('keydown','ArrowLeft',()=>new Rotate(-.1)),
+ startRightRotate = observeKey('keydown','ArrowRight',()=>new Rotate(.1)),
+ stopLeftRotate = observeKey('keyup','ArrowLeft',()=>new Rotate(0)),
+ stopRightRotate = observeKey('keyup','ArrowRight',()=>new Rotate(0)),
+ startThrust = observeKey('keydown','ArrowUp', ()=>new Thrust(true)),
+ stopThrust = observeKey('keyup','ArrowUp', ()=>new Thrust(false))
+
Since we’re going to start worrying about the physics of our simulation, we’re going to need some helper code. First, a handy dandy Vector class. It’s just standard vector maths, so hopefully self explanatory. In the spirit of being pure and declarativeDeclarative languages focus on declaring what a procedure (or function) should do rather than how it should do it.
+ I’ve made it immutable, and all the functions (except len
which returns a number) return new instances of Vec
rather than changing its data in place.
class Vec {
+ constructor(public readonly x: number = 0, public readonly y: number = 0) {}
+ add = (b:Vec) => new Vec(this.x + b.x, this.y + b.y)
+ sub = (b:Vec) => this.add(b.scale(-1))
+ len = ()=> Math.sqrt(this.x*this.x + this.y*this.y)
+ scale = (s:number) => new Vec(this.x*s,this.y*s)
+ ortho = ()=> new Vec(this.y,-this.x)
+ rotate = (deg:number) =>
+ (rad =>(
+ (cos,sin,{x,y})=>new Vec(x*cos - y*sin, x*sin + y*cos)
+ )(Math.cos(rad), Math.sin(rad), this)
+ )(Math.PI * deg / 180)
+
+ static unitVecInDirection = (deg: number) => new Vec(0,-1).rotate(deg)
+ static Zero = new Vec();
+}
+
To implement the toroidal topology of space, we’ll need to know the canvas size.
+For now, we’ll hard code it in a constant CanvasSize
. Alternately, we could query it from the svgScalable Vector Graphics - another part of the HTML standard for specifying images declaratively as sets of shapes and paths.
+ element, or we could set the SVGScalable Vector Graphics - another part of the HTML standard for specifying images declaratively as sets of shapes and paths.
+ size—maybe later.
+The torus wrapping function will use the CanvasSize
to determine the bounds and simply teleport any Vec
which goes out of bounds to the opposite side.
const
+ CanvasSize = 200,
+ torusWrap = ({x,y}:Vec) => {
+ const wrap = (v:number) =>
+ v < 0 ? v + CanvasSize : v > CanvasSize ? v - CanvasSize : v;
+ return new Vec(wrap(x),wrap(y))
+ };
+
We’ll use Vec
in a slightly richer set of State.
type State = Readonly<{
+ pos:Vec,
+ vel:Vec,
+ acc:Vec,
+ angle:number,
+ rotation:number,
+ torque:number
+ }>
+
We create an initialState
using CanvasSize
to start the spaceship at the centre:
const initialState:State = {
+ pos: new Vec(CanvasSize/2,CanvasSize/2),
+ vel: Vec.Zero,
+ acc: Vec.Zero,
+ angle:0,
+ rotation:0,
+ torque:0
+ }
+
Now we are ready to define the actions that are triggered by the key events. Each action modifies state in some way. Let’s define a common type for actions with a single pure method that takes a previous state, and returns a new state after applying the action.
+ +interface Action {
+ apply(s: State): State;
+}
+
Now we define a class implementing Action
for each of Tick
, Rotate
, and Thrust
:
class Tick implements Action {
+ constructor(public readonly elapsed:number) {}
+ apply(s:State):State {
+ return {...s,
+ rotation: s.rotation+s.torque,
+ angle: s.angle+s.rotation,
+ pos: torusWrap(s.pos.add(s.vel)),
+ vel: s.vel.add(s.acc)
+ }
+ }
+}
+class Rotate implements Action {
+ constructor(public readonly direction:number) {}
+ apply(s:State):State {
+ return {...s,
+ torque: this.direction
+ }
+ }
+}
+class Thrust implements Action {
+ constructor(public readonly on:boolean) {}
+ apply(s:State):State {
+ return {...s,
+ acc: this.on ? Vec.unitVecInDirection(s.angle).scale(0.05)
+ : Vec.Zero
+ }
+ }
+}
+
And now our function to reduce state is very simple, taking advantage of sub-type polymorphism to apply the correct update to State
:
reduceState = (s: State, action: Action) => action.apply(s);
+
And finally we merge
our different inputs and scan over State
, and the final subscribe
calls the updateView
, once again, a self-contained function which does whatever is required to render the State. We describe the updated updateView
in the next section.
interval(10)
+ .pipe(
+ map(elapsed=>new Tick(elapsed)),
+ merge(startLeftRotate,startRightRotate,stopLeftRotate,stopRightRotate),
+ merge(startThrust,stopThrust),
+ scan(reduceState, initialState))
+ .subscribe(updateView);
+
Note, there are two versions of merge
in RxJS. One is a function which merges multiple ObservablesA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ and returns a new ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+, it is imported from 'rxjs'
. Here we are using the operator version of merge
to merge additional ObservablesA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ into the pipe, imported from 'rxjs/operators'
.
Once again, the above completely decouples the view from state management. But now we have a richer state, we have more stuff we can show in the view. We’ll start with a little CSSCascading Style Sheets - another declarative (part of the HTML5 standard) for specifying reusable styles for web page rendering. +, not only to style elements, but also to hide or show flame from our boosters.
+ +.ship {
+ fill: lightblue;
+}
+.booster {
+ fill: orange;
+}
+.hidden {
+ visibility:hidden;
+}
+
We add a few more polygons for the booster flames:
+ +<script src="/bundle.js"></script>
+<svg id="svgCanvas" width="200" height="200" style="background-color:black">
+ <g id="ship" transform="translate(100,100)">
+ <polygon class="booster hidden" id="forwardThrust" points="-3,20 0,35 3,20">
+ </polygon>
+ <polygon class="booster hidden" id="leftThrust" points="2,-10 15,-12 2,-14">
+ </polygon>
+ <polygon class="booster hidden" id="rightThrust" points="-2,-10 -15,-12 -2,-14">
+ </polygon>
+ <polygon class="ship" points="-15,20 15,20 0,-20">
+ </polygon>
+ </g>
+</svg>
+
And here’s our updated updateView function where we not only move the ship but also show flames shooting out of it as it powers around the torus:
+ + function updateView(s: State) {
+ const
+ ship = document.getElementById("ship")!,
+ show = (id:string,condition:boolean)=>((e:HTMLElement) =>
+ condition ? e.classList.remove('hidden')
+ : e.classList.add('hidden'))(document.getElementById(id)!);
+ show("leftThrust", s.torque<0);
+ show("rightThrust", s.torque>0);
+ show("forwardThrust", s.acc.len()>0);
+ ship.setAttribute('transform', `translate(${s.pos.x},${s.pos.y}) rotate(${s.angle})`);
+ }
+
Things get more complicated when we start adding more objects to the canvas that all participate in the physics simulation. Furthermore, objects like asteroids and bullets will need to be added and removed from the canvas dynamically—unlike the ship whose visual is currently defined in the svg
and never leaves.
However, we now have all the pieces of our MVC architecture in place, all tied together with an observableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming. + stream:
+ +So completing the game is just a matter of:
+ +Shoot
)updateView
function so the user can see itWe’ll start with bullets that can be fired with the Space key, and which expire after a set period of time:
+ + + +The first complication is generalising bodies that participate in the force model with their own type Body
, separate from the State
:
type Body = Readonly<{
+ id:string,
+ pos:Vec,
+ vel:Vec,
+ thrust:boolean,
+ angle:number,
+ rotation:number,
+ torque:number,
+ radius:number,
+ createTime:number
+ }>
+ type State = Readonly<{
+ time:number,
+ ship:Body,
+ bullets:ReadonlyArray<Body>,
+ exit:ReadonlyArray<Body>,
+ objCount:number
+ }>
+
So the ship
is a Body
, and we will have collections of Body
for both bullets
and rocks
. What’s this exit
thing? Well, when we remove something from the canvas, e.g. a bullet, we’ll create a new state with a copy of the bullets
array minus the removed bullet, and we’ll add that removed bullet—together with any other removed Body
s—to the exit
array. This notifies the updateView
function that they can be removed.
Note the objCount
. This counter is incremented every time we add a Body
and gives us a way to create a unique id that can be used to match the Body
against its corresponding view object.
Now we define functions to create objects:
+ + function createBullet(s:State):Body {
+ const d = Vec.unitVecInDirection(s.ship.angle);
+ return {
+ id: `bullet${s.objCount}`,
+ pos:s.ship.pos.add(d.scale(s.ship.radius)),
+ vel:s.ship.vel.add(d.scale(2)),
+ createTime:s.time,
+ thrust:false,
+ angle:0,
+ rotation:0,
+ torque:0,
+ radius:3
+ }
+ }
+ function createShip():Body {
+ return {
+ id: 'ship',
+ pos: new Vec(CanvasSize/2,CanvasSize/2),
+ vel: Vec.Zero,
+ thrust:false,
+ angle:0,
+ rotation:0,
+ torque:0,
+ radius:20,
+ createTime:0
+ }
+ }
+ const initialState:State = {
+ time:0,
+ ship: createShip(),
+ bullets: [],
+ exit: [],
+ objCount: 0
+ }
+
We’ll add a new action type and observableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming. + for shooting with the space bar:
+ +class Shoot implements Action {
+ /**
+ * a new bullet is created and added to the bullets array
+ * @param s State
+ * @returns new State
+ */
+ apply = (s: State) => ({ ...s,
+ bullets: s.bullets.concat([createBullet(s)]),
+ objCount: s.objCount + 1
+ })
+}
+const shoot = keyObservable('keydown','Space', ()=>new Shoot())
+
And now a function to move objects, same logic as before but now applicable to any Body
:
const moveObj = (o:Body) => <Body>{
+ ...o,
+ rotation: o.rotation + o.torque,
+ angle:o.angle+o.rotation,
+ pos:torusWrap(o.pos.sub(o.vel)),
+ vel:o.thrust?o.vel.sub(Vec.unitVecInDirection(o.angle).scale(0.05)):o.vel
+ }
+
And our Tick
action is a little more complicated now:
class Tick implements Action {
+ constructor(public readonly elapsed:number) {}
+ apply(s:State):State {
+ const not = <T>(f:(x:T)=>boolean)=>(x:T)=>!f(x),
+ expired = (b:Body)=>(this.elapsed - b.createTime) > 100,
+ expiredBullets:Body[] = s.bullets.filter(expired),
+ activeBullets = s.bullets.filter(not(expired));
+ return <State>{...s,
+ ship:moveObj(s.ship),
+ bullets:activeBullets.map(moveObj),
+ exit:expiredBullets,
+ time:this.elapsed
+ }
+ }
+}
+
Note that bullets have a life time (presumably they are energy balls that fizzle into space after a certain time). When a bullet expires it is sent to exit
.
We merge the Shoot stream in as before:
+ + interval(10).pipe(
+...
+ merge(shoot),
+...
+
And we tack a bit on to updateView
to draw and remove bullets:
function updateView(s: State) {
+...
+ const svg = document.getElementById("svgCanvas")!;
+ s.bullets.forEach(b=>{
+ const createBulletView = ()=>{
+ const v = document.createElementNS(svg.namespaceURI, "ellipse")!;
+ v.setAttribute("id",b.id);
+ v.classList.add("bullet")
+ svg.appendChild(v)
+ return v;
+ }
+ const v = document.getElementById(b.id) || createBulletView();
+ v.setAttribute("cx",String(b.pos.x))
+ v.setAttribute("cy",String(b.pos.y))
+ v.setAttribute("rx", String(b.radius));
+ v.setAttribute("ry", String(b.radius));
+ })
+ s.exit.forEach(o=>{
+ const v = document.getElementById(o.id);
+ if(v) svg.removeChild(v)
+ })
+ }
+
Finally, we make a quick addition to the CSSCascading Style Sheets - another declarative (part of the HTML5 standard) for specifying reusable styles for web page rendering. + so that the bullets are a different colour to the background:
+ +.bullet {
+ fill: red;
+}
+
So far the game we have built allows you to hoon around in a space-ship blasting the void with fireballs which is kind of fun, but not very challenging. The Asteroids game doesn’t really become “Asteroids” until you actually have… asteroids. Also, you should be able to break them up with your blaster and crashing into them should end the game. Here’s a preview:
+ + + +Before we go forward, let’s put all the magic numbers that are starting to permeate our code in one, immutable place:
+ + const
+ Constants = {
+ CanvasSize: 600,
+ BulletExpirationTime: 1000,
+ BulletRadius: 3,
+ BulletVelocity: 2,
+ StartRockRadius: 30,
+ StartRocksCount: 5,
+ RotationAcc: 0.1,
+ ThrustAcc: 0.1,
+ StartTime: 0
+ } as const
+
We will need to store two new pieces of state: the collection of asteroids (rocks
) which is another array of Body
, just like bullets; and also a boolean that will become true
when the game ends due to collision between the ship and a rock.
type State = Readonly<{
+ ...
+ rocks:ReadonlyArray<Body>,
+ gameOver:boolean
+ }>
+
Since bullets and rocks are both just circular Body
s with constant velocity, we can generalise what was previously the createBullet
function to create either:
type ViewType = 'ship' | 'rock' | 'bullet'
+ const createCircle = (viewType: ViewType)=> (oid:number)=> (time:number)=> (radius:number)=> (pos:Vec)=> (vel:Vec)=>
+ <Body>{
+ createTime: time,
+ pos:pos,
+ vel:vel,
+ thrust: false,
+ angle:0, rotation:0, torque:0,
+ radius: radius,
+ id: viewType+oid,
+ viewType: viewType
+ };
+
Our initial state is going to include several rocks drifting in random directions, as follows:
+ + const
+ startRocks = [...Array(Constants.StartRocksCount)]
+ .map((_,i)=>createCircle("rock")(i)
+ (Constants.StartTime)(Constants.StartRockRadius)(Vec.Zero)
+ (new Vec(0.5 - Math.random(), 0.5 - Math.random()))),
+ initialState:State = {
+ time:0,
+ ship: createShip(),
+ bullets: [],
+ rocks: startRocks,
+ exit: [],
+ objCount: Constants.StartRocksCount,
+ gameOver: false
+ }
+
Our tick
function is more or less the same as above, but it will apply one more transformation to the state that it returns, by applying the following function. This function checks for collisions between the ship and rocks, and also between bullets and rocks.
// check a State for collisions:
+ // bullets destroy rocks spawning smaller ones
+ // ship colliding with rock ends game
+ const handleCollisions = (s:State) => {
+ const
+ // Some array utility functions
+ not = <T>(f:(x:T)=>boolean)=>(x:T)=>!f(x),
+ mergeMap = <T, U>(
+ a: ReadonlyArray<T>,
+ f: (a: T) => ReadonlyArray<U>
+ ) => Array.prototype.concat(...a.map(f)),
+
+ bodiesCollided = ([a,b]:[Body,Body]) => a.pos.sub(b.pos).len() < a.radius + b.radius,
+ shipCollided = s.rocks.filter(r=>bodiesCollided([s.ship,r])).length > 0,
+ allBulletsAndRocks = mergeMap(s.bullets, b=> s.rocks.map(r=>([b,r]))),
+ collidedBulletsAndRocks = allBulletsAndRocks.filter(bodiesCollided),
+ collidedBullets = collidedBulletsAndRocks.map(([bullet,_])=>bullet),
+ collidedRocks = collidedBulletsAndRocks.map(([_,rock])=>rock),
+
+ // spawn two children for each collided rock above a certain size
+ child = (r:Body,dir:number)=>({
+ radius: r.radius/2,
+ pos:r.pos,
+ vel:r.vel.ortho().scale(dir)
+ }),
+ spawnChildren = (r:Body)=>
+ r.radius >= Constants.StartRockRadius/4
+ ? [child(r,1), child(r,-1)] : [],
+ newRocks = mergeMap(collidedRocks, spawnChildren)
+ .map((r,i)=>createCircle('rock')(s.objCount + i)(s.time)(r.radius)(r.pos)(r.vel)),
+
+ // search for a body by id in an array
+ elem = (a:ReadonlyArray<Body>) => (e:Body) => a.findIndex(b=>b.id === e.id) >= 0,
+ // array a except anything in b
+ except = (a:ReadonlyArray<Body>) => (b:Body[]) => a.filter(not(elem(b)))
+
+ return <State>{
+ ...s,
+ bullets: except(s.bullets)(collidedBullets),
+ rocks: except(s.rocks)(collidedRocks).concat(newRocks),
+ exit: s.exit.concat(collidedBullets,collidedRocks),
+ objCount: s.objCount + newRocks.length,
+ gameOver: shipCollided
+ }
+ },
+
Finally, we need to update the updateView
function. Again, the view update is the one place in our program where we allow imperativeImperative programs are a sequence of statements that change a programs state. This is probably the dominant paradigm for programming languages today. Languages from Assembler to Python are built around this concept and most modern languages still allow you to program in this style.
+ style, effectful code. Called only from the subscribe at the very end of our ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ chain, and not mutating any state that is read anywhere else in the ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+, we ensure that is not the source of any but the simplest display bugs, which we can hopefully diagnose with local inspection of this one function.
First, we need to update the visuals for each of the rocks, but these are the same as bullets. The second, slightly bigger, change, is simply to display the text “Game Over” on s.gameover
true.
function updateView(s: State) {
+ ...
+ s.bullets.forEach(updateBodyView);
+ s.rocks.forEach(updateBodyView);
+ s.exit.forEach(o=>{
+ const v = document.getElementById(o.id);
+ if(v) svg.removeChild(v);
+ })
+ if(s.gameOver) {
+ subscription.unsubscribe();
+ const v = document.createElementNS(svg.namespaceURI, "text")!;
+ attr(v,{
+ x: Constants.CanvasSize/6,
+ y: Constants.CanvasSize/2,
+ class: "gameover"
+ });
+ v.textContent = "Game Over";
+ svg.appendChild(v);
+ }
+ }
+
where we’ve created a little helper function attr
to bulk set properties on an Element
:
const
+ attr = (e:Element, o:{ [key:string]: Object }) =>
+ { for(const k in o) e.setAttribute(k,String(o[k])) },
+
Note that we need to specify the types of values inside o
, otherwise, we will get an implicit any error.
The other thing happening at game over, is the call to subscription.unsubscribe
. This subscription
is the object returned by the subscribe call on our main ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+:
const subscription = interval(10).pipe(
+ map(elapsed=>new Tick(elapsed)),
+ merge(startLeftRotate,startRightRotate,stopLeftRotate,stopRightRotate),
+ merge(startThrust,stopThrust),
+ merge(shoot),
+ scan(reduceState, initialState)
+ ).subscribe(updateView);
+
This section here, is key to FRP, with four key ingredients:
+ +Any other game in FRP style, will likely involve an almost identical skeleton of:
+ + const subscription = interval(FRAME_RATE).pipe(
+ map(elapsed=>new Tick(elapsed)),
+ merge(USER_INPUT_STREAMS),
+ scan(reduceState, INITIAL_GAME_STATE)
+ ).subscribe(updateView);
+
The job of the programmer (e.g., you in Assignment 1) will be to create appropriate functions to handle modification of the state, view, and inputs to ensure correct behaviour of the chosen game.
+ +Finally, we need to make a couple more additions to the CSSCascading Style Sheets - another declarative (part of the HTML5 standard) for specifying reusable styles for web page rendering. + to display the rocks and game over text:
+ +.rock {
+ fill: burlywood;
+}
+.gameover {
+ font-family: sans-serif;
+ font-size: 80px;
+ fill: red;
+}
+
+
+At this point we have more-or-less all the elements of a game. The implementation above could be extended quite a lot. For example, we could add score, ability to restart the game, multiple lives, perhaps some more physics. But generally, these are just extensions to the framework above: manipulation and then display of additional state.
+ +The key thing is that the observableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming. + has allowed us to keep well separated state management (model), its input and manipulation (control) and the visuals (view). Further extensions are just additions within each of these elements—and doing so should not add greatly to the complexity.
+ +I invite you to click through on the animations above, to the live code editor where you can extend or refine the framework I’ve started.
+ ++ + + 10 + + min read
+Either
type handles values with two possibilities, typically used for error handling and success cases.Functor
, Applicative
, and Monad
type classesA way in Haskell to associate functions with types, similar to TypeScript interfaces. They define a set of functions that must be available for instances of those type classes.
+ to the Either
type, learning how to implement instances for each.do
blocks in simplifying code and handling complex workflows.In Haskell, the Either
type is used to represent values with two possibilities: a value of type Either a b
is either Left a
or Right b
. By convention, Left
is used to hold an error or exceptional value, while Right
is used to hold a correct or expected value. This is particularly useful for error handling.
data Either a b = Left a | Right b
+
In Haskell’s Either
type, convention (and the official documentation) says errors go on the Left
and successes on the Right
. Why? Because if it is not right (correct) it must be left. This can be considered another example of bias against the left-handed people around the world, but alas, it is a cruel world.
The Left
/Right
convention is also more general then a Success
/Error
naming, as Left
does not always need to be an error, but it is the most common usage.
We can use Either
to help us with error catching, similar to a Maybe
type. However, since the error case, has a value, rather than Nothing
, allowing to store an error message to give information to the programmer/user.
divide :: Double -> Double -> Either String Double
+divide _ 0 = Left "Division by zero error"
+divide x y = Right (x / y)
+
Similar to Maybe
s, we can also use pattern matchingA mechanism in functional programming languages to check a value against a pattern and to deconstruct data.
+ against Either
s in a function.
handleResult :: Either String Double -> String
+handleResult (Left err) = "Error: " ++ err
+handleResult (Right val) = "Success: " ++ show val
+
The Either
type constructor has the kind * -> * -> *
. This means that Either
takes two type parametersA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+ and returns a concrete type.
Either String Int
is a concrete type. It has the kind *
because both type parametersA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+ (String
and Int
) are concrete types.
In Haskell, types are classified into different kinds. A kind can be thought of as a type of a type, describing the number of type parametersA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+ a type constructor takes and how they are applied. The Either
type has an interesting kind, which we’ll explore in detail.
Before diving into the Either
typeclass, let’s briefly recap what kinds are:
*
(pronounced “star”) represents the kind of all concrete types. For example, Int
and Bool
have the kind *
.
+* -> *
represents the kind of type constructors that take one type parameterA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+ and return a concrete type. For example, Maybe
and []
(the list type constructor) have the kind * -> *
.
+* -> * -> *
represents the kind of type constructors that take two type parametersA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+ and return a concrete type. For example, Either
and (,)
(the tuple type constructor) have the kind * -> * -> *
.
The Functor
type classA type system construct in Haskell that defines a set of functions that can be applied to different types, allowing for polymorphic functions.
+ expects a type of kind * -> *
. For Either
, this means partially applying the first type parameterA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+, e.g., instance Functor (Either a)
, where a
will be the type of the Left
.
We can then define, fmap
over either, considering Left
as the error case, and applying the function, when we have a correct (Right
) case.
instance Functor (Either a) where
+ fmap _ (Left x) = Left x
+ fmap f (Right y) = Right (f y)
+
An example of using this will be:
+ +fmap (+1) (Right 2) -- Result: Right 3
+(+1) <$> (Right 2) -- or using infix <$>
+fmap (+1) (Left "Error") -- Result: Left "Error"
+
The ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>). + type classA type system construct in Haskell that defines a set of functions that can be applied to different types, allowing for polymorphic functions. + allows for function application lifted over wrapped values.
+ +In this instance, pure
wraps a value in Right
, and <*>
applies the function inside a Right
to another Right
, propagating Left
values unchanged.
instance Applicative (Either a) where
+ pure = Right
+ Left x <*> _ = Left x
+ Right f <*> r = fmap f r
+
pure (+1) <*> Right 2 -- Result: Right 3
+pure (+1) <*> Left "Error" -- Result: Left "Error"
+Right (+1) <*> Right 2 -- Result: Right 3
+Right (+1) <*> Left "Error" -- Result: Left "Error"
+Left "Error" <*> Right 2 -- Result: Left "Error"
+
The MonadA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context. + type classA type system construct in Haskell that defines a set of functions that can be applied to different types, allowing for polymorphic functions. + allows for chaining operations that produce wrapped values.
+ +This involves defining the methods return
(which should be identical to pure
) and >>=
(bindthe defining function which all monads must implement.
+).
instance Monad (Either a) where
+ return = Right
+ (>>=) (Left x) _ = Left x
+ (>>=) (Right y) f = f y
+
Right 3 >>= (\x -> Right (x + 1)) -- Result: Right 4
+Left "Error" >>= (\x -> Right (x + 1)) -- Result: Left "Error"
+Right 3 >>= (\x -> Left "Something went wrong") -- Result: Left "Something went wrong"
+
First, we’ll define custom error types to represent possible failures at each stage.
+ +data FileError = FileNotFound | FileReadError deriving (Show)
+data ReadError = ReadError String deriving (Show)
+data TransformError = TransformError String deriving (Show)
+
Define a function to read data from a file. If reading succeeds, it returns a Right
with the file contents, otherwise, it returns a Left
with a FileError
.
import System.IO (readFile)
+import Control.Exception (catch, IOException)
+
+readFileSafe :: FilePath -> IO (Either FileError String)
+-- catch any IOException, and use `handleError` on IOException
+readFileSafe path = catch (Right <$> (readFile path)) handleError
+ where
+ handleError :: IOException -> IO (Either FileError String)
+ handleError _ = return $ Left FileReadError
+
Define a function to split the file content in to separate lines, if it exists. It returns a Right
with the read data or a Left
with a ReadError
.
readData :: String -> Either ReadError [String]
+readData content
+ | null content = Left $ ReadError "Empty file content"
+ | otherwise = Right $ lines content
+
+
Define a function to transform the read data. It returns a Right
with transformed data or a Left
with a TransformError
.
transformData :: [String] -> Either TransformError [String]
+transformData lines
+ | null lines = Left $ TransformError "No lines to transform"
+ -- Simple transformation, where, we reverse each line.
+ | otherwise = Right $ map reverse lines
+
The outer do
block, is using the IO
monadA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context.
+, while the inner do
block is using the Either
monadA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context.
+. This code looks very much like imperativeImperative programs are a sequence of statements that change a programs state. This is probably the dominant paradigm for programming languages today. Languages from Assembler to Python are built around this concept and most modern languages still allow you to program in this style.
+ code, using the power of monadA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context.
+ to allow for sequencing of operations. However, this is powerful, as it will allow the Left
error to be threaded through the monadic do
block, with the user not needing to handle the threading of the error state.
main :: IO ()
+main = do
+ -- Attempt to read the file
+ fileResult <- readFileSafe "example.txt"
+
+ let result = do
+ -- Use monad instance to compute sequential operations
+ content <- fileResult
+ readData <- readData content
+ transformData readdData
+ print result
+
Functor: A type class in Haskell that represents types that can be mapped over. Instances of Functor must define the fmap function, which applies a function to every element in a structure.
+ +Applicative: A type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure
and (<*>
).
Monad: A type class in Haskell that represents computations as a series of steps. It provides the bind operation (>>=
) to chain operations and the return (or pure
) function to inject values into the monadic context.
+ + + 5 + + min read
+This page will support the workshop solutions with a worked example of how we can use the observables, filled with pretty animations
+ +Consider, the definitions for ranks, suits and card, as per the workshop:
+ +
+const ranks = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'] as const;
+const suits = ['♠','♣','♦','♥'] as const;
+
+// use typeof to define string literal types
+type Rank = typeof ranks[number];
+type Suit = typeof suits[number];
+
+type Card = Readonly<{
+ readonly suit: Suit;
+ readonly rank: Rank;
+}>;
+
+const suits$ = from(suits)
+const ranks$ = from(ranks)
+
Using the webtool, we can visualize each of these streams.
+ +
+
To create a card, for each suit, we can look through each rank, and create a string of suits and rank.
+ +const deck = suits$.pipe(
+ map(suit => rank$.pipe(
+ map(rank => (`${suit}${rank}`)))))
+
However, this exists as four nested streams rather than one continuous flat stream. How do we fix this? We use mergeMap to merge the sub-streams into a single long continuous stream of cards. We now have a lovely little deck of cards :)
+ +const deck = suits$.pipe(
+ mergeMap(suit => rank$.pipe(
+ map(rank => (`${suit}${rank}`)))))
+
However, this is only one deck? How can we create multiple decks. We will create a range, which will create a fixed range of numbers, and for each of those we can create a deck.
+ +const decks = (numDecks : number) => range(0, numDecks).pipe(map(_ => deck))
+
But this poses a similar problem to the above issue with nested streams. So again, we use the power of mergeMap to flatten these streams in to one!
+ +const decks = (numDecks : number) => range(0, numDecks).pipe(mergeMap(_ => deck))
+
All in order, oh no, let us shuffle them. Assuming we have these functions, which can insert an element in to a random position in an array. We will use the reduce, and the randomInsertion to shuffle them.
+ +function impureRandomNumberGenerator(n:number) {
+ return Math.floor(Math.random() * (n+1)); // impure!!
+}
+
+function randomInsert<T>(a:readonly T[],e:T): readonly T[] {
+ return (i=>[...a.slice(0,i),e,...a.slice(i)])
+ (impureRandomNumberGenerator(a.length + 1))
+}
+const shoe = (numDecks : number) => range(0, numDecks).pipe(
+ mergeMap(_ => deck),
+ reduce(randomInsert, [])
+)
+
This should be correct? Not quite, we reduce
to a single value, an array. So, now our stream contains a single element, an array, Rather, then being a stream of elements. This array will be all of our cards, shuffled. You can see that as we hover over the element and it attempts to print the contents.
We need to turn this back into a stream. How can we do that, with the power of mergeMap! This will take our list and convert it to a stream, and then flatten it, such that our final result is a long stream of shuffled cards.
+ +const shuffledShoe = (numDecks : number) => range(0, numDecks).pipe(
+ mergeMap(_ => deck),
+ reduce(randomInsert, []),
+ mergeMap(from)
+)
+
Wow, now we have a beautiful, shiny, shuffled shoe of cards in an Observable!
+ ++ + + 41 + + min read
+The elements of JavaScript covered in our introduction, specifically:
+ +are sufficient for us to explore a paradigm called functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + programming. In the functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + programming paradigm the primary model of computation is through the evaluation of functions. FunctionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + Programming is highly inspired by the Lambda CalculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s. +, a theory which develops a model for computation based on application and evaluation of mathematical functions.
+ +While JavaScript (and many—but not all, as we shall see—other languages inspired by the functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + paradigm) do not enforce it, true functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + programming mandates the functions be pure in the sense of not causing side effectsAny state change that occurs outside of a function’s local environment or any observable interaction with the outside world, such as modifying a global variable, writing to a file, or printing to a console. +.
+ +Side effectsAny state change that occurs outside of a function’s local environment or any observable interaction with the outside world, such as modifying a global variable, writing to a file, or printing to a console. + of a function are changes to state outside of the result explicitly returned by the function.
+ +Examples of side-effects from inside a function:
+ +In languages without compilers that specifically guard against them, side effectsAny state change that occurs outside of a function’s local environment or any observable interaction with the outside world, such as modifying a global variable, writing to a file, or printing to a console. + can occur:
+ +We’ll see more examples in actual code below.
+ +A pure functionA function that always produces the same output for the same input and has no side effects. +:
+ +In the context of functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + programming, referential transparencyAn expression that can be replaced with its value without changing the program’s behavior, indicating no side effects and consistent results. + is:
+ +Imagine a simple function:
+ +const square = x=>x*x
+
And some code that uses it:
+ +const four = square(2)
+
The expression square(2)
evaluates to 4
. Can we replace the expression square(2)
with the value 4
in the program above without changing the behaviour of the program? YES! So is the expression square(2)
referentially transparent? YES!
But what if the square
function depends on some other data stored somewhere in memory and may return a different result if that data mutates? (an example might be using a global variable or reading an environment variable from IO or requiring user input). What if square
performs some other action instead of simply returning the result of a mathematical computation? What if it sends a message to the console or mutates a global variable? That is, what if it has side effectsAny state change that occurs outside of a function’s local environment or any observable interaction with the outside world, such as modifying a global variable, writing to a file, or printing to a console.
+ (is not a pure functionA function that always produces the same output for the same input and has no side effects.
+)? In that case is replacing square(2)
with the value 4
still going to leave our program behaving the same way? Possibly not!
Put another way, if the square
function is not pure—i.e. it produces side effectsAny state change that occurs outside of a function’s local environment or any observable interaction with the outside world, such as modifying a global variable, writing to a file, or printing to a console.
+ (meaning it has hidden output other than its return value) or it is affected by side effectsAny state change that occurs outside of a function’s local environment or any observable interaction with the outside world, such as modifying a global variable, writing to a file, or printing to a console.
+ from other code (meaning it has hidden inputs)—then the expression square(2)
would not be referentially transparent.
A real life example where this might be useful would be when a cached result for a given input already exists and can be returned without further computation. Imagine a pure functionA function that always produces the same output for the same input and has no side effects.
+ which computes PI to a specified number of decimal points precision. It takes an argument n
, the number of decimal points, and returns the computed value. We could modify the function to instantly return a precomputed value for PI from a lookup table if n
is less than 10
. This substitution is trivial and is guaranteed not to break our program, because its effects are strictly locally contained.
Pure functionsA function that always produces the same output for the same input and has no side effects. + and referential transparencyAn expression that can be replaced with its value without changing the program’s behavior, indicating no side effects and consistent results. + are perhaps most easily illustrated with some examples and counterexamples. +Consider the following impure function:
+ + function impureSquares(a) {
+ let i = 0
+ while (i < a.length) {
+* a[i] = a[i] * a[i++];
+ }
+ }
+
Since the function modifies a
in place we get a different outcome if we call it more than once.
const myArray=[1,2,3]
+impureSquares(myArray)
+// now myArray = [1,4,9]
+impureSquares(myArray)
+// now myArray = [1,16,81]
+
Furthermore, the very imperativeImperative programs are a sequence of statements that change a programs state. This is probably the dominant paradigm for programming languages today. Languages from Assembler to Python are built around this concept and most modern languages still allow you to program in this style.
+ style computation in impureSquares
at the line marked with *
is not pure.
+It has two effects: incrementing i
and mutating a
.
+You could not simply replace the expression with the value computed by the expression and have the program work in the same way.
+This piece of code does not have the property of referential transparencyAn expression that can be replaced with its value without changing the program’s behavior, indicating no side effects and consistent results.
+.
True pure functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + languages (such as Haskell) enforce referential transparencyAn expression that can be replaced with its value without changing the program’s behavior, indicating no side effects and consistent results. + through immutable variablesA variable declared with const whose value cannot be reassigned. + (note: yes, “immutable variableA variable declared with const whose value cannot be reassigned. +” sounds like an oxymoron—two words with opposite meanings put together). That is, once any variable in such a language is bound to a value, it cannot be reassigned.
+ +In JavaScript we can opt-in to immutable variablesA variable declared with const whose value cannot be reassigned.
+ by declaring them const
, but it is only a shallow immutability. Thus, the variable myArray
above cannot be reassigned to reference a different array. However, we can change the contents of the array as shown above.
A more functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus.
+ way to implement the squares
function would be more like the examples we have seen previously:
function squares(a) {
+ return a.map(x=> x*x)
+}
+
The above function is pure. Its result is a new array containing the squares of the input array and the input array itself is unchanged. It has no side effectsAny state change that occurs outside of a function’s local environment or any observable interaction with the outside world, such as modifying a global variable, writing to a file, or printing to a console. + changing values of variables, memory or the world, outside of its own scope. You could replace the computation of the result with a different calculation returning the same result for a given input and the program would be unchanged. It is referentially transparent.
+ +const myArray = [1,2,3]
+const mySquares = squares(myArray)
+const myRaisedToTheFours = squares(mySquares)
+
We could make the following substitution in the last line with no unintended consequences:
+ + const myRaisedToTheFours = squares(squares(myArray))
+
An impure function typical of something you may see in OO code:
+ +let messagesSent = 0;
+function send(message, recipient) {
+ let success = recipient.notify(message);
+ if (success) {
+ ++messagesSent;
+ } else {
+ console.log("send failed! " + message);
+ }
+ console.log("messages sent " + messagesSent);
+}
+
This function is impure in three ways:
+ +Side effectsAny state change that occurs outside of a function’s local environment or any observable interaction with the outside world, such as modifying a global variable, writing to a file, or printing to a console. + are bad for transparency (knowing everything about what a function is going to do) and maintainability. When state in your program is being changed from all over the place bugs become very difficult to track down.
+ +Do the following functions have side effectsAny state change that occurs outside of a function’s local environment or any observable interaction with the outside world, such as modifying a global variable, writing to a file, or printing to a console. +?
+ +Yes/No and why?
+ + let counter = 0;
+
+ function incrementCounter() {
+ counter++;
+ }
+
Yes/No and why?
+ +function greet(name) {
+ return `Hello, ${name}!`;
+}
+
Yes/No and why?
+ +function multiplyArray(numbers) {
+ for (let i = 0; i < numbers.length; i++) {
+ numbers[i] = numbers[i] * 2;
+ }
+ return numbers
+}
+
Yes/No and why?
+ +function logMessage(message) {
+ console.log(message);
+}
+
Yes/No and why?
+ +function doubleNumbers(numbers) {
+ return numbers.map(x => x * 2);
+}
+
Yes/No and why?
+ +function getRandomNumber() {
+ return Math.random();
+}
+
Yes, it modifies the global variable counter.
+No, it simply returns a new string and does not modify any external state.
+Yes, it modifies the input array numbers in place.
+Yes, it writes to the console, which is an external action.
+No, it returns a new array without modifying the input array.
+Yes, it relies on and modifies a global seed for random number generation.
+Passing functions around, anonymous or not, is incredibly useful and pops up in many practical programming situations.
+ +Loops are the source of many bugs: fence-post errors, range errors, typos, incrementing the wrong counter, etc.
+ +A typical for loop has four distinct places where it’s easy to make errors that can cause critical problems:
+ +for ([initialization]; [condition]; [final-expression])
+ statement
+
For many standard loops, however, the logic is the same every time and can easily be abstracted into a function. Examples: Array.map, Array.reduce, Array.forEach, etc. The logic of the loop body is specified with a function which can execute in its own scope, without the risk of breaking the loop logic.
+ +Consider this loop to multiply each item in the someArray
by two. Try to find the mistake in this code:
const someArray = [1,2,3,4,5];
+ let newArray = [];
+ for (let i = 0; i <= someArray.length; i++) {
+ newArray.push(someArray[i] * 2);
+ }
+
The condition should be <
, not <=
.
To avoid the likelihood of errors, we can replace the for
loop with the use of .map
.
We use the map
function since we want to apply a function to every element in the list.
const someArray = [1,2,3,4,5];
+ const newArray = someArray.map(x => x * 2);
+
We use an arrow function here to allow our function definition to be short and to the point!
+Consider this code which aims to compute the product of a list. Try to find the mistake in this code:
+ + const someArray = [1,2,3,4,5];
+ let result = 1;
+ for (let i = 0; i < someArray.length; i++) {
+ result *= i;
+ }
+
We should multiply by someArray[i]
, not i
.
Again, to avoid the likelihood of errors, we can replace the for
loop with the use of .reduce
.
We use the reduce
function since we want to reduce the list to a singular value.
const someArray = [1,2,3,4,5];
+ const result = someArray.reduce((acc, el) => el * acc, 1);
+
Refactor this code to use map
and filter
instead of a loop:
const numbers = [2, 6, 3, 7, 10];
+ const result = [];
+ for (let i = 0; i < numbers.length; i++) {
+ if (numbers[i] % 2 === 0) {
+ result.push(numbers[i] / 2);
+ }
+ }
+
Refactor this code to remove the loop:
+ + const words = ["apple", "banana", "cherry"];
+ let totalLength = 0;
+ for (let i = 0; i < words.length; i++) {
+ totalLength += words[i].length;
+ }
+
Refactor this code to remove the loop:
+ + const people = [
+ {name: "Alice", age: 20},
+ {name: "Bob", age: 15},
+ {name: "Charlie", age: 30},
+ {name: "David", age: 10},
+ ];
+ let firstChildName = undefined;
+ for (let i = 0; i < people.length; i++) {
+ if (people[i].age < 18) {
+ firstChildName = people[i].name;
+ break;
+ }
+ }
+
Refactor this code to remove the loop:
+ + const people = [
+ {name: "Alice", age: 20},
+ {name: "Bob", age: 15},
+ {name: "Charlie", age: 30},
+ {name: "David", age: 10},
+ ];
+ let result = "";
+ for (let i = 0; i < people.length; i++) {
+ result += `Person #${i + 1}: ${people[i].name} is ${people[i].age} years old`;
+ if (i != people.length - 1) {
+ result += "\n";
+ }
+ }
+
Identify what is wrong with this code and correct it:
+ + // Don’t worry about this line
+ const value = document.getElementById('some-input-element').value.toLowerCase();
+
+ const filteredNames = [];
+
+ roleNames.forEach(role => {
+ if (role.slice(0, value.length).toLowerCase() === value)
+ filteredNames.push(role);
+ });
+
We can use filter
to get the even numbers, then map
to divide each of those numbers by 2:
const numbers = [2, 6, 3, 7, 10];
+ const result = numbers.filter(x => x % 2 === 0).map(x => x / 2);
+
We need to sum up the lengths of each word. One way to do that would be to use reduce
:
const words = ["apple", "banana", "cherry"];
+ const totalLength = words.reduce((acc, x) => acc + x.length, 0);
+
Here, we initialise the accumulator to 0, and then add the length of the string x
tot the accumulator for each string in the words
array.
We want to get the name of the first person under 18 years old. We can get the person with the find
method and then access the name
property if the person exists:
const people = [
+ {name: "Alice", age: 20},
+ {name: "Bob", age: 15},
+ {name: "Charlie", age: 30},
+ {name: "David", age: 10},
+ ];
+ const firstChild = people.find(x => x.age < 18);
+ const firstChildName = firstChild !== undefined ? firstChild.name : undefined;
+
We could also do the last part more succinctly using optional chaining (?.
):
const firstChildName = people.find(x => x.age < 18)?.name;
+
We can use the second parameter in the function passed to map
to get the index of each element, and then combine the resulting array of strings into one string with join
:
const people = [
+ {name: "Alice", age: 20},
+ {name: "Bob", age: 15},
+ {name: "Charlie", age: 30},
+ {name: "David", age: 10},
+ ];
+ const result = people
+ .map((x, i) => `Person #${i + 1}: ${x.name} is ${x.age} years old`)
+ .join("\n");
+
We should use the filter
method to get a new array with only the roles that match value
:
// Don’t worry about this line
+ const value = document.getElementById('some-input-element').value.toLowerCase();
+
+ const filteredNames = roleNames
+ .filter(role => role.slice(0, value.length).toLowerCase() === value);
+
We should reuse existing methods and functions wherever possible, and filter
does exactly what we want to do without having to manually and impurely push
each matching element into a new array.
Generally, you should only use forEach
when you don’t care about the result and thus want to do something that will cause a side effect, such as printing each element (e.g. roleNames.forEach(role => console.log(role))
). While the function passed to forEach
in the original code does technically cause a side effect (appending to the filteredNames
array), the overall effect of the lines below
const filteredNames = [];
+
+ roleNames.forEach(role => {
+ if (role.slice(0, value.length).toLowerCase() === value)
+ filteredNames.push(role);
+ });
+
is to simply create a new array, which can be done purely with filter
.
In JavaScript and HTML5 events trigger actions associated with all mouse clicks and other interactions with the page. You subscribe to an event on a given HTMLHyper-Text Markup Language - the declarative language for specifying web page content. + element as follows:
+ +element.addEventHandler('click',
+e=>{
+// do something when the event occurs,
+// maybe using the result of the event e
+});
+
Note that callback functions passed as event handlers are a situation where the one semantic difference between the arrow syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language.
+ and regular anonymous functionA function defined without a name, often used as an argument to other functions. Also known as lambda function.
+ syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language.
+ really matters. In the body of the arrow function above, this
will be bound to the context of the caller, which is probably what you want if you are coding a class for a reusable component. For functions defined with the function
keyword the object that this
refers to will depend on the context of the callee, although the precise behaviour of this
for functions defined with function
may vary with the particular JavaScript engine and the mode of execution.
Here’s a situation where this makes a difference. Recall that JavaScript functions are just objects. Therefore, we can also assign properties to them. We can do this from within the function itself using the this
keyword:
function Counter() {
+ this.count = 0;
+ setInterval(function increment() {
+ console.log(this.count++)
+ }, 500);
+}
+const ctr = new Counter();
+
But, if I run this program at a console, I get the following, each line emitted 500 milliseconds apart:
+ +++ +NaN
+
+NaN
+NaN
+…
This occurs because the this
inside the function passed to setInterval
is referring to the first function enclosing its scope, i.e. the increment
function. Since increment
has no count property, we are trying to apply ++
to undefined
and the result is NaN
(Not a Number).
Arrow functions have different scoping rules for this
. That is, they take the this
of the enclosing scope (outside the arrow function), so in the following we get the expected behaviour:
function Counter() {
+ this.count = 0;
+ setInterval(()=>console.log(this.count++), 500);
+}
+new Counter();
+
++ +0
+
+1
+2
+…
Continuations are functions which, instead of returning the result of a computation directly to the caller, pass the result on to another function, specified by the caller.
+We can rewrite basically any function to pass their result to a user-specified continuation function instead of returning the result directly. The parameter done
in the continuationPlus
function below will be a function specified by the caller to do something with the result.
function simplePlus(a, b) {
+ return a + b;
+}
+function continuationPlus(a, b, done) {
+ done(a+b);
+}
+
An example of using this to log the result:
+ +continuationPlus(3, 5, console.log)
+
This will output “8” to the console.
+ +We can also rewrite tail-recursive functions to end with continuations, which specify some custom action to perform when the recursion is complete:
+ +Consider a tail recursive implementation of factorial
+ +function tailRecFactorial(a, n) {
+ return n<=1 ? a : tailRecFactorial(n*a, n-1);
+}
+
The function tailRecFactorial
is tail recursive because the final operation in the function is the recursive call to itself, with no additional computation after this call. We can convert this function in to a continuation version, by adding an extra parameter finalAction
function continuationFactorial(
+a, n, finalAction=(result)=>{})
+{
+ if (n<=1) finalAction(a);
+ else continuationFactorial(n*a, n-1, finalAction);
+}
+
The continuationFactorial
function uses a continuation by passing a finalAction
callback that gets called with the result a when the recursion reaches the base case (n <= 1)
, allowing further actions to be specified and executed upon completion.
Continuations are essential in asynchronous processing, which abounds in web programming. For example, when an HTTP request is dispatched by a client to a server, there is no knowing precisely when the response will be returned (it depends on the speed of the server, the network between client and server, and load on that network). However, we can be sure that it will not be instant and certainly not before the line of code following the dispatch is executed by the interpreter. Thus, continuation style call-back functions are typically passed through to functions which trigger such asynchronous behaviour, for those call-back functions to be invoked when the action is completed. A simple example of an asynchronous function invocation is the built-in setTimeout
function, which schedules an action to occur after a certain delay. The setTimeout
function itself returns immediately after dispatching the job, e.g. to the JavaScript event loop:
setTimeout(()=>console.log('done.'), 0);
+// the above tells the event loop to execute
+// the continuation after 0 milliseconds delay.
+// even with a zero-length delay, the synchronous code
+// after the setTimeout will be run first…
+console.log('job queued on the event loop…');
+
++ +job queued on the event loop…
+
+done.
Chained functions are a common pattern.
+Take a simple linked-list data structure as an example. We’ll hard code a list object to start off with:
const l = {
+ data: 1,
+ next: {
+ data: 2,
+ next: {
+ data: 3,
+ next: null
+ }
+ }
+};
+
We can create simple functions similar to those of Array, which we can chain:
+ +const
+ map = (f,l) => l ? ({data: f(l.data), next: map(f,l.next)}) : null
+, filter = (f,l) => !l ? null :
+ (next =>
+ f(l.data) ? ({data: l.data, next})
+ : next
+ ) (filter(f,l.next))
+ // the above is using an immediately invoked
+ // function expression (IIFE) such that the function
+ // parameter `next` is used like a local variable for
+ // filter(f,l.next)
+, take = (n,l) => l && n ? ({data: l.data, next: take(n-1,l.next)})
+ : null;
+
(An IIFE is an immediately invoked function expression.)
+ +We can chain calls to these functions like so:
+ +take(2,
+ filter(x=> x%2 === 0,
+ map(x=> x+1, l)
+ )
+)
+
++ +{ data: 2, next: { data: 4, next: null }}
+
The definition of map
(and friends) may look scary, but lets break it down. We will write it in a more verbose way for now:
function map(func, list) {
+ if (list !== null) {
+ return {
+ data: func(list.data),
+ next: map(func, list.next)
+ };
+ }
+ else {
+ return null;
+ }
+}
+
The map function will recursively apply the given func
to each element of the linked list list
, constructing and returning a new linked list where each element is the result of applying func
to the corresponding data element in the original list.
Try to expand and step through the filter
and take
and see if you can understand how they work.
In the chained function calls above you have to read them inside-out to understand the flow. Also, keeping track of how many brackets to close gets a bit annoying. Thus, in object oriented languages you will often see class definitions that allow for method chaining, by providing methods that return an instance of the class itself, or another chainable class.
+ +class List {
+ constructor(private head) {}
+ map(f) {
+ return new List(map(f, this.head));
+ }
+ ...
+
Then the same flow as above is possible without the nesting and can be read left-to-right, top-to-bottom:
+ +new List(l)
+ .map(x=>x+1)
+ .filter(x=>x%2===0)
+ .take(2)
+
This is called fluent programming style. +Interfaces in object-orientedObject-oriented languages are built around the concept of objects where an objects captures the set of data (state) and behaviours (methods) associated with entities in the system. + languages that chain a sequence of method calls (as above) are often called fluent interfaces. One thing to be careful about fluent interfaces in languages that do not enforce purity is that the methods may or may not be pure.
+ +That is, the type system does not warn you whether the method mutates the object upon which it is invoked and simply returns this
, or creates a new object, leaving the original object untouched. We can see, however, that List.map
as defined above creates a new list and is pure.
Pure functionsA function that always produces the same output for the same input and has no side effects. + may seem restrictive, but in fact pure functionA function that always produces the same output for the same input and has no side effects. + expressions and higher-order functionsA function that takes other functions as arguments or returns a function as its result. + can be combined into powerful programs. In fact, anything you can compute with an imperativeImperative programs are a sequence of statements that change a programs state. This is probably the dominant paradigm for programming languages today. Languages from Assembler to Python are built around this concept and most modern languages still allow you to program in this style. + program can be computed through function composition. Side effectsAny state change that occurs outside of a function’s local environment or any observable interaction with the outside world, such as modifying a global variable, writing to a file, or printing to a console. + are required eventually, but they can be managed and the places they occur can be isolated. Let’s do a little demonstration; although it might be a bit impractical, we’ll make a little list processing environment with just functions:
+ +const cons = (_head, _rest)=> selector=> selector(_head, _rest);
+
With just the above definition we can construct a list (the term cons dates back to LISP) with three elements, terminated with null, like so:
+ +const list123 = cons(1, cons(2, cons(3, null)));
+
The data element, and the reference to the next node in the list are stored in the closureA function and the set of variables it accesses from its enclosing scope.
+ returned by the cons
function. Created like this, the only side-effect of growing the list is creation of new cons closuresA function and the set of variables it accesses from its enclosing scope.
+. Mutation of more complex structures such as trees can be managed in a similarly ‘pure’ way, and surprisingly efficiently, as we will see later in this course.
So cons
is a function that takes two parameters _head
and _rest
(the _
prefix is just to differentiate them from the functions I create below), and returns a function that itself takes a function (selector) as argument. The selector function is then applied to _head
and _rest
.
The selector
function that we pass to the list is our ticket to accessing its elements:
list123((_head, _rest)=> _head)
+
++ +1
+
list123((_,r)=>r)((h,_)=>h) // we can call the parameters whatever we like
+
++ +2
+
We can create accessor functions to operate on a given list (by passing the list the appropriate selector function):
+ +const
+ head = list=> list((h,_)=>h),
+ rest = list=> list((_,r)=>r)
+
Now, head
gives us the first data element from the list, and rest
gives us another list. Now we can access things in the list like so:
const one = head(list123), // ===1
+ list23 = rest(list123),
+ two = head(list23), // ===2
+ ... // and so on
+
Now, here’s the ubiquitous map function:
+ +const map = (f, list)=> !list ? null
+ : cons(f(head(list)), map(f, rest(list)));
+
We can now apply our map function to list123
to perform some transformation of the data
const list234 = map(x => x + 1, list123);
+
++ ++
cons(2, cons(3, cons(4, null)));
In the above, we are using closuresA function and the set of variables it accesses from its enclosing scope. + to store data. It’s just a trick to show the power of functions and to put us into the right state of mind for the Lambda CalculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s. +—which provides a complete model of computation using only anonymous functionsA function defined without a name, often used as an argument to other functions. Also known as lambda function. + like those above. In a real program I would expect you would use JavaScript’s class and object facilities to create data structures.
+ +Thus, with only pure functionA function that always produces the same output for the same input and has no side effects.
+ expressions and JavaScript conditional expressions (?:
) we can begin to perform complex computations. We can actually go further and eliminate the conditional expressions with more functions! Here’s the gist of it: we wrap list nodes with another function of two arguments, one argument, whenempty
, is a function to apply when the list is empty, the other argument, notempty
, is applied by all internal nodes in the list. An empty list node (instead of null) applies the whenempty
function when visited, a non-empty node applies the notempty
function. The implementations of each of these functions then form the two conditions to be handled by a recursive algorithm like map
or reduce
. See “Making Data out of Functions” by Braithwaite for a more detailed exposition of this idea.
These ideas, of computation through pure functionA function that always produces the same output for the same input and has no side effects. + expressions, are inspired by Alonzo Church’s lambda calculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s. +. We’ll be looking again at the lambda calculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s. + later. Obviously, for the program to be at all useful you will need some sort of side effect, such as outputting the results of a computation to a display device. When we begin to explore PureScript and Haskell later in this course we will discuss how such languages manage this trick while remaining “pure”.
+ +fromArray
function to construct a cons
list from an arrayfilter
function, which takes a function and a cons list, and returns another cons list populated only with those elements of the list for which the function returns truereduce
function for these cons lists, similar to javascript’s Array.reduc
ereduceRight
function for these cons lists, similar to javascript’s Array.reduceRigh
tconcat
function that takes two lists as arguments and returns a new list of their concatenation.The solutions for this exercise will be discussed in class.
+ +We saw in the introduction to JavaScript that one can create objects with a straightforward chunk of JSON:
+ +const studentVersion1 = {
+ name: "Tim",
+ assignmentMark: 20,
+ examMark: 15
+};
+
++ +studentVersion1
+
+{name: “Tim”, assignmentMark: 20, examMark: 15}
Conveniently, one can copy all of the properties from an existing object into a new object using the “spread” operator ...
, followed by more JSON properties that can potentially overwrite those of the original. For example, the following creates a new object with all the properties of the first, but with a different assignmentMark:
const studentVersion2 = {
+ ...studentVersion1,
+ assignmentMark: 19
+};
+
++ +studentVersion2
+
+{name: “Tim”, assignmentMark: 19, examMark: 15}
One can encapsulate such updates in a succinct pure functionA function that always produces the same output for the same input and has no side effects. +:
+ +function updateExamMark(student, newMark) {
+ return {...student, examMark: newMark};
+}
+
+const studentVersion3 = updateExamMark(studentVersion2, 19);
+
++ +studentVersion3
+
+{name: “Tim”, assignmentMark: 19, examMark: 19}
Note that when we declared each of the variables studentVersion1-3
as const
, these variables are only constant in the sense that the object reference cannot be changed. That is, they cannot be reassigned to refer to different objects:
studentVersion1 = studentVersion2;
+
++ +VM430:1 Uncaught TypeError: Assignment to constant variable.
+
However, there is nothing in these definitions to prevent the properties of those objects from being changed:
+ +studentVersion1.name = "Tom";
+
++ +studentVersion1
+
+{name: “Tom”, assignmentMark: 20, examMark: 15}
We will see later how the TypeScript compiler allows us to create deeply immutable objects that will trigger compile errors if we try to change their properties.
+ +You may wonder how pure functionsA function that always produces the same output for the same input and has no side effects. + can be efficient if the only way to mutate data structures is by returning a modified copy of the original. There are two responses to such a question, one is: “purity helps us avoid errors in state management through wanton mutation effects—in modern programming correctness is often a bigger concern than efficiency”, the other is “properly structured data permits log(n) time copy-updates, which should be good enough for most purposes”. We’ll explore what is meant by the latter in later sections of these notes.
+ +Callback: A function passed as an argument to another function, to be executed after some event or action has occurred.
+ +Chained Functions: A programming pattern where multiple function calls are made sequentially, with each function returning an object that allows the next function to be called.
+ +Continuation: A function that takes a result and performs some action with it, instead of returning the result directly. Used extensively in asynchronous programming.
+ +Fluent Interface: A method chaining pattern where a sequence of method calls is made on the same object, with each method returning the object itself or another chainable object.
+ +Pure FunctionA function that always produces the same output for the same input and has no side effects. +: A function that always produces the same output for the same input and has no side effectsAny state change that occurs outside of a function’s local environment or any observable interaction with the outside world, such as modifying a global variable, writing to a file, or printing to a console. +.
+ +Referential TransparencyAn expression that can be replaced with its value without changing the program’s behavior, indicating no side effects and consistent results. +: An expression that can be replaced with its value without changing the program’s behavior, indicating no side effectsAny state change that occurs outside of a function’s local environment or any observable interaction with the outside world, such as modifying a global variable, writing to a file, or printing to a console. + and consistent results.
+ +Side EffectsAny state change that occurs outside of a function’s local environment or any observable interaction with the outside world, such as modifying a global variable, writing to a file, or printing to a console. +: Any state change that occurs outside of a function’s local environment or any observable interaction with the outside world, such as modifying a global variable, writing to a file, or printing to a console.
+ ++ + + 32 + + min read
+FunctionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + Reactive Programming describes an approach to modelling complex, asynchronousOperations that occur independently of the main program flow, allowing the program to continue executing while waiting for the operation to complete. + behaviours that uses many of the functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + programming principles we have already explored. In particular:
+ +We will explore FRP through an implementation of the ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming. + data structure in the Reactive Extensions for JavaScript (RxJS) library. We will then see it applied in application to a straight-forward browser-based user interfaceA TypeScript construct that defines the shape of an object, specifying the types of its properties and methods. + problem.
+ +To support the code examples, the streams are visualized using rxviz
+ +We have seen a number of different ways of wrapping collections of things in containers: built-in JavaScript arrays, linked-list data structures, and also lazy sequences. Now we’ll see that ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming. + is just another type of container with some simple examples, before demonstrating that it also easily applies to asynchronousOperations that occur independently of the main program flow, allowing the program to continue executing while waiting for the operation to complete. + streams.
+ +You can also play with a live version of this code. Note that the code in this live version begins with a pair of import
statements, bringing the set of functions that we describe below into scope for this file from the rxjs
libraries:
import { of, range, fromEvent, zip, merge } from 'rxjs';
+import { last,filter,scan,map,mergeMap,take,takeUntil } from 'rxjs/operators';
+
Conceptually, the ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ data structure just wraps a collection of things in a container in a similar way to the data structures we have seen before.
+The function of
creates an ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ that will emit the specified elements in its parameter list in order. However, nothing actually happens until we initialise the stream. We do this by “subscribing” to the ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+, passing in an “effectful” function that is applied to each of the elements in the stream. For example, we could print the elements out with console.log
:
of(1,2,3,4)
+ .subscribe(console.log)
+
++ +1
+
+2
+3
+4
The requirement to invoke subscribe
before anything is produced by the ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ is conceptually similar to the lazy sequence, where nothing happened until we started calling next
. But there is also a difference.
+You could think of our lazy sequences as being “pull-based” data structures, because we had to “pull” the values out one at a time by calling the next
function as many times as we wanted elements of the list. ObservablesA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ are a bit different. They are used to handle “streams” of things, such as asynchronousOperations that occur independently of the main program flow, allowing the program to continue executing while waiting for the operation to complete.
+ UI (e.g. mouse clicks on an element of a web page) or communication events (e.g. responses from a web service). These things are asynchronousOperations that occur independently of the main program flow, allowing the program to continue executing while waiting for the operation to complete.
+ in the sense that we do not know when they will occur.
Just as we have done for various data structures (arrays and so on) in previous chapters, we can define a transform over an ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ to create a new ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+. This transformation may have multiple steps the same way that we chained filter
and map
operations over arrays previously. In RxJS’s ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ implementation, however, they’ve gone a little bit more functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus.
+, by insisting that such operations are composed (rather than chained) inside a pipe
. For example, here’s the squares of even numbers in the range [0,10):
const isEven = x=>x%2===0,
+ square = x=>x*x
+range(10)
+ .pipe(
+ filter(isEven),
+ map(square))
+ .subscribe(console.log)
+
++ +0
+
+4
+16
+36
+64
The three animations represent the creation (range
) and the two transformations (filter
and map
), respectively.
We can relate this to similar operations on arrays which we have seen before:
+ +const range = n => Array(n).fill().map((_, i) => i)
+range(10)
+ .filter(isEven)
+ .map(square)
+ .forEach(console.log)
+
To solve the first Project Euler problem using RxJS, we generate a sequence of numbers from 0 to 999 with range(1000)
. We then use the filter
operator to select numbers divisible by 3 or 5. We then use he scan
operator, akin to reduce, accumulates the sum of these filtered numbers over time, and the last
operator emits only the final accumulated sum. Finally, we subscribe to the observableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ and log the result to the console. Here’s the complete code:
range(1000)
+ .pipe(
+ filter(x=> x%3===0 || x%5===0),
+ scan((a,v)=>a+v),
+ last())
+ .subscribe(console.log);
+
In the developer console, only one number will be printed:
+ +++ +233168
+
We can see the values changes as they move further and further down the stream. The four animations represent the creation (range
) and the three transformations (filter
, scan
and last
), respectively. The last
animation is empty, since we only emit the last value, which will be off screen.
Scan is very much like the reduce
function on Array in that it applies an accumulator function to the elements coming through the ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+, except instead of just outputting a single value (as reduce
does), it emits a stream of the running accumulation (in this case, the sum so far). Thus, we use the last
function to produce an ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ with just the final value.
There are also functions for combining ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ streams. The zip
function lets you pair the values from two streams into an array:
const
+ columns = of('A','B','C'),
+ rows = range(3);
+
+zip(columns,rows)
+ .subscribe(console.log)
+
++ +[“A”,0]
+
+[“B”,1]
+[“C”,2]
If you like mathy vector speak, you can think of the above as an inner product of the two streams.
+By contrast, the mergeMap
operator gives the cartesian product of two streams. That is, it gives us a way to take, for every element of a stream, a whole other stream, but flattened (or projected) together with the parent stream. The following enumerates all the row/column indices of cells in a spreadsheet:
columns.pipe(
+ mergeMap(column=>rows.pipe(
+ map(row=>[column, row])
+ ))
+).subscribe(console.log)
+
++ +[“A”, 0]
+
+[“A”, 1]
+[“A”, 2]
+[“B”, 0]
+[“B”, 1]
+[“B”, 2]
+[“C”, 0]
+[“C”, 1]
+[“C”, 2]
If we contrast mergeMap
and map
, map will produce an ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ of ObservablesA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+, while mergeMap, will produce a single stream with all of the values. Contrast the animation for map
, with the previous mergeMap
animation. map
has three separate branches, where each one represents its own observableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ stream. The output of the console.log
, is an instance of the ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ class itself, which is not very useful!
columns.pipe(
+ map(column=>rows.pipe(
+ map(row=>[column, row])
+ ))
+).subscribe(console.log)
+
++ +Observable
+
+Observable +Observable
Another way to combine streams is merge
. Streams that are generated with of
and range
have all their elements available immediately, so the result of a merge is not very interesting, just the elements of one followed by the elements of the other:
merge(columns,rows)
+ .subscribe(console.log)
+
++ +A
+
+B
+C
+0
+1
+2
However, merge
when applied to asynchronousOperations that occur independently of the main program flow, allowing the program to continue executing while waiting for the operation to complete.
+ streams will merge the elements in the order that they arrive in the stream. For example, a stream of key-down and mouse-down events from a web-page:
const
+ key$ = fromEvent<KeyboardEvent>(document,"keydown"),
+ mouse$ = fromEvent<MouseEvent>(document,"mousedown");
+
It’s a convention to end variable names referring to ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ streams with a $
(I like to think it’s short for “$tream”, or implies a plurality of the things in the stream, or maybe it’s just because cash rules everything around me).
We can analogously think of mouse$
as an array of MouseEvent
objects, e.g., [MouseEvent, MouseEvent, MouseEvent, MouseEvent, MouseEvent]
, and then we can perform operations on this array just as we would with a typical array of values. However, rather than being a fixed array of MouseEvent
, they are an ongoing stream of MouseEvent
objects that occur over time. Therefore, instead of being a static collection of events that you can iterate over all at once, the ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ mouse$
represents a dynamic, potentially infinite sequence of events that are emitted as they happen in real-time.
The following lets us see in the console the keys pressed as they come in, it will keep running for as long as the web page is open:
+ +key$.pipe(
+ map(e=>e.key)
+).subscribe(console.log)
+
The animation displays the stream as the user types in the best FIT unit in to the webpage
+ +The following prints “!!” on every mousedown:
+ +mouse$.pipe(
+ map(_=>"!!")
+).subscribe(console.log)
+
The yellow highlight signifies when the mouse is clicked!
+ +Once again this will keep producing the message for every mouse click for as long as the page is open. Note that the subscribes do not “block”, so the above two subscriptions will run in parallel. That is, we will receive messages on the console for either key or mouse downs whenever they occur.
+ +The following achieves the same thing with a single subscription using merge
:
merge(key$.pipe(map(e=>e.key)),
+ mouse$.pipe(map(_=>"!!"))
+).subscribe(console.log)
+
The following is a very small (but sufficiently useful) subset of the functionality available for RxJS. +I’ve simplified the types rather greatly for readability and not always included all the optional arguments.
+ +The following functions create ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming. + streams from various sources.
+ +// produces the list of arguments as elements of the stream
+of<T>(...args: T[]): Observable<T>
+
+// produces a stream of numbers from “start” until “count” been emitted
+range(start?: number, count?: number): Observable<number>
+
+// produces a stream for the specified event, element type depends on
+// event type and should be specified by the type parameter, e.g.: MouseEvent, KeyboardEvent
+fromEvent<T>(target: FromEventTarget<T>, eventName: string): Observable<T>
+
+// produces a stream of increasing numbers, emitted every “period” milliseconds
+// emits the first event immediately
+interval(period?: number): Observable<number>
+
+// after given initial delay, emit numbers in sequence every specified duration
+timer(initialDelay: number, period?: number): Observable<number>
+
+
Creating new ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming. + streams from existing streams
+ +// create a new Observable stream from the merge of multiple Observable streams.
+// The resulting stream will have elements of Union type.
+// i.e. the type of the elements will be the Union of the types of each of the merged streams
+// Note: there is also an operator version.
+merge<T, U...>(t: Observable<T>, u: Observable<U>, ...): Observable<T | U | ...>
+
+// create n-ary tuples (arrays) of the elements at the head of each of the incoming streams
+zip<T, U...>(t: Observable<T>, r: Observable<U>):Observable<[T, U, ...]>
+
Methods on the ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming. + object itself that may be chained.
+ +// composes together a sequence of operators (see below) that are applied to transform the stream
+pipe<A>(...op1: OperatorFunction<T, A>): Observable<A>;
+
+// {next} is a function applied to each element of the stream
+// {error} is a function applied in case of error (only really applicable for communications)
+// {complete} is a function applied on completion of the stream (e.g. cleanup)
+// @return {Subscription} returns an object whose “unsubscribe” method may be called to cleanup
+// e.g. unsubscribe could be called to cancel an ongoing Observable
+subscribe(next?: (value: T) => void, error?: (error: any) => void, complete?: () => void): Subscription;
+
Operators are passed to pipe
. They all return an OperatorFunction
which is used by pipe
.
// transform the elements of the input stream using the “project” function
+map<T, R>(project: (value: T) => R)
+
+// only take elements which satisfy the predicate
+filter<T>(predicate: (value: T) => boolean)
+
+// take “n” elements
+take<T>(n: number)
+
+// take the last element
+last<T>()
+
+// AKA flatMap: produces an Observable<R> for every input stream element T
+mergeMap<T, R>(project: (value: T) => Observable<R>)
+
+// accumulates values from the stream
+scan<T, R>(accumulator: (acc: R, value: T) => R, seed?: R)
+
+// push an arbitrary object on to the start/end of the stream
+startWith<T>(o: T)
+endWith<T>(o: T)
+
Modern computer systems often have to deal with asynchronousOperations that occur independently of the main program flow, allowing the program to continue executing while waiting for the operation to complete. + processing. Examples abound:
+ +Under the hood, most of these systems work on an event model, a kind of single-threaded multitasking where the program (after initialisation) polls a FIFO (First-In-First-Out) queue for incoming events in the so-called event loop. When an event is popped from the queue, any subscribed actions for the event will be applied.
+ +In JavaScript the first event loop you are likely to encounter is the browser’s. Every object in the DOM (Document Object Model - the tree data structure behind every webpage) has events that can be subscribed to, by passing in a callback function which implements the desired action. We saw a basic click handler earlier.
+ +Handling a single event in such a way is pretty straightforward. Difficulties arise when events have to be nested to handle a (potentially bifurcating) sequence of possible events.
+ +A simple example that begins to show the problem is implementing a UI to allow a user to drag an object on (e.g.) an SVGScalable Vector Graphics - another part of the HTML standard for specifying images declaratively as sets of shapes and paths. + canvas (play with it here!). We illustrate the desired behaviour below. When the user presses and holds the left mouse button we need to initiate dragging of the blue rectangle. The rectangle should move with the mouse cursor such that the x and y offsets of the cursor position from the top-left corner of the rectangle remain constant.
+ +The state machine that models this behaviour is pretty simple:
+ +There are only three transitions, each triggered by an event.
+ +The typical way to add interaction to web-pages and other UIs has historically been by adding Event Listeners to the UI elements for which we want interactive behaviour. In software engineering terms it’s typically referred to as the Observer Pattern (not to be confused with the “ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming. +” FRP abstraction we have been discussing).
+ +Here’s an event-driven code fragment that provides such dragging for some SVGScalable Vector Graphics - another part of the HTML standard for specifying images declaratively as sets of shapes and paths.
+ element draggableRect
that is a child of an SVGScalable Vector Graphics - another part of the HTML standard for specifying images declaratively as sets of shapes and paths.
+ canvas element referred to by the variable svg
:
const svg = document.getElementById("svgCanvas")!;
+const rect = document.getElementById("draggableRect")!;
+rect.addEventListener('mousedown',e => {
+ const
+ xOffset = Number(rect.getAttribute('x')) - e.clientX,
+ yOffset = Number(rect.getAttribute('y')) - e.clientY,
+ moveListener = (e:MouseEvent)=>{
+ rect.setAttribute('x',String(e.clientX + xOffset));
+ rect.setAttribute('y',String(e.clientY + yOffset));
+ },
+ done = ()=>{
+ svg.removeEventListener('mousemove', moveListener);
+ };
+ svg.addEventListener('mousemove', moveListener);
+ svg.addEventListener('mouseup', done);
+})
+
We add “event listeners” to the HTMLHyper-Text Markup Language - the declarative language for specifying web page content.
+ elements, which invoke the specified functions when the event fires. There are some awkward dependencies. The moveListener
function needs access to the mouse coordinates from the mousedown event, the done
function which ends the drag on a mouseup
event needs a reference to the moveListener
function so that it can clean it up.
It’s all a bit amorphous:
+ +removeEventListener
(or potentially deal with weird behaviour when unwanted zombie events fire).The last issue is not unlike the kind of resource cleanup that RAII is meant to deal with.
+Generally speaking, nothing about this function resembles the state machine diagram.
+The code sequencing has little sensible flow. The problem gets a lot worse in highly interactive web pages with lots of different possible interactions all requiring their own event handlers and cleanup code.
We now rewrite precisely the same behaviour using ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming. + FRP:
+ + const svg = document.getElementById("svgCanvas")!;
+ const rect = document.getElementById("draggableRect")!;
+
+ const mousedown = fromEvent<MouseEvent>(rect,'mousedown'),
+ mousemove = fromEvent<MouseEvent>(svg,'mousemove'),
+ mouseup = fromEvent<MouseEvent>(svg,'mouseup');
+
+ mousedown
+ .pipe(
+ map(({clientX, clientY}) => ({
+ mouseDownXOffset: Number(rect.getAttribute('x')) - clientX, // <-\
+ mouseDownYOffset: Number(rect.getAttribute('y')) - clientY // <-|
+ })), // D
+ mergeMap(({mouseDownXOffset, mouseDownYOffset}) => // E
+ mousemove // P
+ .pipe( // E
+ takeUntil(mouseup), // N
+ map(({clientX, clientY}) => ({ // D
+ x: clientX + mouseDownXOffset, // E
+ y: clientY + mouseDownYOffset // N
+ }))))) // C
+ .subscribe(({x, y}) => { // Y
+ rect.setAttribute('x', String(x)) // >-----------------------------|
+ rect.setAttribute('y', String(y)) // >-----------------------------/
+ });
+
The ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming. +’s mousedown, mousemove and mouseup are like streams which we can transform with familiar operators like map and takeUntil. The mergeMap operator “flattens” the inner mousemove ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming. + stream back to the top level, then subscribe will apply a final action before doing whatever cleanup is necessary for the stream.
+ +Compared to our state machine diagram above:
+ +takeUntil
function when it closes the streams.However, there is still something not very elegant about this version. As indicated by my crude ASCII art in the comment above, there is a dependency in the function applied to the stream by the first map
, on the DOM element being repositioned in the function applied by subscribe. This dependency on mutable state outside the function scope makes this solution impure.
We can remove this dependency on mutable state, making our event stream a pure “closed system”, by introducing a scan
operator on the stream to accumulate the state using a pure functionA function that always produces the same output for the same input and has no side effects.
+.
+First, let’s define a type for the state that will be accumulated by the scan
operator. We are concerned with
+the position of the top-left corner of the rectangle, and (optionally, since it’s only relevant during mouse-down dragging) the offset of the click position from the top-left of the rectangle:
type State = Readonly<{
+ pos:Point,
+ offset?:Point
+}>
+
We’ll introduce some types to model the objects coming through the stream and the effects they have when applied to a State
object in the scan
. First, all the events we care about have a position on the SVGScalable Vector Graphics - another part of the HTML standard for specifying images declaratively as sets of shapes and paths.
+ canvas associated with them, so we’ll have a simple immutable Point
interfaceA TypeScript construct that defines the shape of an object, specifying the types of its properties and methods.
+ with x
and y
positions and a couple of handy vector math methods (note that these create a new Point
rather than mutating any existing state within the Point
):
class Point {
+ constructor(public readonly x:number, public readonly y:number){}
+ add(p:Point) { return new Point(this.x+p.x,this.y+p.y) }
+ sub(p:Point) { return new Point(this.x-p.x,this.y-p.y) }
+}
+
Now we create a subclass of Point
with a constructor letting us instantiate it for a given (DOM) MouseEvent
and an abstract
(placeholder) definition for a function to apply the correct update action to the State
:
abstract class MousePosEvent extends Point {
+ constructor(e:MouseEvent) { super(e.clientX, e.clientY) }
+ abstract apply(s:State):State;
+}
+
And now two further subclasses with concrete definitions for apply
.
class DownEvent extends MousePosEvent {
+ apply(s:State) { return { pos: s.pos, offset: s.pos.sub(this) }}
+ }
+ class DragEvent extends MousePosEvent {
+ apply(s:State) { return { pos: this.add(s.offset), offset: s.offset }}
+ }
+
Setup of the streams is as before:
+ +const svg = document.getElementById("svgCanvas")!,
+ rect = document.getElementById("draggableRect")!,
+ mousedown = fromEvent<MouseEvent>(rect,'mousedown'),
+ mousemove = fromEvent<MouseEvent>(svg,'mousemove'),
+ mouseup = fromEvent<MouseEvent>(svg,'mouseup');
+
But now we’ll capture initial position of the rectangle one time only in an immutable Point
object outside of the stream logic.
const initialState: State = {
+ pos: new Point(
+ Number(rect.getAttribute('x')),
+ Number(rect.getAttribute('y')))
+}
+
Now we will be able to implement the ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ stream logic, using a function passed to scan
to manage state.
+Since we use only pure functionsA function that always produces the same output for the same input and has no side effects.
+ we have a strong guarantee that the logic is self-contained, with no dependency on the state of the outside world!
mousedown
+ .pipe(
+ mergeMap(mouseDownEvent =>
+ mousemove.pipe(
+ takeUntil(mouseup),
+ map(mouseDragEvent=>new DragEvent(mouseDragEvent)),
+ startWith(new DownEvent(mouseDownEvent)))),
+ scan((s: State, e: MousePosEvent) => e.apply(s),
+ initialState))
+ .subscribe(e => {
+ rect.setAttribute('x', String(e.rect.x))
+ rect.setAttribute('y', String(e.rect.y))
+ });
+
Note that inside the mergeMap
we use the startWith
operator to force a DownEvent
onto the start of the flattened stream. Then the accumulator function passed to scan
uses sub-type polymorphism to cause the correct behaviour for the different types of MousePosEvent
`.
The advantage of this code is not brevity; with the introduced type definitions it’s longer than the previous implementations of the same logic. Rather, the advantages of this pattern are:
+ +subscribe
;merge
in more input streams, adding Event types to handle their State
updates, and the only place we have to worry about effects visible to the outside world is in the function passed to subscribe
.As an example of scalability we will be using this same pattern to implement the logic of an asteroids arcade game in the next chapter.
+ +In RxJS, mergeMap
, switchMap
, and concatMap
are operators used for transforming and flattening observablesA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+. Each has its own specific behavior in terms of how it handles incoming values and the resulting observableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ streams. Here’s a breakdown of each:
Let’s consider three almost identical pieces of code
+ +fromEvent(document, "mousedown").pipe(mergeMap(() => interval(200)))
+fromEvent(document, "mousedown").pipe(switchMap(() => interval(200)))
+fromEvent(document, "mousedown").pipe(concatMap(() => interval(200)))
+
With mergeMap
, each mousedown event triggers a new interval(200)
observableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+. All these interval observablesA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ will run concurrently, meaning their emitted values will interleave in the output. In the animation, the x2
occurs when two observablesA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ emit at approximately the same time, and the values overlap too much to show separately.
With switchMap
, each time a mousedown
event occurs, it triggers an interval(200)
observableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+. If another mousedown event occurs before the interval observableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ finishes (interval doesn’t finish on its own), the previous interval observableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ is canceled, and a new one begins. This means only the most recent mousedown event’s observableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ is active. This can be seen as the counter restarting every single time a click occurs (remember interval emits sequential numbers).
With concatMap
, each time a mousedown event occurs, it starts emitting values from the interval(200)
observableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+. Importantly, if a second mousedown event occurs while the previous interval observableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ is still emitting, the new interval won’t start until the previous one has completed. However, since interval is a never-ending observableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+, in practice, each mousedown event’s observableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ will queue up and only start after the previous ones are manually stopped or canceled. Therefore, no matter how many times a click occurs, the next interval will never begin.
We can make an adjustment to this, where, we stop the interval after four items.
+ +fromEvent(document, "mousedown").pipe(concatMap(() => interval(200).pipe(take(4))))
+
Unlike the previous example with a never-ending interval, in this case, each interval observableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming. + completes after emitting four values, so the next mousedown event’s observableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming. + will queue up and start automatically as soon as the previous one completes. This setup ensures that each click’s sequence of interval emissions will be handled one after the other, with no overlap, maintaining the order of clicks and processing each one to completion before starting the next.
+ +Asynchronous: Operations that occur independently of the main program flow, allowing the program to continue executing while waiting for the operation to complete.
+ +Functional Reactive Programming (FRP): A programming paradigm that combines functional and reactive programming to handle asynchronous data streams and event-driven systems.
+ +Observable: A data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ ++ + + 24 + + min read
+where
and let
clauses.This section is not your usual “First Introduction To Haskell” because it assumes you have arrived here having already studied some reasonably sophisticated functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + programming concepts in JavaScript, basic parametric polymorphic types in TypeScript, and Lambda CalculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s. +. Familiarity with higher-order and curried functionsFunctions that take multiple arguments one at a time and return a series of functions. + is assumed.
+ +I try to summarise the syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. + we use with “cheatsheets” throughout, but the official CheatSheet will be a useful reference. The other reference every Haskell programmer needs is the official library docs on Hackage which is searchable by the excellent HoogleA Haskell API search engine that allows users to search for functions by name or by type signature. + search engine, which lets you search by types as well as function keywords.
+ +If you would like a more gradual introduction, “Haskell Programming from First Principles” by Allen and Moronuki is a recent and excellent introduction to Haskell that is quite compatible with the goals of this course. The ebook is not too expensive, but unfortunately, it is independently published and hence not available from our library. There are a few copies of Programming Haskell by Hutton which is an excellent academic textbook, but it’s expensive to buy. “Learn you a Haskell” by Miran Lipovaca is a freely available alternative that is also a useful introduction.
+ +A good way to get started with haskell is simply to experiment with the GHCi REPLThe interactive Read-Eval-Print Loop for GHC, the Glasgow Haskell Compiler, allowing users to test Haskell programs and expressions interactively. + (or Read Eval Print Loop). You can install GHC from here.
+ +Start by making a file: fibs.hs
fibs 0 = 1 -- two base cases,
+fibs 1 = 1 -- resolved by pattern matching
+fibs n = fibs (n-1) + fibs (n-2) -- recursive definition
+
Then load it into GHCi like so:
+ +ghci fibs.hs
+
You’ll get a prompt that looks like:
+ +ghci>
+
You can enter haskell expressions directly at the prompt:
+ +ghci> fibs 6
+
++ +13
+
I’m going to stop showing the prompt now, but you can enter all of the following directly at the prompt and you will see similar results printed to those indicated below.
+ +Basic logic operators are similar to C/Java/etc: ==
, &&
, ||
.
fibs 6 == 13
+
++ +True
+
An exception is “not-equal”, whose operator is /=
(Haskell tends to prefer more “mathy” syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language.
+ whenever possible).
fibs 6 /= 13
+
++ +False
+
If-then-else expressions return a result (like javascript ternary ? :
)
if fibs 6 == 13 then "yes" else "no"
+
++ +“yes”
+
if fibs 6 == 13 && fibs 7 == 12 then "yes" else "no"
+
++ +“no”
+
GHCi also has a number of non-haskell commands you can enter from the prompt, they are prefixed by :
.
+You can reload your .hs file into ghci after an edit with :r
. Type :h
for help.
You can declare local variables in the repl
:
x = 3
+y = 4
+x + y
+
++ +7
+
One of Haskell’s distinguishing features from other languages is that its variables are strictly immutable (i.e. not variable at all really). This is similar to variables declared with JavaScript’s const
keyword - but everything in Haskell is deeply immutable by default.
However, in the GHCi REPLThe interactive Read-Eval-Print Loop for GHC, the Glasgow Haskell Compiler, allowing users to test Haskell programs and expressions interactively. +, you can redefine variables and haskell will not complain.
+ +Both the simplest and tail-recursive versions of our PureScript fibs code are also perfectly legal Haskell code. The main function will be a little different, however:
+ +main :: IO ()
+main = print $ map fibs [1..10]
+
I’ve included the type signature for main although it’s not absolutely necessary (the compiler can usually infer the type of such functions automatically, as it did for our fibs function definition above), but it is good practice to define types for all top-level functions (functions that are not nested inside other functions) and also the IO
type is interesting, and will be discussed at length later. The main function takes no inputs (no need for ->
with something on the left) and it returns something in the IO
monad. Without getting into it too much, yet, monads are special types that can also wrap some other value. In this case, the main function just does output, so there is no wrapped value and hence the ()
(called unitA type with exactly one value, (), used to indicate the absence of meaningful return value, similar to void in other languages.
+) indicates this. You can think of it as being similar to the void type in C, Java or TypeScript.
What this tells us is that the main function produces an IO side effect. This mechanism is what allows Haskell to be a pure functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus.
+ programming language while still allowing you to get useful stuff done. Side effectsAny state change that occurs outside of a function’s local environment or any observable interaction with the outside world, such as modifying a global variable, writing to a file, or printing to a console.
+ can happen, but when they do occur they must be neatly bundled up and declared to the type system, in this case through the IO
monad. For functions without side-effects, we have strong, compiler-checked guarantees that this is indeed so (that they are pure).
By the way, once you are in the IO
monad, you can’t easily get rid of it. Any function that calls a function that returns an IO
monad, must have IO
in its return type. Thus, effectful code is possible, but the type system ensures we are aware of it and can limit its taint. The general strategy is to use pure functionsA function that always produces the same output for the same input and has no side effects.
+ wherever possible, and push the effectful code as high in your call hierarchy as possible—that is, limit the size and scope of impure code as much as possible. Pure functionsA function that always produces the same output for the same input and has no side effects.
+ are much more easily reusable in different contexts.
The print
function is equivalent to the PureScript log $ show
. That is, it uses any available show
function for the type of value being printed to convert it to a string, and it then prints that string. Haskell defines show for many types in the PreludeThe default library loaded in Haskell that includes basic functions and operators.
+, but print in this case invokes it for us. The other difference here is that square brackets operators are defined in the preludeThe default library loaded in Haskell that includes basic functions and operators.
+ for linked lists. In PureScript they were used for Arrays - which (in PureScript) don’t have the range operator (..
) defined so I avoided them. Speaking of List operators, here’s a summary:
The default Haskell lists are cons lists (linked lists defined with a cons
function), similar to those we defined in JavaScript.
[] -- an empty list
+[1,2,3,4] -- a simple lists of values
+[1..4] -- ==[1,2,3,4] (..) is range operator
+1:[2,3,4] -- ==[1,2,3,4], use `:` to “cons” an element to the start of a list
+1:2:3:[4] -- ==[1,2,3,4], you can chain `:`
+[1,2]++[3,4] -- ==[1,2,3,4], i.e. (++) is concat
+
+-- You can use `:` to pattern match lists in function definitions.
+-- Note the enclosing `()` to delimit the pattern for the parameter.
+length [] = 0
+length (x:xs) = 1 + length xs -- x is bound to the head of the list and xs the tail
+-- (although you don’t need to define `length`, it’s already loaded by the prelude)
+
+length [1,2,3]
+
++ +3
+
Some other useful functions for dealing with lists:
+ +head [1,2,3] -- 1
+tail [1,2,3] -- [2,3]
+
+sum [1,2,3] -- 6 (but only applicable for lists of things that can be summed)
+minimum [1,2,3] -- 1 (but only for Ordinal types)
+maximum [1,2,3] -- 3
+
+map f [1,2,3] -- maps the function f over the elements of the list returning the result in another list
+
Tuples are fixed-length collections of values that may not necessarily be of the same type. They are enclosed in ()
t = (1,"hello") -- define variable t to a tuple of an Int and a String.
+fst t
+
++ +1
+
snd t
+
++ +“hello”
+
And you can destructure and pattern match tuples:
+ +(a,b) = t
+a
+
++ +1
+
b
+
++ +“hello”
+
Note that we created tuples in JavaScript using []
—actually they were fixed-length arrays, don’t confuse them for Haskell lists or tuples.
Haskell strategy for evaluating expressions is lazy by default—that is it defers evaluation of expressions until it absolutely must produce a value. Laziness is of course possible in other languages (as we have seen in JavaScript), and there are many lazy data-structures defined and available for PureScript (and most other functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + languages). +Conversely, Haskell can be forced to use strict evaluation and has libraries of datastructures with strict semanticsThe processes a computer follows when executing a program in a given language. + if you need them.
+ +However, lazy by default sets Haskell apart. It has pros and cons, on the pro side:
+ +But there are definitely cons:
+ +The Haskell way of defining Lambda (anonymous) functions is heavily inspired by Lambda CalculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s. +, but also looks a bit reminiscent of the JavaScript arrow syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. +:
+ +Lambda Calculus
+λx. x
+
+JavaScript
+x => x
+
+Haskell
+\x -> x
+
+
+Since it’s lazy-by-default, it’s possible to transfer the version of the Y-combinatorA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments. + we explored in Lambda CalculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s. + into haskell code almost as it appears in Lambda CalculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s. +:
+ +y = \f -> (\x -> f (x x)) (\x -> f (x x))
+
However, to get it to type-check one has to either write some gnarly type definitions or force the compiler to do some unsafe type coercion. The following (along with versions of the Y-CombinatorA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments. + that do type check in haskell) are from an excellent Stack Overflow post:
+ +import Unsafe.Coerce
+y :: (a -> a) -> a
+y = \f -> (\x -> f (unsafeCoerce x x)) (\x -> f (unsafeCoerce x x))
+main = putStrLn $ y ("circular reasoning works because " ++)
+
Consider the following pseudocode for a simple recursive definition of the Quick Sort algorithm:
+ +QuickSort list:
+ Take head of list as a pivot
+ Take tail of list as rest
+ return
+QuickSort( elements of rest < pivot ) ++ (pivot : QuickSort( elements of rest >= pivot ))
+
+
+We’ve added a bit of notation here: a : l
inserts a (“cons”es) to the front of a list l
; l1 ++ l2
is the concatenation of lists l1
and l2
.
In JavaScript the fact that we have anonymous functionsA function defined without a name, often used as an argument to other functions. Also known as lambda function. + through compact arrow syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. + and expression syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. + if (with ? : ) means that we can write pure functionsA function that always produces the same output for the same input and has no side effects. + that implement this recursive algorithm in a very functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. +, fully-curried style. However, the language syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. + really doesn’t do us any favours!
+ +For example,
+ +const
+ sort = lessThan=>
+ list=> !list ? null :
+ (pivot=>rest=>
+ (lesser=>greater=>
+ concat(sort(lessThan)(lesser))
+ (cons(pivot)(sort(lessThan)(greater)))
+ )(filter(a=> lessThan(a)(pivot))(rest))
+ (filter(a=> !lessThan(a)(pivot))(rest))
+ )(head(list))(tail(list))
+
Consider, the following, more-or-less equivalent haskell implementation:
+ +sort [] = []
+sort (pivot:rest) = lesser ++ [pivot] ++ greater
+ where
+ lesser = sort $ filter (<pivot) rest
+ greater = sort $ filter (>=pivot) rest
+
An essential thing to know before trying to type in the above function is that Haskell delimits the scope of multi-line function definitions (and all multiline expressions) with indentation (complete indentation rules reference here). The where
keyword lets us create multiple function definitions that are visible within the scope of the parent function, but they must all be left-aligned with each other and to the right of the start of the line containing the where
keyword.
Haskell also helps with a number of other language features.
+First, is pattern matchingA mechanism in functional programming languages to check a value against a pattern and to deconstruct data.
+. Pattern matchingA mechanism in functional programming languages to check a value against a pattern and to deconstruct data.
+ is like function overloading that you may be familiar with from languages like Java or C++ - where the compiler matches the version of the function to invoke for a given call by matching the type of the parameters to the type of the call - except in Haskell the compiler goes a bit deeper to inspect the values of the parameters.
There are two declarations of the sort function above. The first handles the base case of an empty list. The second handles the general case, and pattern matchingA mechanism in functional programming languages to check a value against a pattern and to deconstruct data. + is again used to destructure the lead cons expression into the pivot and rest variables. No explicit call to head and tail functions is required.
+ +The next big difference between our Haskell quicksort and our previous JavaScript definition is the Haskell style of function application - which has more in common with lambda calculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s.
+ than JavaScript. The expression f x
is application of the function f
to whatever x
is.
Another thing that helps with readability is infix operators. For example, ++
is an infix binary operator for list concatenation. The :
operator for cons is another. There is also the aforementioned $ which gives us another trick for removing brackets, and finally, the <
and >=
operators. Note, that infix operators can also be curried and left only partially applied as in (<pivot)
.
Next, we have the where
which lets us create locally scoped variables within the function declaration without the need for the trick I used in the JavaScript version of using the parameters of anonymous functionsA function defined without a name, often used as an argument to other functions. Also known as lambda function.
+ as locally scoped variables.
Finally, you’ll notice that the haskell version of sort appears to be missing a parameterisation of the order function. Does this mean it is limited to number types? In fact, no - from our use of <
and >=
the compiler has inferred that it is applicable to any ordered type. More specifically, to any type in the type classA type system construct in Haskell that defines a set of functions that can be applied to different types, allowing for polymorphic functions.
+ Ord
.
I deliberately avoided the type declaration for the above function because: (1) we haven’t really talked about types properly yet, and (2) I wanted to show off how clever Haskell type inference is. However, it is actually good practice to include the type signature. If one were to load the above code, without type definition, into GHCi (the Haskell REPL), one could interrogate the type like so:
+ +> :t sort
+sort :: Ord t => [t] -> [t]
+
Thus, the function sort
has a generic type-parameter t
(we’ll talk more about such parametric polymorphismA type of polymorphism where functions or data types can be written generically so that they can handle values uniformly without depending on their type.
+ in haskell later) which is constrained to be in the Ord
type classA type system construct in Haskell that defines a set of functions that can be applied to different types, allowing for polymorphic functions.
+ (anything that is orderable - we’ll talk more about type classes too). Its input parameter is a list of t
, as is its return type. This is also precisely the syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language.
+ that one would use to declare the type explicitly. Usually, for all top-level functions in a Haskell file it is good practice to explicitly give the type declaration. Although it is not always necessary, it can avoid ambiguity in many situations and once you get good at reading Haskell types it becomes useful documentation.
Here’s another refactoring of the quick-sort code. This time with type declaration because I just said it was the right thing to do:
+ +sort :: Ord t => [t] -> [t]
+sort [] = []
+sort (pivot:rest) = below pivot rest ++ [pivot] ++ above pivot rest
+ where
+ below p = partition (<p)
+ above p = partition (>=p)
+ partition comparison = sort . filter comparison
+
The list
parameter for below
and above
has been eta-reduced away just as we were able to eta-reduce lambda calculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s.
+ expressions. The definition of the partition
function in this version uses the .
operator for function composition. That is, partition comparison
is the composition of sort
and filter comparison
and again the list
parameter is eta-reduced away.
Although it looks like the comparison parameter could also go away here with eta conversionSubstituting functions that simply apply another expression to their argument with the expression in their body. This is a technique in Haskell and Lambda Calculus where a function f x is simplified to f, removing the explicit mention of the parameter when it is not needed.
+, actually the low precedence of the .
operator means there is (effectively) implicit parentheses around filter comparison. We will see how to more aggressively refactor code to be point-free later.
The idea of refactoring our code into the above form was to demonstrate the freedom that Haskell gives us to express logic +in a way that makes sense to us. This version reads almost like a natural language declarativeDeclarative languages focus on declaring what a procedure (or function) should do rather than how it should do it. + definition of the algorithm. That is, you can read:
+ +sort (pivot:rest) = below pivot rest ++ [pivot] ++ above pivot rest
+
as:
+++ +the sort of a list where we take the first element as the “pivot” and everything after as “rest” is +everything that is below pivot in rest,
+
+concatenated with a list containing just the pivot,
+concatenated with everything that is above pivot in rest.
Haskell has a number of features that allow us to express ourselves in different ways. Above we used a where
clause to give a post-hoc, locally-scoped declaration of the below and above functions. Alternately, we could define them at the start of the function body with let <variable declaration expression> in <body>
. Or we can use let
, in
and where
all together, like so:
sort :: Ord t => [t] -> [t]
+sort [] = []
+sort (pivot:rest) = let
+ below p = partition (<p)
+ above p = partition (>=p)
+ in
+ below pivot rest ++ [pivot] ++ above pivot rest
+ where
+ partition comparison = sort . filter comparison
+
Note that where is only available in function declarations, not inside expressions and therefore is not available in a lambda. However, let
-in
is part of the expression, and therefore available inside a lambda function. A silly example would be: \i -> let f x = 2*x in f i
, which could also be spread across lines, but be careful to get the correct indentation.
Provides alternative cases for function definitions matching different values or possible destructurings of the function arguments (more detail). As per examples above and:
+ +fibs 0 = 1
+fibs 1 = 1
+fibs n = fibs (n-1) + fibs (n-2)
+
if <condition> then <case 1> else <case 2>
+
just like javascript ternary if operator: <condition> ? <case 1> : <case 3>
fibs n = if n == 0 then 1 else if n == 1 then 1 else fibs (n-1) + fibs (n-2)
+
Can test Bool expressions (i.e. not just values matching as in pattern matchingA mechanism in functional programming languages to check a value against a pattern and to deconstruct data. +)
+ +fibs n
+ | n == 0 = 1
+ | n == 1 = 1
+ | otherwise = fibs (n-1) + fibs (n-2)
+
case <expression> of
+ <pattern1> -> <result if pattern1 matches>
+ <pattern2> -> <result if pattern2 matches>
+ _ -> <result if no pattern above matches>
+
For example:
+ +fibs n = case n of
+ 0 -> 1
+ 1 -> 1
+ _ -> fibs (n-1) + fibs (n-2)
+
GHCi REPL: The interactive Read-Eval-Print Loop for GHC, the Glasgow Haskell Compiler, allowing users to test Haskell programs and expressions interactively.
+ +Pattern Matching: A mechanism in Haskell that checks a value against a pattern. It is used to simplify code by specifying different actions for different input patterns.
+ +Guards: A feature in Haskell used to test boolean expressions. They provide a way to conditionally execute code based on the results of boolean expressions.
+ +Where Clauses: A way to define local bindings in Haskell, allowing variables or functions to be used within a function body.
+ +Let Clauses: A way to bind variables or functions within an expression in Haskell, allowing for more localized definitions.
+ +Hoogle: A Haskell API search engine that allows users to search for functions by name or by type signature.
+ +Prelude: The default library loaded in Haskell that includes basic functions and operators.
+ +Case Expressions: A way to perform pattern matching in Haskell that allows for more complex conditional logic within expressions.
+ +Type Class: A type system construct in Haskell that defines a set of functions that can be applied to different types, allowing for polymorphic functions.
+ +Unit: A type with exactly one value, (), used to indicate the absence of meaningful return value, similar to void in other languages.
+ ++ + + 20 + + min read
+We can declare custom types for data in Haskell using the data
keyword. Consider the following declaration of our familiar cons list:
data ConsList = Nil | Cons Int ConsList
+
The |
operator looks rather like the union typeA TypeScript construct that allows a variable to hold values of multiple specified types, separated by the |
symbol.
+ operator in TypeScript, and indeed it serves a similar purpose. Here, a ConsList
is defined as being a composite type, composed of either Nil
or a Cons
of an Int
value and another ConsList
. This is called an “algebraic data type” because |
is like an “or”, or algebraic “sum” operation for combining elements of the type while separating them with a space is akin to “and” or a “product” operation.
Note that neither Nil
or Cons
are built in. They are simply labels for constructor functions for the different versions of a ConsList
node. You could equally well call them EndOfList
and MakeList
or anything else that’s meaningful to you. Nil
is a function with no parameters, Cons
is a function with two parameters. Int
is a built-in primitive type for limited-precision integers.
Now we can create a small list like so:
+ +l = Cons 1 $ Cons 2 $ Cons 3 Nil
+
We can construct a type UserId
with one parameter, Int
data UserId = UserId Int
+newtype UserId = UserId Int
+
These are almost identical, and we can use them both equivallentally, e.g.,
+ +student :: UserId
+student = UserId 1337
+
The newtype
keyword is used to define a type that has exactly one constructor with exactly one field. It is primarily used for creating a distinct type from an existing type with zero runtime overhead. This can be useful for adding type safety to your code by creating new types that are distinct from their underlying types or giving types a greater semantic meaning, e.g., a UserId compared to an Int.
The data keyword is used to define an algebraic data type (ADT). This allows for the creation of complex data structures that can have multiple constructors. Each constructor can take zero or more arguments, and these arguments can be of any type.
+ +In Haskell, we can define multiple versions of a function to handle the instances of an algebraic data types(or ADTs) Custom data types in Haskell defined using the data keyword, allowing the combination of different types into one composite type using the | operator.
+. This is done by providing a pattern in the parameter list of the function definition, in the form of an expression beginning with the constructor of the data instance (e.g. Cons
or Nil
) and variable names which will be bound to the different fields of the data instance.
For example, we can create a function to determine a ConsList
’s length using pattern matchingA mechanism in functional programming languages to check a value against a pattern and to deconstruct data.
+; to not only create different definitions of the function for each of the possible instances of a ConsList
, but also to destructure the non-empty Cons
:
consLength :: ConsList -> Int
+consLength Nil = 0
+consLength (Cons _ rest) = 1 + consLength rest
+
Since we don’t care about the head value in this function, we match it with _
, an unnamed variable, which effectively ignores it. Note that another way to conditionally destructure with pattern matchingA mechanism in functional programming languages to check a value against a pattern and to deconstruct data.
+ is using a case statement.
Note that such a definition for lists is made completely redundant by Haskell’s wonderful built-in lists, where []
is the empty list, and :
is an infix cons operator. We can pattern match the empty list or destructure (head:rest)
, e.g.:
intListLength :: [Int] -> Int -- takes a list of Int as input and returns an Int
+intListLength [] = 0
+intListLength (_:rest) = 1 + intListLength rest
+
Similar to TypeScript, Haskell provides parametric polymorphismA type of polymorphism where functions or data types can be written generically so that they can handle values uniformly without depending on their type.
+. That is, the type definitions for functions and data structures (defined with data
like the ConsList
above) can have type parametersA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+ (AKA type variables). For example, the definition intListLength
above is defined to only work with lists with Int
elements. This seems a silly restriction because in this function we don’t actually do anything with the elements themselves. Below, we introduce the type parameterA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+ a
so that the length
function will able to work with lists of any type of elements.
length :: [a] -> Int -- a is a type parameter
+length [] = 0
+length (_:rest) = 1 + length rest
+
The following visual summary shows pair data structures with accessor functions fst
and sec
defined using Record SyntaxAn alternate way to define data structures in Haskell with named fields, automatically creating accessor functions for those fields.
+ with varying degrees of type flexibility, and compared with the equivalent TypeScript generic notation:
Int
pairs onlya
in Haskell, and T
in TypeScript)GHCi allows you to use the :kind
(or :k
) command to interrogate the Kind of types – think of it as “meta information” about types and their type parametersA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+. The kind syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language.
+ indicates the arity or number of type parametersA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+ a type has. Note that it is like the syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language.
+ for function types (with the ->
), you can think of it as information about what is required in terms of type parametersA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+ to instantiate the type. If the constructor takes no type parametersA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+ the kind is just *
, (it returns a type), *->*
if it takes one type parameterA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+, *->*->*
for two type parametersA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+ and so on.
Another sort of “kind” are for type classesA way in Haskell to associate functions with types, similar to TypeScript interfaces. They define a set of functions that must be available for instances of those type classes.
+ which we will introduce more properly in a moment.
+For example, the “kind” for the Ord
type classA type system construct in Haskell that defines a set of functions that can be applied to different types, allowing for polymorphic functions.
+ (the class of things that are Orderable and which we came across in our simple implementation of quicksort) is:
> :k Ord
+Ord :: * -> Constraint
+
This tells us that Ord
takes one type parameterA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+ (for example it could be an Int
or other numeric type, or something more complex like the Student
type below), and returns a Constraint
rather than an actual type. Such a constraintA restriction on type parameters in Haskell, specifying that a type must belong to a certain type class.
+ is used to narrow the set of types to which a function may be applied, just as we saw Ord
being used as the type constraintA restriction on type parameters in Haskell, specifying that a type must belong to a certain type class.
+ for sort
:
> :t sort
+sort :: Ord t => [t] -> [t]
+
Consider the following simple record data type:
+ +data Student = Student Int String Int
+
A Student
has three fields, mysteriously typed Int
, String
and Int
. Let’s say my intention in creating the above data type was to store a student’s id, name and mark. I would create a record like so:
> t = Student 123 "Tim" 95
+
Here’s how one would search for the student with the best mark:
+ +best :: [Student] -> Student -> Student
+best [] b = b
+best (a@(Student _ _ am):rest) b@(Student _ _ bm) =
+ if am > bm
+ then best rest a
+ else best rest b
+
The @
notation, as in b@(Student _ _ bm)
stores the record itself in the variable b but also allows you to unpack its elements, e.g. bm
is bound to mark.
To get the data out of a record I would need to either destructure using pattern matchingA mechanism in functional programming languages to check a value against a pattern and to deconstruct data. +, as above, every time, or create some accessor functions:
+ +id (Student n _ _) = n
+name (Student _ n _) = n
+mark (Student _ _ n) = n
+> name t
+"Tim"
+
It’s starting to look a bit like annoying boilerplate code. Luckily, Haskell has another way to define such record types, called record syntaxAn alternate way to define data structures in Haskell with named fields, automatically creating accessor functions for those fields. +:
+ +data Student = Student { id::Int, name::String, mark::Int }
+
This creates a record type in every way the same as the above, but the accessor functions id
, name
and mark
are created automatically.
Haskell uses “type classesA way in Haskell to associate functions with types, similar to TypeScript interfaces. They define a set of functions that must be available for instances of those type classes. +” as a way to associate functions with types. A type classA type system construct in Haskell that defines a set of functions that can be applied to different types, allowing for polymorphic functions. + is like a promise that a certain type will have specific operations and functions available. You can think of it as being similar to a TypeScript interfaceA TypeScript construct that defines the shape of an object, specifying the types of its properties and methods. +.
+ +Despite the name however, it is not like an ES6/TypeScript class, since a Haskell type classA type system construct in Haskell that defines a set of functions that can be applied to different types, allowing for polymorphic functions.
+ does not actually give definitions for the functions themselves, only their type signatures.
+The function bodies are defined in “instances” of the type classA type system construct in Haskell that defines a set of functions that can be applied to different types, allowing for polymorphic functions.
+. A good starting point for gaining familiarity with type classesA way in Haskell to associate functions with types, similar to TypeScript interfaces. They define a set of functions that must be available for instances of those type classes.
+ is seeing how they are used in the standard Haskell preludeThe default library loaded in Haskell that includes basic functions and operators.
+. From GHCi we can ask for information about a specific typeclass with the :i
command, for example, Num
is a typeclass common to numeric types:
GHCi> :i Num
+class Num a where
+ (+) :: a -> a -> a
+ (-) :: a -> a -> a
+ (*) :: a -> a -> a
+ negate :: a -> a
+ abs :: a -> a
+ signum :: a -> a
+ fromInteger :: Integer -> a
+ {-# MINIMAL (+), (*), abs, signum, fromInteger, (negate | (-)) #-}
+ -- Defined in `GHC.Num'
+instance Num Word -- Defined in `GHC.Num'
+instance Num Integer -- Defined in `GHC.Num'
+instance Num Int -- Defined in `GHC.Num'
+instance Num Float -- Defined in `GHC.Float'
+instance Num Double -- Defined in `GHC.Float'
+
The first line (beginning class
) tells us that for a type to be an instance of the Num
typeclass, it must provide the operators +
, *
and the functions abs
, signum
and fromInteger
, and either (-)
or negate
. The last is an option because a default definition exists for each in terms of the other. The last five lines (beginning with “instance
”) tell us which types have been declared as instances of Num
and hence have definitions of the necessary functions. These are Word
, Integer
, Int
, Float
and Double
. Obviously this is a much more finely grained set of types than JavaScript’s universal “number
” type. This granularity allows the type system to guard against improper use of numbers that might result in loss in precision or division by zero.
The main numeric type we will use in this course is Int
, i.e. fixed-precision integers.
Note some obvious operations we would likely need to perform on numbers that are missing from the Num
typeclass. For example, equality checking. This is defined in a separate type classA type system construct in Haskell that defines a set of functions that can be applied to different types, allowing for polymorphic functions.
+ Eq
, that is also instanced by concrete numeric types like Int
:
> :i Eq
+class Eq a where
+ (==) :: a -> a -> Bool
+ (/=) :: a -> a -> Bool
+ {-# MINIMAL (==) | (/=) #-}
+...
+instance Eq Int
+...
+
Note again that instances need implement only ==
or /=
(not equal to), since each can be easily defined in terms of the other. Still we are missing some obviously important operations, e.g., what about greater-than and less-than? These are defined in the Ord
type classA type system construct in Haskell that defines a set of functions that can be applied to different types, allowing for polymorphic functions.
+:
> :i Ord
+class Eq a => Ord a where
+ compare :: a -> a -> Ordering
+ (<) :: a -> a -> Bool
+ (<=) :: a -> a -> Bool
+ (>) :: a -> a -> Bool
+ (>=) :: a -> a -> Bool
+ max :: a -> a -> a
+ min :: a -> a -> a
+ {-# MINIMAL compare | (<=) #-}
+
-- The compare function returns an Ordering:
+> :i Ordering
+data Ordering = LT | EQ | GT
+
A custom data type can be made an instance of Ord
by implementing either compare
or <=
. The definition Eq a => Ord a
means that anything that is an instance of Ord
must also be an instance of Eq
. Thus, typeclasses can build upon each other into rich hierarchies:
If we have our own data types, how can we make standard operations like equality and inequality testing work with them? Luckily, the most common type classesA way in Haskell to associate functions with types, similar to TypeScript interfaces. They define a set of functions that must be available for instances of those type classes.
+ can easily be instanced automatically through the deriving
keyword. For example, if we want to define a Suit
type for a card game we can automatically generate default instances of the functions (and operators) associated with Eq
uality testing, Ord
inal comparisons, Enum
erating the different possible values of the type, and Show
ing them (or converting them to string):
data Suit = Spade|Club|Diamond|Heart
+ deriving (Eq,Ord,Enum,Show)
+
+> Spade < Heart
+True
+
The Show
typeclass allows the data to be converted to strings with the show
function (e.g. so that GHCi can display it). The Enum
typeclass allows enumeration, e.g.:
> [Spade .. Heart]
+[Spade,Club,Diamond,Heart]
+
We can also create custom instances of typeclasses by providing our own implementation of the necessary functions, e.g.:
+ +instance Show Suit where
+ show Spade = "^" -- OK, these characters are not
+ show Club = "&" -- brilliant approximations of the
+ show Diamond = "O" -- actual playing card symbols ♠ ♣ ♦ ♥
+ show Heart = "V" -- but GHCi support for unicode
+ -- characters is a bit sketch
+> [Spade .. Heart]
+[^,&,O,V]
+
Another important built-in type is Maybe
:
> :i Maybe
+data Maybe a = Nothing | Just a
+
All the functions we have considered so far are assumed to be total. That is, the function provides a mapping for every element in the input type to an element in the output type. Maybe
allows us to have a sensible return-type for partial functions, that is, functions which do not have a mapping for every input:
For example, the built-in function lookup
can be used to search a list of key-value pairs, and fail gracefully by returning Nothing
if there is no matching key.
phonebook :: [(String, String)]
+phonebook = [ ("Bob", "01788 665242"), ("Fred", "01624 556442"), ("Alice", "01889 985333") ]
+
+> :t lookup
+lookup :: Eq a => a -> [(a, b)] -> Maybe b
+
+> lookup "Fred" phonebook
+Just "01624 556442"
+
+> lookup "Tim" phonebook
+Nothing
+
We can use pattern matchingA mechanism in functional programming languages to check a value against a pattern and to deconstruct data.
+ to extract values from a Maybe
(when we have Just
a value), or to perform some sensible default behaviour when we have Nothing
.
printNumber :: String -> IO ()
+printNumber name = msg $ lookup name phonebook
+where
+ msg (Just number) = print number
+ msg Nothing = print $ name ++ " not found in database"
+
+*GHCi> printNumber "Fred"
+"01624 556442"
+*GHCi> printNumber "Tim"
+"Tim not found in database"
+
We can also do this using a case statement.
+ +printNumber :: String -> IO ()
+printNumber name = msg $ lookup name phonebook
+where
+ msg value = case value of
+ (Just number) -> print number
+ _ -> print $ name ++ " not found in database"
+
Here we use the wildcard _
to match any other possible value, in this case, there is only one other value, Nothing
.
Algebraic Data Types: (or ADTs) Custom data types in Haskell defined using the data keyword, allowing the combination of different types into one composite type using the | operator.
+ +Record Syntax: An alternate way to define data structures in Haskell with named fields, automatically creating accessor functions for those fields.
+ +Type Classes: A way in Haskell to associate functions with types, similar to TypeScript interfaces. They define a set of functions that must be available for instances of those type classes.
+ +Constraint: A restriction on type parameters in Haskell, specifying that a type must belong to a certain type class.
+ +Type Kind: Meta-information about types and their type parameters in Haskell, indicating the number of type parameters a type has and the type it returns.
+ +Maybe: A built-in type in Haskell used to represent optional values, allowing functions to return either Just a value or Nothing to handle cases where no value is available.
+ +Total Functions: Functions that provide a mapping for every element in the input type to an element in the output type.
+ +Partial Functions: Functions that do not have a mapping for every input, potentially failing for some inputs.
+ ++ + + 74 + + min read
+In this chapter we see how the Haskell language features we introduced in previous chapters (from function application rules based on Lambda CalculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s. + to Typeclasses) lead to highly flexible and refactorable code and powerful abstractions.
+ +fmap
or (<$>)
operation.(<*>)
operator) to containers of values.The following equivalences make many refactorings possible in Haskell:
+ +Exactly as per Lambda CalculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s. +:
+ +f x ≡ g x
+f ≡ g
+
Remember haskell binary operators are just infix curried functionsFunctions that take multiple arguments one at a time and return a series of functions. + of two parameters and that putting brackets around them makes them prefix instead of infix.
+ +x + y ≡ (+) x y
+ ≡ ((+) x) y -- making function application precedence explicit
+ ≡ (x+) y -- binary operators can also be partially applied
+
Such operator sectioningThe process of partially applying an infix operator in Haskell by specifying one of its arguments. For example, (+1) is a section of the addition operator with 1 as the second argument. + allows us to get the right-most parameter of the function on it’s own at the right-hand side of the body expression such that we can apply eta conversionSubstituting functions that simply apply another expression to their argument with the expression in their body. This is a technique in Haskell and Lambda Calculus where a function f x is simplified to f, removing the explicit mention of the parameter when it is not needed. +, thus:
+ +f x = 1 + x
+f x = (1+) x
+f = (1+) -- eta conversion
+
Has its own operator in haskell (.)
, inspired by the mathematical function composition symbol ∘
:
(f ∘ g) (x) ≡ f (g(x)) -- math notation
+ (f . g) x ≡ f (g x) -- Haskell
+
Again, this gives us another way to get the right-most parameter on its own outside the body expression:
+ +f x = sqrt (1 / x)
+f x = sqrt ((1/) x) -- operator section
+f x = (sqrt . (1/)) x -- by the definition of composition
+f = sqrt . (1/) -- eta conversion
+
We have discussed point-free and tacit coding style earlier in these notes. In particular, eta-conversion works in Haskell the same as in lambda calculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s. + and for curried JavaScript functions. It is easy to do and usually declutters code of unnecessary arguments, e.g.:
+ +lessThan :: (Ord a) => a -> [a] -> [a]
+lessThan n aList = filter (<n) aList
+
The following is more concise, and once you are used to reading haskell type definitions, just as self evident:
+ +lessThan :: (Ord a) => a -> [a] -> [a]
+lessThan n = filter (<n)
+
But the above still has an argument (a point), n
. Can we go further?
It is possible to be more aggressive in refactoring code to achieve point-free styleA way of defining functions without mentioning their arguments.
+ by using the compose operatorRepresented as (.) in Haskell, it allows the composition of two functions, where the output of the second function is passed as the input to the first function.
+ (.)
:
(.) :: (b -> c) -> (a -> b) -> a -> c
+(f . g) x = f (g x)
+
To see how to use (.)
in lessThan
we need to refactor it to look like the right-hand side of the definition above, i.e. f (g x)
. For lessThan
, this takes a couple of steps, because the order we pass arguments to (<)
matters. Partially applying infix operators like (<n)
is called operator sectioningThe process of partially applying an infix operator in Haskell by specifying one of its arguments. For example, (+1) is a section of the addition operator with 1 as the second argument.
+. Placing n
after <
means that it is being passed as the second argument to the operator, which is inconvenient for eta-conversion. Observe that (<n)
is equivalent to (n>)
, so the following is equivalent to the definition above:
lessThan n = filter (n>)
+
Now we can use the non-infix form of (>):
+ +lessThan n = filter ((>) n)
+
And we see from our definition of compose, that if we were to replace filter by f
, (>)
by g
, and n
by x
, we would have exactly the definition of (.)
. Thus,
lessThan n = (filter . (>)) n
+
And now we can apply eta-conversion:
+ +lessThan = filter . (>)
+
Between operator sectioningThe process of partially applying an infix operator in Haskell by specifying one of its arguments. For example, (+1) is a section of the addition operator with 1 as the second argument.
+, the compose combinatorA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments.
+ (.)
, and eta-conversion it is possible to write many functions in point-free form. For example, the flip combinatorA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments.
+:
flip :: (a -> b -> c) -> b -> a -> c
+flip f a b = f b a
+
can also be useful in reversing the arguments of a function or operator in order to get them into a position such that they can be eta-reduced.
+ +In code written by experienced haskellers it is very common to see functions reduced to point-free form. Does it make code more readable? To experienced haskellers, many times yes. To novices, perhaps not. When to do it is a matter of preference. Experienced haskellers tend to prefer it, they will argue that it reduces functions like the example one above “to their essence”, removing the “unnecessary plumbing” of explicitly named variables. Whether you like it or not, it is worth being familiar with the tricks above, because you will undoubtedly see them used in practice. The other place where point-free styleA way of defining functions without mentioning their arguments. + is very useful is when you would otherwise need to use a lambda function.
+ +Some more (and deeper) discussion is available on the Haskell Wiki.
+ +g x y = x^2 + y
+
f a b c = (a+b)*c
+
Extension: Warning, this one scary +This is very non-assessable, and no one will ask anything harder then first two questions
+ +f a b = a*a + b*b
+
g x y = x^2 + y
+g x y = (+) (x^2) y -- operator sectioning
+g x = (+) (x^2) -- eta conversion
+g x = (+) ((^2) x) -- operator sectioning
+g x = ((+) . (^2)) x -- composition
+g = (+) . (^2) -- eta conversion
+
f a b c = (a+b)*c
+f a b c = (*) (a + b) c -- operator sectioning
+f a b = (*) (a + b) -- eta conversion
+f a b = (*) (((+) a) b) -- operator sectioning
+f a b = ((*) . ((+) a)) b -- composition
+f a = (*) . ((+) a) -- eta conversion
+f a = ((*) .) ((+) a)
+f a = (((*) . ) . (+)) a -- composition
+f = ((*) . ) . (+) -- eta conversion
+
Only look at this one if you are curious (very non-assessable, and no one will ask anything harder then first two questions)
+ +f a b = a*a + b*b
+f a b = (+) (a * a) (b * b)
+
Where do we go from here?
+We need a function which applies the *
function to the same argument b
+Lets invent one:
apply :: (b -> b -> c) -> b -> c
+apply f b = f b b
+
f a b = a*a + b*b
+f a b = (+) (a * a) (b * b)
+f a b = (+) (apply (*) a) (apply (*) b) -- using our apply function
+f a b = ((+) (apply (*) a)) ((apply (*)) b) -- this is in the form f (g x), where f == ((+) (apply (*) a)) and g == (apply (*))
+
+f a b = f (g b)
+ where
+ f = ((+) (apply (*) a))
+ g = (apply (*))
+
+f a b = (((+) (apply (*) a)) . (apply (*))) b -- apply function composition
+f a = ((+) (apply (*) a)) . (apply (*)) -- eta conversion
+f a = (. (apply (*))) ((+) (apply (*) a)) -- operator sectioning
+f a = (. (apply (*))) ((+) . (apply (*)) a) -- composition inside brackets ((+) (apply (*) a))
+f a = (. (apply (*))) . ((+) . (apply (*))) a -- composition
+f = (. (apply (*))) . ((+) . (apply (*))) -- eta conversion
+f = (. apply (*)) . (+) . apply (*) -- simplify brackets
+
We’ve been mapping over lists and arrays many times, first in JavaScript:
+ +console> [1,2,3].map(x=>x+1)
+[2,3,4]
+
Now in Haskell:
+ +GHCi> map (\i->i+1) [1,2,3]
+[2,3,4]
+
Or (eta-reduce the lambda to be point-free):
+ +GHCi> map (+1) [1,2,3]
+[2,3,4]
+
Here’s the implementation of map
for lists as it’s defined in the GHC standard library:
map :: (a -> b) -> [a] -> [b]
+map _ [] = []
+map f (x:xs) = f x : map f xs
+
It’s easy to generalise this pattern to any data structure that holds one or more values: mapping a function over a data structure creates a new data structure whose elements are the result of applying the function to the elements of the original data structure. We have seen examples of generalizing the idea of mapping previously, for example, mapping over a Tree
.
In Haskell this pattern is captured in a type classA type system construct in Haskell that defines a set of functions that can be applied to different types, allowing for polymorphic functions.
+ called Functor
, which defines a function called fmap
.
Prelude> :i Functor
+type Functor :: (* -> *) -> Constraint
+class Functor f where
+ fmap :: (a -> b) -> f a -> f b
+...
+instance Functor [] -- naturally lists are an instance
+instance Functor Maybe -- but this may surprise!
+... -- and some other instances we’ll talk about shortly
+
The first line says that an instances of the FunctorA type class in Haskell that represents types that can be mapped over. Instances of Functor must define the fmap function, which applies a function to every element in a structure.
+ typeclass f
must be over a type that has the kind (* -> *)
, that is, their constructors must be parameterised with a single type variable. After this, the class
definition specifies fmap
as a function that will be available to any instance of FunctorA type class in Haskell that represents types that can be mapped over. Instances of Functor must define the fmap function, which applies a function to every element in a structure.
+ and that f
is the type parameterA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+ for the constructor function, which again, takes one type parameterA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+, e.g. f a
as the input to fmap
, which returns an f b
. While, this may sound complex and abstract, the power of fmap
is just applying the idea of a map
function to any collection of item(s).
Naturally, lists have an instance:
+ +Prelude> :i []
+...
+instance Functor [] -- Defined in `GHC.Base'
+
We can actually look up GHC’s implementation of fmap
for lists and we see:
instance Functor [] where
+ fmap = map
+
fmap
is defined for other types we have seen, such as a Maybe
. Maybes
can be considered as list of 0 items (Nothing
) or 1 item (Just
), and therefore, naturally, we should be able to fmap
over a Maybe
.
instance Functor Maybe where
+ fmap _ Nothing = Nothing
+ fmap f (Just a) = Just (f a)
+
We can use fmap
to apply a function to a Maybe
value without needing to unpack it. The true power of fmap
lies in its ability to apply a function to value(s) within a context without requiring knowledge of how to extract those values from the context.
GHCi> fmap (+1) (Just 6)
+Just 7
+
This is such a common operation that there is an operator alias for fmap: <$>
GHCi> (+1) <$> (Just 6)
+Just 7
+
Which also works over lists:
+ +GHCi> (+1) <$> [1,2,3]
+[2,3,4]
+
Lists of Maybe
s frequently arise. For example, the mod
operation on integers (e.g. mod 3 2 == 1
) will throw an error if you pass 0 as the divisor:
> mod 3 0
+*** Exception: divide by zero
+
We might define a safe modulo function:
+ +safeMod :: Integral a => a-> a-> Maybe a
+safeMod _ 0 = Nothing
+safeMod numerator divisor = Just $ mod numerator divisor
+
This makes it safe to apply safeMod
to an arbitrary list of Integral
values:
> map (safeMod 3) [1,2,0,4]
+[Just 0,Just 1,Nothing,Just 3]
+
But how do we keep working with such a list of Maybe
s? We can map an fmap
over the list:
GHCi> map ((+1) <$>) [Just 0,Just 1,Nothing,Just 3]
+[Just 1,Just 2,Nothing,Just 4]
+
Or equivalently:
+ +GHCi> ((+1) <$>) <$> [Just 0,Just 1,Nothing,Just 3]
+[Just 1,Just 2,Nothing,Just 4]
+
In addition to lists and Maybe
s, a number of other built-in types have instances of Functor
:
GHCi> :i Functor
+...
+instance Functor (Either a) -- Defined in `Data.Either'
+instance Functor IO -- Defined in `GHC.Base'
+instance Functor ((->) r) -- Defined in `GHC.Base'
+instance Functor ((,) a) -- Defined in `GHC.Base'
+
The definition for functions (->)
might surprise:
instance Functor ((->) r) where
+ fmap = (.)
+
So the composition of functions f
and g
, f . g
, is equivalent to ‘mapping’ f
over g
, e.g. f <$> g
.
GHCi> f = (+1)
+GHCi> g = (*2)
+GHCi> (f.g) 3
+7
+GHCi> (f<$>g) 3
+7
+
We can formalise the definition of FunctorA type class in Haskell that represents types that can be mapped over. Instances of Functor must define the fmap function, which applies a function to every element in a structure. + with two laws:
+ +The law of identity
+ +∀ x: (id <$> x) ≡ x
+ +The law of composition
+ +∀ f, g, x: (f ∘ g <$> x) ≡ (f <$> (g <$> x))
+ +Note that these laws are not enforced by the compiler when you create your own instances of Functor
. You’ll need to test them for yourself. Following these laws guarantees that general code (e.g. algorithms) using fmap
will also work for your own instances of Functor
.
Let’s make a custom instance of Functor
for a simple binary tree type and check that the laws hold. Here’s a simple binary tree datatype:
data Tree a = Empty
+ | Leaf a
+ | Node (Tree a) a (Tree a)
+ deriving (Show)
+
Note that Leaf
is a bit redundant as we could also encode nodes with no children as Node Empty value Empty
—but that’s kind of ugly and makes showing our trees more verbose. Also, having both Leaf
and Empty
provides a nice parallel to Maybe
.
Here’s an example tree defined:
+ +tree = Node (Node (Leaf 1) 2 (Leaf 3)) 4 (Node (Leaf 5) 6 (Leaf 7))
+
And here’s a visualisation of the tree:
+ +Node 4
+ ├──Node 2
+ | ├──Leaf 1
+ | └──Leaf 3
+ └──Node 6
+ ├──Leaf 5
+ └──Leaf 7
+
+
+And here’s the instance of Functor
for Tree
that defines fmap
.
instance Functor Tree where
+ fmap :: (a -> b) -> Tree a -> Tree b
+ fmap _ Empty = Empty
+ fmap f (Leaf v) = Leaf $ f v
+ fmap f (Node l v r) = Node (fmap f l) (f v) (fmap f r)
+
Just as in the Maybe
instance above, we use pattern matchingA mechanism in functional programming languages to check a value against a pattern and to deconstruct data.
+ to define a case for each possible constructor in the ADT. The Empty
and Leaf
cases are very similar to Maybe
fmap
for Nothing
and Just
respectively, that is, for Empty
we just return another Empty
, for Leaf
we return a new Leaf
containing the application of f
to the value x
stored in the leaf. The fun one is Node
. As for Leaf
, fmap f
of a Node
returns a new Node
whose own value is the result of applying f
to the value stored in the input Node
, but the left and right children of the new node will be the recursive application of fmap f
to the children of the input node.
Now we’ll demonstrate (but not prove) that the two laws hold at least for our example tree:
+ +Law of Identity:
+ +> id <$> tree
+Node (Node (Leaf 1) 2 (Leaf 3)) 4 (Node (Leaf 5) 6 (Leaf 7))
+
Law of Composition:
+ +> (+1) <$> (*2) <$> tree
+Node (Node (Leaf 3) 5 (Leaf 7)) 9 (Node (Leaf 11) 13 (Leaf 15))
+> (+1).(*2) <$> tree
+Node (Node (Leaf 3) 5 (Leaf 7)) 9 (Node (Leaf 11) 13 (Leaf 15))
+
The typeclass Applicative
introduces a new operator <*>
(pronounced “apply”), which lets us apply functions inside a computational context.
ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ is a “subclass” of Functor
, meaning that an instance of Applicative
can be fmap
ed over, but ApplicativesA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ also declare (at least) two additional functions, pure
and (<*>)
(pronounced ‘apply’—but I like calling it “TIE Fighter”):
GHCi> :i Applicative
+class Functor f => Applicative (f :: * -> *) where
+ pure :: a -> f a
+ (<*>) :: f (a -> b) -> f a -> f b
+
As for Functor
all instances of the ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ type-class must satisfy certain laws, which again are not checked by the compiler.
+ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ instances must comply with these laws to produce the expected behaviour.
+You can read about the ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ Laws
+if you are interested, but they are a little more subtle than the basic FunctorA type class in Haskell that represents types that can be mapped over. Instances of Functor must define the fmap function, which applies a function to every element in a structure.
+ laws above, and it is
+not essential to understand them to be able to use <*>
in Haskell.
As for Functor
, many Base Haskell types are also Applicative
, e.g. []
, Maybe
, IO
and (->)
.
For example, a function inside a Maybe
can be applied to a value in a Maybe
.
GHCi> Just (+3) <*> Just 2
+Just 5
+
Or a list of functions [(+1),(+2)]
to things inside a similar context (e.g. a list [1,2,3]
).
> [(+1),(+2)] <*> [1,2,3]
+[2,3,4,3,4,5]
+
Note that lists definition of <*>
produces the Cartesian product of the two lists, that is, all the possible ways to apply the functions in the left list to the values in the right list. It is interesting to look at the source for the definition of Applicative
for lists on Hackage:
instance Applicative [] where
+ pure x = [x]
+ fs <*> xs = [f x | f <- fs, x <- xs] -- list comprehension
+
The definition of <*>
for lists uses a list comprehension. List comprehensions are a short-hand way to generate lists, using notation similar to mathematical “set builder notation”. The set builder notation here would be: {f(x) | f ∈ fs ∧ x ∈ xs}
. In English it means: “the set (Haskell list) of all functions in fs
applied to all values in xs
”.
A common use-case for ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>). + is applying a binary (two-parameter) function over two ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>). + values, e.g.:
+ +> pure (+) <*> Just 3 <*> Just 2
+Just 5
+
So:
+ +pure
puts the binary function into the applicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ (i.e. pure (+) :: Maybe (Num -> Num -> Num)
),<*>
ing this function inside over the first Maybe
value Just 3
achieves a partial applicationThe process of fixing a number of arguments to a function, producing another function of smaller arity.
+ of the function inside the Maybe
. This gives a unary function inside a Maybe
: i.e. Just (3+) :: Maybe (Num->Num)
.<*>
this function inside a Maybe
over the remaining Maybe
value.This is where the name “applicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+” comes from, i.e. Applicative
is a type over which a non-unary function may be applied. Note, that the following use of <$>
(infix fmap
) is equivalent and a little bit more concise:
> (+) <$> Just 3 <*> Just 2
+Just 5
+
This is also called “liftingThe process of applying a function to arguments that are within a context, such as a Functor or Applicative. +” a function over an ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>). +. Actually, it’s so common that ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>). + also defines dedicated functions for liftingThe process of applying a function to arguments that are within a context, such as a Functor or Applicative. + binary functions (in the GHC.Base module):
+ +> GHC.Base.liftA2 (+) (Just 3) (Just 2)
+Just 5
+
Here’s a little visual summary of ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>). + and liftingThe process of applying a function to arguments that are within a context, such as a Functor or Applicative. + (box metaphor inspired by adit.io):
+ +It’s also useful to lift binary data constructors over two ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>). + values, e.g. for tuples:
+ +> (,) <$> Just 3 <*> Just 2
+Just (3, 2)
+
We can equally well apply functions with more than two arguments over the correct number of values inside ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>). + contexts. +Recall our student data type:
+ +data Student = Student { id::Integer, name::String, mark::Int }
+
with a ternary constructor:
+ +Student::Integer -> String -> Int -> Student
+
Let’s say we want to create a student for a given id
but we need to look up the name and mark from tables, i.e. lists of key-value pairs:
+names::[(Integer,String)]
and marks::[(Integer,Int)]
. We’ll use the function lookup :: Eq a => a -> [(a, b)] -> Maybe b
, which will either succeed with Just
the result, or fail to find the value for a given key, and return Nothing
.
lookupStudent :: Integer -> Maybe Student
+lookupStudent sid = Student sid <$> lookup sid names <*> lookup sid marks
+
Lists are also instances of Applicative
. Given the following types:
data Suit = Spade|Club|Diamond|Heart
+ deriving (Eq,Ord,Enum,Bounded)
+
+instance Show Suit where
+ show Spade = "^" -- ♠ (closest I could come in ASCII was ^)
+ show Club = "&" -- ♣
+ show Diamond = "O" -- ♦
+ show Heart = "V" -- ♥
+
+data Rank = Two|Three|Four|Five|Six|Seven|Eight|Nine|Ten|Jack|Queen|King|Ace
+ deriving (Eq,Ord,Enum,Show,Bounded)
+
+data Card = Card Suit Rank
+ deriving (Eq, Ord, Show)
+
We can make one card using the Card constructor:
+ +GHCi> Card Spade Ace
+Card ^ Ace
+
Or, since both Suit
and Rank
derive Enum
, we can enumerate the full lists of Suit
s and Rank
s, and then lift the Card
operator over both lists to create a whole deck:
GHCi> Card <$> [Spade ..] <*> [Two ..]
+[Card ^ Two,Card ^ Three,Card ^ Four,Card ^ Five,Card ^ Six,Card ^ Seven,Card ^ Eight,Card ^ Nine,Card ^ Ten,Card ^ Jack,Card ^ Queen,Card ^ King,Card ^ Ace,Card & Two,Card & Three,Card & Four,Card & Five,Card & Six,Card & Seven,Card & Eight,Card & Nine,Card & Ten,Card & Jack,Card & Queen,Card & King,Card & Ace,Card O Two,Card O Three,Card O Four,Card O Five,Card O Six,Card O Seven,Card O Eight,Card O Nine,Card O Ten,Card O Jack,Card O Queen,Card O King,Card O Ace,Card V Two,Card V Three,Card V Four,Card V Five,Card V Six,Card V Seven,Card V Eight,Card V Nine,Card V Ten,Card V Jack,Card V Queen,Card V King,Card V Ace]
+
Show
for Rank
and Card
such that a deck of cards displays much more succinctly, e.g.:[^2,^3,^4,^5,^6,^7,^8,^9,^10,^J,^Q,^K,^A,&2,&3,&4,&5,&6,&7,&8,&9,&10,&J,&Q,&K,&A,O2,O3,O4,O5,O6,O7,O8,O9,O10,OJ,OQ,OK,OA,V2,V3,V4,V5,V6,V7,V8,V9,V10,VJ,VQ,VK,VA]
+
show
for Rank
a one-liner using zip
and lookup
.instance Show Suit where
+ show Spade = "^" -- Represents Spades
+ show Club = "&" -- Represents Clubs
+ show Diamond = "O" -- Represents Diamonds
+ show Heart = "V" -- Represents Hearts
+
+instance Show Rank where
+ show Two = "2"
+ show Three = "3"
+ show Four = "4"
+ show Five = "5"
+ show Six = "6"
+ show Seven = "7"
+ show Eight = "8"
+ show Nine = "9"
+ show Ten = "10"
+ show Jack = "J"
+ show Queen = "Q"
+ show King = "K"
+ show Ace = "A"
+
+instance Show Card where
+ show (Card s r) = show s ++ show r
+
To define the show method for Rank as a one-liner using zip
and lookup
, we can leverage these functions to directly map the Rank constructors to their respective string representations.
instance Show Rank where
+ show r = fromMaybe "" $ lookup r (zip [Two .. Ace] ["2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"])
+
g x -- apply function g to argument x
+ g $ x -- apply function g to argument x
+ g <$> f x -- apply function g to argument x which is inside Functor f
+ f g <*> f x -- apply function g in Applicative context f to argument x which is also inside f
+
We saw that functions (->)
are FunctorsA type class in Haskell that represents types that can be mapped over. Instances of Functor must define the fmap function, which applies a function to every element in a structure.
+, such that (<$>)=(.)
. There is also an instance of ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ for functions of input type r
. We’ll give the types of the essential functions for the instance:
instance Applicative ((->)r) where
+ pure :: a -> (r->a)
+ (<*>) :: (r -> (a -> b)) -> (r -> a) -> (r -> b)
+
This is very convenient for creating point-free implementations of functions which operate on their parameters more than once. For example, imagine our Student
type from above has additional fields with breakdowns of marks: e.g. exam
and nonExam
, requiring a function to compute the total mark:
totalMark :: Student -> Int
+totalMark s = exam s + nonExam s
+
Here’s the point-free version, taking advantage of the fact that exam
and nonExam
, both being functions of the same input type Student
, are both in the same ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ context:
totalMark = (+) <$> exam <*> nonExam
+
Or equivalently:
+ +totalMark = (+) . exam <*> nonExam
+
pure
and <*>
for Maybe
and for functions ((->)r)
.MaybesA built-in type in Haskell used to represent optional values, allowing functions to return either Just a value or Nothing to handle cases where no value is available. +
+ +First lets consider Maybe
. The type signature for pure
is:
pure :: a -> Maybe a
+
The idea behind pure
is to take the value of type a
and put it inside the context. So, we take the value x
and put it inside the Just
constructor.
pure :: a -> Maybe a
+pure x = Just x
+
A simple way to define this is to consider all possible options of the two parameters, and define the behaviour +by following the types
+ +(<*>) :: Maybe (a -> b) -> Maybe a -> Maybe b
+
(<*>) :: Maybe (a -> b) -> Maybe a -> Maybe b
+(<*>) (Just a) (Just b) = Just (a b) -- We can apply the function to the value, we have both!
+(<*>) Nothing (Just b) = Nothing -- Do not have the function, so all we can do is return Nothing
+(<*>) (Just a) Nothing = Nothing -- Do not have the value, so all we can do is return Nothing
+(<*>) Nothing Nothing = Nothing -- Do not have value or function, not much we can do here...
+
We observe that only one case returns a value, while all other cases, return Nothing
, so we can simplify our code using the wildcard _
when pattern matchingA mechanism in functional programming languages to check a value against a pattern and to deconstruct data.
+.
(<*>) :: Maybe (a -> b) -> Maybe a -> Maybe b
+(<*>) (Just a) (Just b) = Just (a b) -- We can apply the function to the value, we have both!
+(<*>) _ _ = Nothing -- All other cases, return Nothing
+
Functions
+ +The type definitions for the function type ((->)r)
is a bit more nuanced. When we write ((->) r)
, we are partially applying the ->
type constructor. The ->
type constructor takes two type arguments: an argument type and a return type. By supplying only the first argument r
, we get a type constructor that still needs one more type to become a complete type.
For a bit of intuition around we can make a function in instance of the applicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ instance, you can consider the context is an environment r. The pure
function for the reader applicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ takes a value and creates a function that ignores the environment and always returns that value. The <*>
function for the function instance combines two functions that depend on the same environment.
First, lets consider pure
.
pure :: a -> (((->)r) a)
+
This may look confusing, but if you replace ((->)r)
with Maybe
, you can see it is essentially the same. Similar to converting prefix functions to infix, we can do the same thing with the type operation here, therefore, this is equivalent to:
pure :: a -> (r -> a)
+
We can follow the types to write a definition for this:
+ +pure :: a -> (r -> a)
+pure a = \r -> a
+
This definition takes a single parameter a and returns a function. This function, when given any parameter r, will return the original parameter a. Since all functions are curried in Haskell, this is equivalent to:
+ +pure :: a -> (r -> a)
+pure a _ = -> a
+
The function pure
helps you create a function that, no matter what the second input is, will always return this first value, this is exactly the K-combinatorA combinator that takes two arguments and returns the first one.
+.
Reminder the definition for applicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>). + is:
+ +(<*>) :: (f (a -> b)) -> (f a) -> (f b)
+
For the function applicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+, our f
is ((->)r)
(<*>) :: (((->) r) (a -> b)) -> (((->) r) a) -> (((->) r) b)
+
Converting the type signature to use (->)
infix rather than prefix
(<*>) :: (r -> (a -> b)) -> (r -> a) -> (r -> b)
+
For the function body, our function takes two arguments and returns a function of type r -> b
.
We have to do some Lego to fit the variables together to get out the correct type b
.
(<*>) :: (r -> (a -> b)) -> (r -> a) -> (r -> b)
+(<*>) f g = \r -> (f r) (g r)
+
The function (<*>)
takes two functions, one of type r -> (a -> b)
and another of type r -> a
, and combines them to produce a new function of type r -> b
. It does this by applying both functions to the same input r
and then applying the result of the first function to the result of the second.
One neat function we can make out of this, we will call apply
passes one argument to a binary function twice which can be a useful trick.
apply :: (b -> b -> c) -> b -> c
+apply f b = (f <*> (\x -> x)) b
+
or more simply:
+ +apply :: (b -> b -> c) -> b -> c
+apply f = f <*> id
+
This will allow us to make more functions point-free
+ +square :: Num a => a => a
+square = a * a
+
square a = apply (*) a
+square = apply (*)
+
The AlternativeA type class in Haskell that extends Applicative, introducing the empty and <|> functions for representing computations that can fail or have multiple outcomes.
+ typeclass is another important typeclass in Haskell, which is closely related to the ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ typeclass. It introduces a set of operators and functions that are particularly useful when dealing with computations that can fail or have multiple possible outcomes. AlternativeA type class in Haskell that extends Applicative, introducing the empty and <|> functions for representing computations that can fail or have multiple outcomes.
+ is also considered a “subclass” of ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+, and it provides additional capabilities beyond what ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ offers. It introduces two main functions, empty
and <|>
(pronounced “alt” or “alternativeA type class in Haskell that extends Applicative, introducing the empty and <|> functions for representing computations that can fail or have multiple outcomes.
+”)
class Applicative f => Alternative (f :: * -> *) where
+ empty :: f a
+ (<|>) :: f a -> f a -> f a
+
empty
: This function represents a computation with no result or a failure. It serves as the identity element. For different data types that are instances of AlternativeA type class in Haskell that extends Applicative, introducing the empty and <|> functions for representing computations that can fail or have multiple outcomes.
+, empty
represents an empty container or a failed computation, depending on the context.
(<|>)
: The <|>
operator combines two computations, and it’s used to express alternativesA type class in Haskell that extends Applicative, introducing the empty and <|> functions for representing computations that can fail or have multiple outcomes.
+. It takes two computations of the same type and returns a computation that will produce a result from the first computation if it succeeds, or if it fails, it will produce a result from the second computation. This operator allows you to handle branching logic and alternativeA type class in Haskell that extends Applicative, introducing the empty and <|> functions for representing computations that can fail or have multiple outcomes.
+ paths in your code.
Like FunctorA type class in Haskell that represents types that can be mapped over. Instances of Functor must define the fmap function, which applies a function to every element in a structure.
+ and ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+, instances of the AlternativeA type class in Haskell that extends Applicative, introducing the empty and <|> functions for representing computations that can fail or have multiple outcomes.
+ typeclass must also adhere to specific laws, ensuring predictable behavior when working with alternativesA type class in Haskell that extends Applicative, introducing the empty and <|> functions for representing computations that can fail or have multiple outcomes.
+. Common instances of the AlternativeA type class in Haskell that extends Applicative, introducing the empty and <|> functions for representing computations that can fail or have multiple outcomes.
+ typeclass include MaybeA built-in type in Haskell used to represent optional values, allowing functions to return either Just a value or Nothing to handle cases where no value is available.
+ and lists ([]
). AlternativesA type class in Haskell that extends Applicative, introducing the empty and <|> functions for representing computations that can fail or have multiple outcomes.
+ are also very useful for ParsersA function or program that interprets structured input, often used to convert strings into data structures.
+, where we try to run first parserA function or program that interprets structured input, often used to convert strings into data structures.
+, if it fails, we run the second parserA function or program that interprets structured input, often used to convert strings into data structures.
+.
> Just 2 <|> Just 5
+Just 2
+
+> Nothing <|> Just 5
+Just 5
+
asum
with the type definition asum :: Alternative f => [f a] -> f a
. Write this function using foldr
A naive approach will be to use recursion (recursion is hard).
+ +asum :: Alternative f => [f a] -> f a
+asum [] = empty -- When we reach empty list, return the empty alternative
+asum (x:xs) = x <|> asum xs -- Otherwise recursively combine the first value and the rest of the list
+
However, you may recognize this pattern, of recursion to a base case, combining current value with an accumulated value.
+ +This is a classic example of a foldr
+ +Therefore we can write asum
simply as:
asum :: Alternative f => [f a] -> f a
+asum = foldr (<|>) empty
+
As we will discuss in more detail later, a parserA function or program that interprets structured input, often used to convert strings into data structures. + is a program which takes some structured input and does something with it. When we say “structured input” we typically mean something like a string that follows strict rules about its syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. +, like source code in a particular programming language, or a file format like JSON. A parserA function or program that interprets structured input, often used to convert strings into data structures. + for a given syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. + is a program which you run over some input and if the input is valid, it will do something sensible with it (like give us back some data), or fail: preferably in a way that we can handle gracefully.
+ +In Haskell, sophisticated parsersA function or program that interprets structured input, often used to convert strings into data structures.
+ are often constructed from simple functions which try to read a certain element of the expected input and either succeed in consuming that input, returning a tuple containing the rest of the input string and the resultant data, or they fail producing nothing. We’ve already seen one type which can be used to encode success or failure, namely Maybe
. Here’s the most trivial parserA function or program that interprets structured input, often used to convert strings into data structures.
+ function I can think of. It tries to take a character from the input stream and either succeeds consuming it or fails if it’s given an empty string:
-- >>> parseChar "abc"
+-- Just ("bc",'a')
+-- >>> parseChar ""
+-- Nothing
+parseChar :: String -> Maybe (String, Char)
+parseChar "" = Nothing
+parseChar (c:rest) = Just (rest, c)
+
And here’s one to parse Int
s off the input stream. It uses the reads
function from the PreludeThe default library loaded in Haskell that includes basic functions and operators.
+:
-- >>> parseInt "123+456"
+-- Just ("+456",123)
+-- >>> parseInt "xyz456"
+-- Nothing
+parseInt :: String -> Maybe (String, Int)
+parseInt s = case reads s of
+ [(x, rest)] -> Just (rest, x)
+ _ -> Nothing
+
So now we could combine these to try to build a little calculator language (OK, all it can do is add two integers, but you get the idea):
+ +-- >>> parsePlus "123+456"
+-- Just ("",579)
+parsePlus :: String -> Maybe (String, Int)
+parsePlus s =
+ case parseInt s of
+ Just (s', x) -> case parseChar s' of
+ Just (s'', '+') -> case parseInt s'' of
+ Just (s''',y) -> Just (s''',x+y)
+ Nothing -> Nothing
+ Nothing -> Nothing
+ Nothing -> Nothing
+
But that’s not very elegant and Haskell is all about elegant simplicity. So how can we use Haskell’s typeclass system to make parsersA function or program that interprets structured input, often used to convert strings into data structures.
+ that are more easily combined? We’ve seen how things that are instances of the Functor
and Applicative
typeclasses can be combined—so let’s make a type definition for parsersA function or program that interprets structured input, often used to convert strings into data structures.
+ and then make it an instance of Functor
and Applicative
. Here’s a generic type for parsersA function or program that interprets structured input, often used to convert strings into data structures.
+:
newtype Parser a = Parser (String -> Maybe (String, a))
+
We can now make concrete parsersA function or program that interprets structured input, often used to convert strings into data structures.
+ for Int
and Char
using our previous functions, which conveniently already had functions in the form (String -> Maybe (String, a))
:
char :: Parser Char
+char = Parser parseChar
+int :: Parser Int
+int = Parser parseInt
+
And here’s a simple generic function we can use to run these parsersA function or program that interprets structured input, often used to convert strings into data structures.
+. All this does, is extract the inner function p
from the Parser
constructor
-- >>> parse int "123+456"
+-- Just ("+456",123)
+parse :: Parser a -> String -> Maybe (String, a)
+parse (Parser p) = p
+
And a parserA function or program that interprets structured input, often used to convert strings into data structures. + which asserts the next character on the stream is the one we are expecting:
+ +is :: Char -> Parser Char
+is c = Parser $
+ \inputString -> case parse char inputString of
+ Just (rest, result) | result == c -> Just (rest, result)
+ _ -> Nothing
+
The is
function constructs a parserA function or program that interprets structured input, often used to convert strings into data structures.
+ that succeeds and consumes a character from the input string if it matches the specified character c, otherwise it fails.
-- >>> parse (is '+') "+456"
+-- Just ("456",'+')
+-- >>> parse (is '-') "+456"
+-- Nothing
+is :: Char -> Parser Char
+is c = Parser $ \inputString ->
+ case inputString of
+ [] -> Nothing
+ (x:xs) -> case x == c of
+ True -> Just (xs, x)
+ False -> Nothing
+
However, this is quickly becoming very deeply nested, lets use our previous char
parserA function or program that interprets structured input, often used to convert strings into data structures.
+ to ensure correct behaviour for the empty string, rather than duplicating that logic.
-- >>> parse (is '+') "+456"
+-- Just ("456",'+')
+-- >>> parse (is '-') "+456"
+-- Nothing
+is :: Char -> Parser Char
+is c = Parser $
+ \inputString -> case parse char inputString of
+ Just (rest, result)
+ | result == c -> Just (rest, result)
+ | otherwise = Nothing
+ _ -> Nothing
+
In this example, the otherwise = Nothing
guard is not needed, as our case
statement can handle that in the wildcard statement
is :: Char -> Parser Char
+is c = Parser $
+ \inputString -> case parse char inputString of
+ Just (rest, result) | result == c -> Just (rest, result)
+ _ -> Nothing
+
By making Parser
an instance of Functor
we will be able to map functions over the result of a parse, this is very useful! For example, consider the int
parserA function or program that interprets structured input, often used to convert strings into data structures.
+, which parses a string to an integer
, and if we want to apply a function to the result, such as adding 1 (+1)
, we can fmap this over the parserA function or program that interprets structured input, often used to convert strings into data structures.
+.
add1 :: Parser Int
+add1 = (+1) <$> int
+
We can now use this to parse a string “12”, and get the result 13. parse add1 "12"
will result in 13
The fmap
function for the Functor
instance of Parser
needs to apply the parserA function or program that interprets structured input, often used to convert strings into data structures.
+ to an input string and apply the given function to the result of the parse, i.e.:
instance Functor Parser where
+ fmap f (Parser p) = Parser $
+ \i -> case p i of
+ Just (rest, result) -> Just (rest, f result)
+ _ -> Nothing
+
That definition may be difficult to understand, on first look, but we take apply the parserA function or program that interprets structured input, often used to convert strings into data structures.
+ p
and apply the function f
to the result of the parsing, i.e., we apply the parserA function or program that interprets structured input, often used to convert strings into data structures.
+ p
and if it succeeds (returns a Just
) we apply the function f
to the result
.
However, we can take advantage of the fact that the Tuple
returned by the parse function is also an instance of Functor
to make the definition more succinct. That is, we are applying the function f
to the second item of a tuple—that is exactly what the fmap
for the Functor
instance of a Tuple
does! So we can rewrite to use the Tuple
fmap
, or rather its alias (<$>)
:
instance Functor Parser where
+ fmap f (Parser p) = Parser $
+ \i -> case p i of
+ Just (rest, result) -> Just (f <$> (rest, result))
+ _ -> Nothing
+
Carefully examining this, what we are doing is applying (f <$>)
if the result is a Just
, or ignoring if the result is Nothing
. This is exactly what the Maybe
instance of Functor
does, so we can fmap
over the Maybe
also:
instance Functor Parser where
+ fmap f (Parser p) = Parser (\i -> (f <$>) <$> p i )
+
Let’s try rearrange to make it point point-free, eliminating the lambda: +First, let’s add some brackets, to make the evaluation order more explicit.
+ +instance Functor Parser where
+ fmap f (Parser p) = Parser (\i -> ((f <$>) <$>) (p i))
+
This is now in the form (f . g) i)
where f
is equal to ((f <$>) <$>)
and g is equal to p
. Therefore:
instance Functor Parser where
+ fmap f (Parser p) = Parser (\i -> (((f <$>) <$>) . p) i)
+
And, if we eta-reduce:
+ +instance Functor Parser where
+ fmap f (Parser p) = Parser (((f <$>) <$>) . p)
+
The last thing we notice is that the Functor
instance for functions is defined as compose. Therefore, we have finally reached the end of our journey and can re-write this as follows.
-- >>> parse ((*2) <$> int) "123+456"
+-- Just ("+456",246)
+instance Functor Parser where
+ fmap f (Parser p) = Parser (((f <$>) <$>) <$> p)
+
The whacky triple-nested application of <$>
comes about because the result type a
in our Parser
type is nested inside a Tuple ((,a)
), nested inside a Maybe
, nested inside function (->r
). So now we can map (or fmap
, to be precise) a function over the value produced by a Parser
. For example:
> parse ((+1)<$>int) "1bc"
+Just ("bc",2)
+
So (+1)<$>int
creates a new Parser
which parses an int
from the input stream and adds one to the value parsed (if it succeeds). Behind the scenes, using the implementation above, the Parser
’s instance of FunctorA type class in Haskell that represents types that can be mapped over. Instances of Functor must define the fmap function, which applies a function to every element in a structure.
+ is effectively liftingThe process of applying a function to arguments that are within a context, such as a Functor or Applicative.
+ over three additional layers of nested types to reach the Int
value, i.e.:
Just as we have seen before, making our Parser
an instance of Applicative
is going to let us do nifty things like liftingThe process of applying a function to arguments that are within a context, such as a Functor or Applicative.
+ a binary function over the results of two Parser
s. Thus, instead of implementing all the messy logic of connecting two ParsersA function or program that interprets structured input, often used to convert strings into data structures.
+ to make plus
above, we’ll be able to lift (+)
over two Parser
s.
Now the definition for the Applicative
is going to stitch together all the messy bits of handling both the Just
and Nothing
cases of the Maybe
that we saw above in the definition of plus
, abstracting it out so that people implementing parsersA function or program that interprets structured input, often used to convert strings into data structures.
+ like plus
won’t have to:
instance Applicative Parser where
+ pure a = Parser (\b -> Just (b, a))
+
+ (Parser f) <*> (Parser g) = Parser $
+ \i -> case f i of -- note that this is just
+ Just (r1, p1) -> case g r1 of -- an abstraction of the
+ Just (r2, p2) -> Just (r2, p1 p2) -- logic we saw in `plus`
+ Nothing -> Nothing
+ Nothing -> Nothing
+
All that pure
does is put the given value on the right side of the tuple.
The key insight for this applicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ instance is that we first use f
(the parserA function or program that interprets structured input, often used to convert strings into data structures.
+ on the LHS of <*>
). This consumes input from i
giving back the remaining input in r1
. We then run the second parserA function or program that interprets structured input, often used to convert strings into data structures.
+ g
on the RHS of <*>
on r1
(the remaining input).
The main take-away message is that <*>
allows us to combine two parsersA function or program that interprets structured input, often used to convert strings into data structures.
+ in sequence, that is, we can run the first one and then the second one.
Let’s walk through a concrete example of this.
+ +> charIntPairParser = (,) <$> char <*> int
+> parse charIntPairParser "a12345b"
+Just ("b",('a',12345))
+
As both <$>
and <*>
have the same precedence, firstly (,) <$> char
will be evaluated, the result will be then applied to int
.
So how does (,) <$> char
work? Well, we parse a character and then make that character the first item of a tuple, therefore:
> charPairParser = (,) <$> char
+> :t charPairParser
+charPairParser :: Parser (b -> (Char, b))
+
So for the applicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ instance the LHS will be the charPairParser
and the RHS will be int
.
+That is, first step in applicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ parsing is to parse the input i
using the LHS parserA function or program that interprets structured input, often used to convert strings into data structures.
+, what we called here charPairParser
.
+This will match the Just (r1, p1)
case where it will be equal to Just ("12345b", ('a',))
. Therefore, r1
is equal to the unparsed portion of the input 12345b
and the result is a tuple partially applied ('a', )
.
We then run the second parserA function or program that interprets structured input, often used to convert strings into data structures.
+ int
on the remaining input "12345b"
. This will match the Just (r2, p2)
case where it will be equal to Just ("b", 12345)
, where r2
is equal to the remaining input "b"
and p2
is equal to "12345"
We then return Just (r2, p1 p2)
, which will evaluate to Just ("b", ('a',12345))
.
Using the <*>
operator we can make our calculator magnificently simple:
-- >>> parse plus "123+456"
+-- Just ("",579)
+plus :: Parser Int
+plus = (+) <$> int <*> (is '+' *> int)
+
Note that we make use of a different version of the applicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ operator here: *>
. Note also that we didn’t have to provide an implementation of *>
- rather, the typeclass system picks up a default implementation of this operator (and a bunch of other functions too) from the base definition of Applicative
. These default implementations are able to make use of the <*>
that we provided for our instance of Applicative
for Parser
.
Prelude> :t (*>)
+(*>) :: Applicative f => f a -> f b -> f b
+
So compared to <*>
which took a function inside the Applicative
as its first parameter which is applied to the value inside the Applicative
of its second parameter,
+the *>
carries through the effect of the first Applicative
, but doesn’t do anything else with the value. You can think of it as a simple chaining of effectful operations: “do the first effectful thing, then do the second effectful thing, but give back the result of the second thing only”.
In the context of the Parser
instance when we do things like (is '+' *> int)
, we try the is
. If it succeeds then we carry on and run the int
. But if the is
fails, execution is short circuited and we return Nothing
. There is also a flipped version of the operator which works the other way:
Prelude> :t (<*)
+(<*) :: Applicative f => f a -> f b -> f a
+
So we could have just as easily implement the plus
parserA function or program that interprets structured input, often used to convert strings into data structures.
+:
-- >>> parse plus "123+456"
+-- Just ("",579)
+plus :: Parser Int
+plus = (+) <$> int <* is '+' <*> int
+
Obviously, the above is not a fully featured parsing system. A real parserA function or program that interprets structured input, often used to convert strings into data structures.
+ would need to give us more information in the case of failure, so a Maybe
is not really a sufficiently rich type to package the result. Also, a real language would need to be able to handle alternativesA type class in Haskell that extends Applicative, introducing the empty and <|> functions for representing computations that can fail or have multiple outcomes.
+ - e.g. minus
or plus
, as well as expressions with an arbitrary number of terms. We will revisit all of these topics with a more feature rich set of parserA function or program that interprets structured input, often used to convert strings into data structures.
+ combinatorsA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments.
+ later.
We briefly introduced both (<*)
and (*>)
but lets deep dive in to why these functions are so useful. The full type definitions are:
(<*) :: Applicative f => f a -> f b -> f a
+(*>) :: Applicative f => f a -> f b -> f b
+
A more intuitive look at these would be:
+ +(<*)
: Executes two actions, but only returns the result of the first action.(*>)
: Executes two actions, but only returns the result of the second action.The key term here is action, we can consider the action of our parserA function or program that interprets structured input, often used to convert strings into data structures. + as doing the parsing
+ +A definition of (<*)
is
(<*) :: Applicative f => f a -> f b -> f a
+(<*) fa fb = liftA2 (\a b -> a) fa fb
+
Where liftA2 = f <$> a <*> b
Let’s relate this to our parsing, and how this executes the two actions. We will consider the example of parsing something in the form of “123+”, wanting to parse the number and ignore the “+”. The execution order will be changed around a little bit, hoping to provide some intuition in to these functions.
+ +So, we can use our “<*” to ignore the second action, i.e., int <* (is '+')
.
Plugging this in to liftA2 definition:
+ +liftA2 (\a _ -> a) int (is '+')
+(\a b -> a) <$> int <*> (is '+') -- from the liftA2 definition
+((\a b -> a) <$> int) <*> (is '+') -- we will complete the functor operator first
+
Recall the definition of Functor
for parsing:
instance Functor Parser where
+ fmap f (Parser p) = Parser $
+ \i -> case p i of
+ Just (rest, result) -> Just (rest, f result)
+ _ -> Nothing
+
So, in this scenario our function f
is (\a b -> a) and our (Parser p)
is the int
parserA function or program that interprets structured input, often used to convert strings into data structures.
+. i
is the input string “123+”.
Therefore, (\a b -> a) <$> int
will result in Just ("+", (\a b -> a) 123)
Now, we want need to consider the second half, which is
+ +Just ("+", (\a b -> a) 123)
being applied to (is '+')
The applicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ definition says to apply the second parserA function or program that interprets structured input, often used to convert strings into data structures.
+ is '+'
to the remaining input, and apply the result to the function inside the RHS of the tuple.
Therefore, after applying this parserA function or program that interprets structured input, often used to convert strings into data structures.
+, it will result in: Just ("", (\a b -> a) 123 "+")
. Finally, we will apply the function call to ignore the second value, finally resulting in: Just ("", 123)
. But the key point, is we still executed the is '+'
but we ignored the value. That is the beauty of using our <*
and *>
to ignore results, while still executing actions
Operator Sectioning: The process of partially applying an infix operator in Haskell by specifying one of its arguments. For example, (+1) is a section of the addition operator with 1 as the second argument.
+ +Compose Operator: Represented as (.) in Haskell, it allows the composition of two functions, where the output of the second function is passed as the input to the first function.
+ +Point-Free Code: A style of defining functions without mentioning their arguments explicitly. This often involves the use of function composition and other combinators.
+ +Functor: A type class in Haskell that represents types that can be mapped over. Instances of Functor must define the fmap function, which applies a function to every element in a structure.
+ +Applicative: A type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ +Lifting: The process of applying a function to arguments that are within a context, such as a Functor or Applicative.
+ +Alternative: A type class in Haskell that extends Applicative, introducing the empty and <|> functions for representing computations that can fail or have multiple outcomes.
+ +Parser: A function or program that interprets structured input, often used to convert strings into data structures.
+ +Parser Combinator: A higher-order function that takes parsers as input and returns a new parser as output. Parser combinators are used to build complex parsers from simpler ones.
+ ++ + + 42 + + min read
+In this chapter we will meet some more typeclasses that abstract common coding patterns for dealing with data.
+ +foldl
and foldr
for left and right folds respectively.Monoid
values trivial to fold
Recall the “reduce
” function that is a member of JavaScript’s Array
type, and which we implemented ourselves for linked and cons lists, was a way to generalise loops over enumerable types.
+In Haskell, this concept is once again generalised with a typeclass called Foldable
– the class of things which can be “folded” over to produce a single value.
+We will come back to the Foldable
typeclass, but first let’s limit our conversation to the familiar Foldable
instance, basic lists.
+Although in JavaScript reduce
always associates elements from left to right, Haskell’s Foldable
typeclass offers both foldl
(which folds left-to-right) and foldr
(which folds right-to-left):
Prelude> :t foldl
+foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b
+
+Prelude> :t foldr
+foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b
+
In the following the examples the Foldable t
instance is a list. Here’s how we right-fold over a list to sum its elements:
While the lambda above makes it explicit which parameter is the accumulator and which is the list element, this is a classic example where point-free coding style makes this expression very succinct:
+ +Prelude> foldr (+) 0 [5,8,3,1,7,6,2]
+
++ +32
+
Here’s a left fold with a picture of the fold:
+ +Note that since the (+)
operator is associative—a+(b+c) = (a+b)+c—foldr
and foldl
return the same result. For functions that are not associative, however, this is not necessarily the case.
(-)
folded over [1,2,3,4]
with initial value 0
.foldr (:) []
applied to any list?map
using foldr
. foldr (-) 0 [1,2,3,4]
+ = 1 - (2 - (3 - (4 - 0)))
+ = 1 - (2 - (3 - 4))
+ = 1 - (2 - (-1))
+ = 1 - 3
+ = -2
+
The left fold processes the list from the left (beginning) to the right (end). The result of each application of the function is passed as the accumulator to the next application.
+ + foldl (-) 0 [1,2,3,4]
+ = (((0 - 1) - 2) - 3) - 4
+ = ((-1 - 2) - 3) - 4
+ = (-3 - 3) - 4
+ = -6 - 4
+ = -10
+
(:)
[]
applied to any list essentially reconstructs the list foldr (:) [] [1, 2, 3, 4]
+ = 1 : (2 : (3 : (4 : [])))
+
(:)
does not change the list, but reconstructs it, therefore, to implement map
, we just apply the function f
to the list as we go. map :: (a -> b) -> [a] -> [b]
+ map f = foldr (\x acc -> f x : acc) []
+
Or, by making the lambda function point-free
+ + map :: (a -> b) -> [a] -> [b]
+ map f = foldr ((:) . f) []
+
In the example fold above, we provide the (+)
function to tell foldl
how to aggregate elements of the list. There is also a typeclass for things that are “automatically aggregatable” or “concatenatable” called Monoid
which declares a general function for mappend
combining two Monoid
s into one, a mempty
value such that any MonoidA type class for types that have an associative binary operation (mappend) and an identity element (mempty). Instances of Monoid can be concatenated using mconcat.
+ mappend
‘ed with mempty
is itself, and a concatenation function for lists of Monoid
called mconcat
.
Prelude> :i Monoid
+class Semigroup a => Monoid a where
+ mempty :: a
+ mappend :: a -> a -> a
+ mconcat :: [a] -> a
+ {-# MINIMAL mempty #-}
+...
+
In the Data.Monoid
library there are some interesting instances of MonoidA type class for types that have an associative binary operation (mappend) and an identity element (mempty). Instances of Monoid can be concatenated using mconcat.
+. For example Sum
is an instance of MonoidA type class for types that have an associative binary operation (mappend) and an identity element (mempty). Instances of Monoid can be concatenated using mconcat.
+ which wraps a Num
, such that lists of Sum
can be mconcat
ed:
Prelude> import Data.Monoid
+Data.Monoid> mconcat $ Sum <$> [5,8,3,1,7,6,2]
+
++ +Sum {getSum = 32}
+
So a sum is a data type with an accessor function getSum that we can use to get back the value:
+ +Prelude Data.Monoid> getSum $ mconcat $ Sum <$> [5,8,3,1,7,6,2]
+
++ +32
+
We make a data type aggregatable by instancing Monoid
and providing definitions for the functions mappend
and mempty
. For Sum
these will be (+)
and 0
respectively.
+Lists are also themselves Monoidal, with mappend
defined as an alias for list concatenation (++)
, and mempty as []
. Thus, we can:
Prelude Data.Monoid> mconcat [[1,2],[3,4],[5,6]]
+[1,2,3,4,5,6]
+
Which has a simple alias concat
defined in the PreludeThe default library loaded in Haskell that includes basic functions and operators.
+:
Prelude> concat [[1,2],[3,4],[5,6]]
+[1,2,3,4,5,6]
+
There is also an operator for mappend
called (<>)
, such the following are equivalent:
Data.Monoid> mappend (Sum 1) (Sum 2)
+Sum {getSum = 3}
+
+Data.Monoid> (Sum 1) <> (Sum 2)
+Sum {getSum = 3}
+
And for lists (and String
) we have:
> mappend [1,2] [3,4]
+[1,2,3,4]
+
+> [1,2] <> [3,4]
+[1,2,3,4]
+
+> [1,2] ++ [3,4]
+[1,2,3,4]
+
So now we’ve already been introduced to foldl
and foldr
for lists, and we’ve also seen the Monoid
typeclass, let’s take a look at the general class of things that are Foldable
.
+As always, your best friend for exploring a new typeclass in Haskell is GHCi’s :i
command:
Prelude> :i Foldable
+class Foldable (t :: * -> *) where
+ foldr :: (a -> b -> b) -> b -> t a -> b -- as described previously, but notice foldr and foldl
+ foldl :: (b -> a -> b) -> b -> t a -> b -- are for any Foldable t, not only lists
+ length :: t a -> Int -- number of items stored in the Foldable
+ null :: t a -> Bool -- True if empty
+ elem :: Eq a => a -> t a -> Bool -- True if the a is an element of the t of a
+ maximum :: Ord a => t a -> a -- biggest element in the Foldable
+ minimum :: Ord a => t a -> a -- smallest element
+ sum :: Num a => t a -> a -- compute the sum of a Foldable of Num
+ product :: Num a => t a -> a -- compute the product of a Foldable of Num
+ Data.Foldable.fold :: Monoid m => t m -> m -- if the elements of t are Monoids then we don’t need an operator to aggregate them
+ foldMap :: Monoid m => (a -> m) -> t a -> m -- uses the specified function to convert elements to Monoid and then folds them
+ Data.Foldable.toList :: t a -> [a] -- convert any Foldable things to a list
+ {-# MINIMAL foldMap | foldr #-}
+ -- Defined in `Data.Foldable'
+instance Foldable [] -- Defined in `Data.Foldable'
+instance Foldable Maybe -- Defined in `Data.Foldable'
+instance Foldable (Either a) -- Defined in `Data.Foldable'
+instance Foldable ((,) a) -- Defined in `Data.Foldable'
+
Note that I’ve reordered the list of functions to the order we want to discuss them, removed a few things we’re not interested in at the moment and the comments are mine.
+However, once you get used to reading types the :info
for this class is pretty self explanatory. Most of these functions are also familiar from their use with lists. The surprise (OK, not really) is that lots of other things can be Foldable
as well.
Prelude> foldr (-) 1 (Just 3)
+2
+Prelude> foldl (-) 1 (Just 3)
+-2
+Prelude> foldr (+) 1 (Nothing)
+1
+Prelude> length (Just 3)
+1
+Prelude> length Nothing
+0
+-- etc
+
If we import the Data.FoldableA type class for data structures that can be folded (reduced) to a single value. It includes functions like foldr, foldl, length, null, elem, maximum, minimum, sum, product, and foldMap.
+ namespace we also get fold
and foldMap
, which we can use with Monoid
types which know how to aggregate themselves (with mappend
):
Prelude> import Data.Foldable
+
+Prelude Data.Foldable> fold [[1,2],[3,4]] -- since lists are also Monoids
+[1,2,3,4]
+
The fun really starts though now that we can make new Foldable
things:
data Tree a = Empty
+ | Leaf a
+ | Node (Tree a) a (Tree a)
+ deriving (Show)
+
+tree = Node (Node (Leaf 1) 2 (Leaf 3)) 4 (Node (Leaf 5) 6 (Leaf 7))
+
Which produces a tree with this structure:
+ +We make this type of binary tree an instance of foldableA type class for data structures that can be folded (reduced) to a single value. It includes functions like foldr, foldl, length, null, elem, maximum, minimum, sum, product, and foldMap.
+ by implementing either of the minimum defining functions, foldMap
or foldr
:
instance Foldable Tree where
+ foldMap :: Monoid m => (a -> m) -> Tree a -> m
+ foldMap _ Empty = mempty
+ foldMap f (Leaf x) = f x
+ foldMap f (Node l x r) = foldMap f l <> f x <> foldMap f r
+
+> length tree
+7
+> foldr (:) [] tree
+[1,2,3,4,5,6,7]
+
We can use foldMap
to map the values stored in the tree to an instance of Monoid
and then concatenate these Monoid
s. For example, we could map and concatenate them as a Sum
:
> getSum $ foldMap Sum tree
+28
+
Or we can compute the same conversion to a list as the above foldr
, by providing foldMap
with a function that places the values into singleton lists, e.g.:
> (:[]) 1 -- cons 1 with an empty list, same as 1:[]
+[1]
+
Since list is an instance of MonoidA type class for types that have an associative binary operation (mappend) and an identity element (mempty). Instances of Monoid can be concatenated using mconcat.
+, foldMap
will concatenate these singleton lists together:
> foldMap (:[]) tree
+[1,2,3,4,5,6,7]
+
Foldable
for Tree
in terms of foldr
instead of foldMap
.instance Foldable Tree where
+ foldr :: (a -> b -> b) -> b -> Tree a -> b
+ foldr _ z Empty = z -- base case, return accumulator
+ foldr f z (Leaf x) = f x z -- when we see a leaf, combine accumulator and leaf
+ foldr f z (Node l x r) = foldr f (f x (foldr f z r)) l -- fold over right first, then over left
+
Traversable
extends both Foldable
and Functor
, in a typeclass for things that we can traverse
a function with an Applicative
effect over. Here’s a sneak peak of what this lets us do:
Prelude> traverse putStrLn ["tim","was","here"]
+tim
+was
+here
+[(),(),()]
+
The first three lines are the strings printed to the terminal (the side effect). The result reported by GHCi is a list [(),(),()]
as discussed below.
Here, as usual, is what GHCi :i
tells us about the TraversableA type class for data structures that can be traversed, applying a function with an Applicative effect to each element. It extends both Foldable and Functor and includes functions like traverse and sequenceA.
+ type classA type system construct in Haskell that defines a set of functions that can be applied to different types, allowing for polymorphic functions.
+:
Prelude> :i Traversable
+class (Functor t, Foldable t) => Traversable (t :: * -> *) where
+ traverse :: Applicative f => (a -> f b) -> t a -> f (t b)
+ sequenceA :: Applicative f => t (f a) -> f (t a)
+ ... -- some other functions
+ {-# MINIMAL traverse | sequenceA #-}
+ -- Defined in `Data.Traversable'
+instance Traversable [] -- Defined in `Data.Traversable'
+instance Traversable Maybe -- Defined in `Data.Traversable'
+instance Traversable (Either a) -- Defined in `Data.Traversable'
+instance Traversable ((,) a) -- Defined in `Data.Traversable'
+
The following map shows how all of these typeclasses are starting to come together to offer some real power:
+ +So what does the traverse function do? By way of example, remember our safe modulo function this we used to experiment with FunctorA type class in Haskell that represents types that can be mapped over. Instances of Functor must define the fmap function, which applies a function to every element in a structure. +:
+ +safeMod :: Integral a => a-> a-> Maybe a
+safeMod _ 0 = Nothing
+safeMod numerator divisor = Just $ mod numerator divisor
+
It lets us map over a list of numbers without throwing divide-by-zero exceptions:
+ +> map (safeMod 3) [1,2,0,2]
+[Just 0,Just 1,Nothing,Just 1]
+
But what if 0
s in the list really are indicative of disaster so that we should bail rather than proceeding? The traverse
function of the Traversable
type-class gives us this kind of “all or nothing” capability:
> traverse (safeMod 3) [1,2,0,2]
+Nothing
+> traverse (safeMod 3) [1,2,2]
+Just [0,1,1]
+
So map
ping a function with an Applicative
effect over the values in a list gives us back a list with each of those values wrapped in the effect. However, traverse
ing such a function over a list gives us back the list of unwrapped values, with the whole list wrapped in the effect.
Traverse applies a function with a result in an Applicative
context (i.e. an ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ effect) to the contents of a Traversable
thing.
Prelude> :t traverse
+traverse
+ :: (Applicative f, Traversable t) => (a -> f b) -> t a -> f (t b)
+
What are some other functions with ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>). + effects? Lots! E.g.:
+ +Just :: a -> Maybe a
(take 5 $ repeat 1) :: Num a => [a]
print :: Show a => a -> IO ()
The print
function converts values to strings (using show if available from an instance of Show
) and sends them to standard-out. The print
function wraps this effect (there is an effect on the state of the console) in an IO
computational context:
Prelude> :t print
+print :: Show a => a -> IO ()
+
The ()
is like void
in TypeScript—it’s a type with exactly one value ()
, and hence is called “UnitA type with exactly one value, (), used to indicate the absence of meaningful return value, similar to void in other languages.
+”. There is no return value from print
, only the IO
effect, and hence the return type is ()
. IO
is also an instance of Applicative
. This means we can use traverse
to print out the contents of a list:
Prelude> traverse print [1,2,3]
+1
+2
+3
+[(),(),()]
+
Here 1,2,3
are printed to the console each on their own line (which is print
’s IO effect), and [(),(),()]
is the return value reported by GHCi—a list of UnitA type with exactly one value, (), used to indicate the absence of meaningful return value, similar to void in other languages.
+.
Prelude> :t traverse print [1,2,3]
+traverse print [1,2,3] :: IO [()]
+
When we ran this at the REPL, GHCi consumed the IO
effect (because it runs all commands inside the IO Monad
). However, inside a pure functionA function that always produces the same output for the same input and has no side effects.
+ there is no easy way to get rid of this IO
return type—which protects you from creating IO
effects unintentionally.
A related function defined in Traversable
is sequenceA
which allows us to convert directly from TraversablesA type class for data structures that can be traversed, applying a function with an Applicative effect to each element. It extends both Foldable and Functor and includes functions like traverse and sequenceA.
+ of ApplicativesA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+, to ApplicativesA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ of TraversablesA type class for data structures that can be traversed, applying a function with an Applicative effect to each element. It extends both Foldable and Functor and includes functions like traverse and sequenceA.
+:
> :t sequenceA
+sequenceA :: (Applicative f, Traversable t) => t (f a) -> f (t a)
+Prelude> sequenceA [Just 0,Just 1,Just 1]
+Just [0,1,1]
+Prelude> sequenceA [Just 0,Just 1,Nothing,Just 1]
+Nothing
+
The default sequenceA
is defined very simply in terms of traverse
(recall id
is just \x->x
):
sequenceA = traverse id
+
A bit more fun with sequenceA
, a list of functions:
> :t [(+3),(*2),(+6)]
+[(+3),(*2),(+6)] :: Num a => [a -> a]
+
is also a list of Applicative
, because function (->)r
is an instance of Applicative
. Therefore, we can apply sequenceA
to a list of functions to make a single function that applies every function in the list to a given value and return a list of the results:
> :t sequenceA [(+3),(*2),(+6)]
+sequenceA [(+3),(*2),(+6)] :: Num a => a -> [a]
+> sequenceA [(+3),(*2),(+6)] 2
+[5,4,8]
+
To create our own instance of Traversable
we need to implement fmap
to make it a Functor
and then either foldMap
or foldr
to make it Foldable
and finally, either traverse
or sequenceA
. So for our Tree
type above, which we already made Foldable
we add:
instance Functor Tree where
+ fmap :: (a -> b) -> Tree a -> Tree b
+ fmap _ Empty = Empty
+ fmap f (Leaf x) = Leaf $ f x
+ fmap f (Node l v r) = Node (fmap f l) (f v) (fmap f r)
+
+instance Traversable Tree where
+ traverse :: Applicative f => (a -> f b) -> Tree a -> f (Tree b)
+ traverse _ Empty = pure Empty
+ traverse f (Leaf a) = Leaf <$> f a
+ traverse f (Node l x r) = Node <$> traverse f l <*> f x <*> traverse f r
+
So now we can traverse a function with an Applicative
effect over the tree:
Prelude> traverse print tree
+1
+2
+3
+4
+5
+6
+7
+
And of course, we can sequence a Tree
of Maybe
s into a Maybe Tree
:
> treeOfMaybes = Just <$> tree -- a tree of Maybes
+> treeOfMaybes
+Node (Node (Leaf (Just 1)) (Just 2) (Leaf (Just 3))) (Just 4) (Node (Leaf (Just 5)) (Just 6) (Leaf (Just 7)))
+> sequenceA treeOfMaybes
+Just (Node (Node (Leaf 1) 2 (Leaf 3)) 4 (Node (Leaf 5) 6 (Leaf 7)))
+
Thus far we have seen a variety of functions for applying functions in and over different contexts. It is useful to note the similarities between these, and recognise that they are all doing conceptually the same thing, i.e. function application. The difference is the in the type of context. The simplest function for applying functions is the ($) operator, with just a function (no context), applied directly to a value. Then fmap
, just a function, mapped over a FunctorA type class in Haskell that represents types that can be mapped over. Instances of Functor must define the fmap function, which applies a function to every element in a structure.
+ context/container. Then ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ (function also in the context). Then, most recently traverse
: the function produces a result in an ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ context, applied (traversed) over some data structure, and the resulting data structure returned in an ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ context. Below, I line up all the types so that the similarities and differences are as clear as possible. It’s worth making sure at this stage that you can read such type signatures, as they really do summarise everything that we have discussed.
($) :: (a -> b) -> a -> b
+(<$>) :: Functor f => (a -> b) -> f a -> f b
+(<*>) :: Applicative f => f (a -> b) -> f a -> f b
+traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)
+
What if we want to parse an exact match for a given string, for example, a token in a programming language like the word function
. Or, to look for a polite greeting at the start of an email before deciding whether to respond, such as “hello”.
> parse (string "hello") "hello world"
+Just (" world", "hello")
+
+> parse (string "hello") "world, hello"
+Nothing
+
So the string “hello” is the prototype for the expected input. How would we do this?
+ +Our parserA function or program that interprets structured input, often used to convert strings into data structures. + would have to process characters from the input stream and check if each successive character is the one expected from the prototype. +If it is the correct character, we would cons it to our result and than parse the next character.
+ +This can have a recursive solution
+ +string [] = pure ""
+string (x:xs) = liftA2 (:) (is x) (string xs)
+
We parse the first character, x
, then recursively parse the rest of the string. We lift the (:)
operator in to the parserA function or program that interprets structured input, often used to convert strings into data structures.
+ context to combine our results in to a single list. This can also be written using a foldr
to parse all the characters while checking with the is
parserA function or program that interprets structured input, often used to convert strings into data structures.
+.
string l = foldr (\c acc -> liftA2 (:) (is c) acc) (pure "") l
+
Remembering liftA2
is equivalent to f <$> a <*> b
.
Our <*>
will allow for the sequencing of the applicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ effect, so this will sequentially parse all characters, making sure they are correct.
+As soon as one applicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ parserA function or program that interprets structured input, often used to convert strings into data structures.
+ fails, the result of the parsing will fail.
This could also be written as:
+ +string l = foldr cons (pure []) l
+ where
+ cons c acc = liftA2 (:) (is c) acc
+
But the title of this section was traverse?
+ +Well, lets consider how we would define a list as an instance of the traversableA type class for data structures that can be traversed, applying a function with an Applicative effect to each element. It extends both Foldable and Functor and includes functions like traverse and sequenceA. + operator. The traverse function is defined for lists exactly as follows:
+ +instance Traversable [] where
+ traverse :: Applicative f => (a -> f b) -> [a] -> f [b]
+ traverse f = foldr cons (pure [])
+ where cons x ys = liftA2 (:) (f x) ys
+
This is almost exactly the definition of our string parserA function or program that interprets structured input, often used to convert strings into data structures.
+ using foldr
but the function f
is exactly the is
ParserA function or program that interprets structured input, often used to convert strings into data structures.
+.
Therefore, we can write string = traverse is
Let’s break down how the string
parserA function or program that interprets structured input, often used to convert strings into data structures.
+ using traverse
and is
works in terms of types:
string :: String -> Parser String
+string = traverse is
+
traverse
is a higher-order functionA function that takes other functions as arguments or returns a function as its result.
+ with the following type:
traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)
+
t
is a traversableA type class for data structures that can be traversed, applying a function with an Applicative effect to each element. It extends both Foldable and Functor and includes functions like traverse and sequenceA.
+ data structure, which in our case is a String
(since String
is a list of characters).
+a
is the element type of the traversableA type class for data structures that can be traversed, applying a function with an Applicative effect to each element. It extends both Foldable and Functor and includes functions like traverse and sequenceA.
+ structure, which is Char
(the individual characters in the String
).
+f
is an applicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ functorA type class in Haskell that represents types that can be mapped over. Instances of Functor must define the fmap function, which applies a function to every element in a structure.
+, which is the Parser
type in our case.
+The function (a -> f b)
is the parserA function or program that interprets structured input, often used to convert strings into data structures.
+ for a single character. In our case, it’s the is
parserA function or program that interprets structured input, often used to convert strings into data structures.
+.
So, we will apply the is
function to each element in the the traversableA type class for data structures that can be traversed, applying a function with an Applicative effect to each element. It extends both Foldable and Functor and includes functions like traverse and sequenceA.
+ t
(the list) and store collect the result in to a Parser [Char]
.
Therefore, traverse is
is of type Parser String
, which is a parserA function or program that interprets structured input, often used to convert strings into data structures.
+ that attempts to parse the entire String and returns it as a result.
Can we also write this using sequenceA
?
string :: String -> Parser String
+string str = sequenceA (map is str)
+
Or in point-free form
+ +string :: String -> Parser String
+string = sequenceA . map is
+
map is str
maps the is parserA function or program that interprets structured input, often used to convert strings into data structures.
+ over each character in the input string str
. This produces a list of parsersA function or program that interprets structured input, often used to convert strings into data structures.
+, where each parserA function or program that interprets structured input, often used to convert strings into data structures.
+ checks if the corresponding character in the input matches the character in the target string.
sequenceA
is then used to turn the list of parsersA function or program that interprets structured input, often used to convert strings into data structures.
+ into a single parserA function or program that interprets structured input, often used to convert strings into data structures.
+. This function applies each parserA function or program that interprets structured input, often used to convert strings into data structures.
+ to the input string and collects the results. If all character parsersA function or program that interprets structured input, often used to convert strings into data structures.
+ succeed, it returns a list of characters; otherwise, it returns Nothing.
In fact an equivalent definition of traverse can be written using the sequenceA
as follows:
traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)
+traverse f l = sequenceA (f <$> l)
+
sequenceA
over a list? (without using traverse)Maybe
data type an instance of traversableA type class for data structures that can be traversed, applying a function with an Applicative effect to each element. It extends both Foldable and Functor and includes functions like traverse and sequenceA.
+?sequenceA
takes a list of applicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ actions and returns an applicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ action that returns a list of results. For lists, this means that sequenceA
should combine a list of applicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ actions into a single applicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ action that returns a list of results.
sequenceA :: (Applicative f) => [f a] -> f [a]
+sequenceA [] = pure []
+sequenceA (x:xs) = (:) <$> x <*> sequenceA xs
+
For Maybe
, the definition of traverse function is traverse :: (Applicative f) => (a -> f b) -> Maybe a -> f (Maybe b)
instance Traversable Maybe where
+ traverse :: (Applicative f) => (a -> f b) -> Maybe a -> f (Maybe b)
+ traverse _ Nothing = pure Nothing
+ traverse f (Just x) = Just <$> f x
+
Nothing
, we return pure Nothing
, which is an applicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ action that produces Nothing
.Just x
, we apply f
to x
and then wrap the result with Just
.We can also parse a tree by traversing a parserA function or program that interprets structured input, often used to convert strings into data structures.
+ over it, the same way we parsed string
by traversing a list of Char
!
Recall from earlier in this section:
+ +data Tree a = Empty
+ | Leaf a
+ | Node (Tree a) a (Tree a)
+ deriving (Show)
+
+instance Traversable Tree where
+ -- traverse :: Applicative f => (a -> f b) -> Tree a -> f (Tree b)
+ traverse _ Empty = pure Empty
+ traverse f (Leaf a) = Leaf <$> f a
+ traverse f (Node l x r) = Node <$> traverse f l <*> f x <*> traverse f r
+
We can write a similar definition for parsing an exact tree compared to parsing a string!
+ +We will consider a Value which is either an integer, or an operator which can combine integers. We will assume the only possible combination operator is +
to avoid complexities with ordering expressions.
data Value = Value Int | BinaryPlus
+ deriving (Show)
+
We can generalize the is
parserA function or program that interprets structured input, often used to convert strings into data structures.
+ to satisfy
, which will run a given parserA function or program that interprets structured input, often used to convert strings into data structures.
+ p
, and make sure the result satisfies a boolean condition.
satisfy :: Parser a -> (a -> Bool) -> Parser a
+satisfy p f = Parser $ \i -> case parse p i of
+ Just (r, v)
+ | f v -> Just (r, v)
+ _ -> Nothing
+
From this satisfy, we will use traverse to ensure our string exactly matches a wanted expression Tree.
+ +isValue :: Value -> Parser Value
+isValue (Value v) = Value <$> satisfy int (==v)
+isValue BinaryPlus = BinaryPlus <$ satisfy char (=='+')
+
+stringTree :: Tree Value -> Parser (Tree Value)
+stringTree = traverse isValue
+
+sampleTree :: Tree Value
+sampleTree =
+ Node
+ (Leaf $ Value 3)
+ BinaryPlus
+ (Node
+ (Leaf (Value 5))
+ BinaryPlus
+ (Leaf (Value 2)))
+
+
+inputString :: String
+inputString = "3+5+2"
+
+parsedResult :: String -> Maybe (String, Tree Value)
+parsedResult = parse (stringTree sampleTree) inputString
+
The parsedResult will only succeed if the input string exactly matches the desired tree.
+ +>>> parsedResult
+Just ("",Node (Leaf (Value 3)) BinaryPlus (Node (Leaf (Value 5)) BinaryPlus (Leaf (Value 2))))
+
To evaluate the parsed expression we can use foldMap and the Sum monoidA type class for types that have an associative binary operation (mappend) and an identity element (mempty). Instances of Monoid can be concatenated using mconcat. +:
+ +evalTree :: Tree Value -> Int
+evalTree tree = getSum $ foldMap toSum tree
+ where
+ toSum :: Value -> Sum Int
+ toSum (Value v) = Sum v
+ toSum BinaryPlus = Sum 0 -- For BinaryPlus, we don’t need to add anything to the sum
+
+evalResult :: Maybe (String, Int)
+evalResult = (evalTree <$>) <$> parsedResult
+-- >>> evalResult = Just ("", 10)
+
Folding: The process of reducing a data structure to a single value by applying a function. Haskell provides two types of folds: foldl (left fold) and foldr (right fold).
+ +foldl: A left fold function that processes elements from left to right. Its type signature is foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b.
+ +foldr: A right fold function that processes elements from right to left. Its type signature is foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b.
+ +Monoid: A type class for types that have an associative binary operation (mappend) and an identity element (mempty). Instances of Monoid can be concatenated using mconcat.
+ +Foldable: A type class for data structures that can be folded (reduced) to a single value. It includes functions like foldr, foldl, length, null, elem, maximum, minimum, sum, product, and foldMap.
+ +Traversable: A type class for data structures that can be traversed, applying a function with an Applicative effect to each element. It extends both Foldable and Functor and includes functions like traverse and sequenceA.
+ +Unit: A type with exactly one value, (), used to indicate the absence of meaningful return value, similar to void in other languages.
+ ++ + + 33 + + min read
+The really exciting aspect of higher-order functionA function that takes other functions as arguments or returns a function as its result. + support in languages like JavaScript is that it allows us to combine simple reusable functions in sophisticated ways. We’ve already seen how functions like map, filter and reduce can be chained to flatten the control flow of data processing. In this section we will look at some tricks that allow us to use functions that work with other functions in convenient ways.
+ +A note about type annotationsA syntax in TypeScript where types are explicitly specified for variables, function parameters, and return types to ensure type safety and correctness. + in this section. If you are following the reading order given by the index for these notes, then you have already read our introduction to TypeScript. Therefore, below we frequently use TypeScript type annotationsA syntax in TypeScript where types are explicitly specified for variables, function parameters, and return types to ensure type safety and correctness. + to be precise about the intended use of the functions. However, as we start to rely more and more heavily on curried higher-order functionsA function that takes other functions as arguments or returns a function as its result. + in this chapter, TypeScript type annotationsA syntax in TypeScript where types are explicitly specified for variables, function parameters, and return types to ensure type safety and correctness. + start to become a bit cumbersome, and for the purposes of concisely representing use of combinatorsA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments. + to create new functions, we abandon them entirely. As an exercise, you may like to think about what the TypeScript annotations for some of these functions should be. This is one of the reasons why we later in these notes move away from JavaScript and TypeScript entirely to instead focus on a real functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + language, Haskell.
+ +Functions that take other functions as parameters or which return functions are called higher-order functionsA function that takes other functions as arguments or returns a function as its result. +. +They are called “higher-order” because they are functions which operate on other functions. +Higher-order functionsA function that takes other functions as arguments or returns a function as its result. + are a very powerful feature and central to the functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + programming paradigm.
+ +We’ve seen many examples of functions which take functions as parameters, for example, operations on arrays:
+ +[1,2,3].map(x=>x*x)
+
++ +[1,4,9]
+
Being able to pass functions into other functions enables code customisability and reuse. For example, a sort function which allows the caller to pass in a comparison function can easily be made to sort in increasing or decreasing order, or to sort data elements on an arbitrary attribute.
+ +And we also saw a simple example of a function which returns a new function:
+ +const add = x => y => x + y
+const add9 = add(9)
+
+add9(3)
+add9(1)
+
++ +12
+
+10
Functions that can create new functions give rise to all sorts of emergent power, such as the ability to customise, compose and combine functions in very useful ways. We will see this later when we look at function composition and combinatorsA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments. +.
+ +Higher-order functionsA function that takes other functions as arguments or returns a function as its result.
+ which take a single parameter and return another function operating on a single parameter are called curried functionsFunctions that take multiple arguments one at a time and return a series of functions.
+. The add
function above is one example. You can either call it twice immediately to operate on two parameters:
add(3)(2)
+
++ +5
+
or call it once, leaving one parameter left unspecified, to create a reusable function, like add9
above.
+Such use of a curried function with only a subset of its parameters is called partial applicationThe process of fixing a number of arguments to a function, producing another function of smaller arity.
+. Partial applicationThe process of fixing a number of arguments to a function, producing another function of smaller arity.
+ returns a function for which further parameters must be supplied before the body of the function can finally be evaluated, e.g.:
add9(1)
+
++ +10
+
Here’s a practical example of a curried function. Let’s say we want a function for computing the volume of cylinders, parameterised by the approximation for π that we plan to use:
+ +function cylinderVolume(pi: number, height: number, radius: number): number {
+ return pi * radius * radius * height;
+}
+
And we invoke it like so:
+ +cylinderVolume(Math.PI, 4, 2);
+
Now consider another version of the same function:
+ +function cylinderVolume(pi: number) {
+ return function(height: number) {
+ return function(radius: number) {
+ return pi * radius * radius * height;
+ }
+ }
+}
+
This one, we can invoke like so:
+ +cylinderVolume(Math.PI)(4)(2)
+
But we have some other options too. For example, we are unlikely to change our minds about what precision approximation of PI we are going to use between function calls. So let’s make a local function that fixes PI:
+ +const cylVol = cylinderVolume(Math.PI);
+
Which we can invoke when we are ready like so:
+ +cylVol(4)(2)
+
What if we want to compute volumes for a whole batch of cylinders of fixed height of varying radii?
+ +const radii = [1.2,3.1,4.5, ... ],
+ makeHeight5Cylinder = cylVol(5),
+ cylinders = radii.map(makeHeight5Cylinder);
+
Or we can make it into a handy function to compute areas of circles:
+ +const circleArea = cylVol(1)
+
Such functions are called curried functionsFunctions that take multiple arguments one at a time and return a series of functions. + and they are named after a mathematician named Haskell Curry. This gives you a hint as to what functions look like in the Haskell programming language and its variants. +We can also create a function to make curried versions of conventional multiparameter JavaScript functions:
+ +function curry2<T,U,V>(f: (x:T, y:U) => V): (x:T) => (y:U) => V {
+ return x => y => f(x,y);
+}
+
Now, given a function like plus = (x,y) => x + y
, we can create the curried add function above, like so:
const add = curry2(plus)
+add(3)(4)
+
++ +7
+
We can also create curried versions of functions with more than two variables; but the TypeScript syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. + for functions with arbitrary numbers of arguments gets a bit scary, requiring advanced use of conditional types. This is one of the many reasons we will shortly switch to Haskell for our exploration of more advanced functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + programming topics.
+ +// A type for a regular Uncurried function with arbitrary number of arguments
+type Uncurried = (...args: any[]) => any;
+
+// Warning - advanced TypeScript types!
+// A type for curried functions with arbitrary numbers of arguments.
+// Full disclosure: I needed ChatGPT to help figure this out.
+type Curried<T> = T extends (...args: infer Args) => infer R
+ ? Args extends [infer First, ...infer Rest]
+ ? (arg: First) => Curried<(...args: Rest) => R>
+ : R
+ : never;
+
+// now the curry function returns a function which, when called in curried style,
+// will build the argument list until it has enough arguments to call the original fn
+function curry<T extends Uncurried>(fn: T): Curried<T> {
+ const curried = (...args: any[]): any =>
+ args.length >= fn.length ? fn(...args)
+ : (...moreArgs: any[]) => curried(...args, ...moreArgs);
+ return curried as Curried<T>;
+}
+
+const weirdAdd = (a:number,b:boolean,c:string) => a + (b?1:0) + parseInt(c)
+
+// type of curriedWeirdAdd is:
+// (arg: number) => (arg: boolean) => (arg: string) => number
+const curriedWeirdAdd = curry(weirdAdd);
+
Consider the following function which takes two functions as input, note the way the types line up:
+ +function compose<U,V,W>(f:(x:V)=>W,g:(x:U)=>V) {
+ return (x:U)=> f(g(x))
+}
+
This function lets us combine two functions into a new reusable function. For example, given a messy list of strings representing numbers of various precision:
+ +const grades = ['80.4','100.000','90','99.25']
+
We can define a function to parse these strings into numbers and then round them to the nearest whole number purely through composition of two existing functions:
+ +const roundFloat = compose(Math.round, Number.parseFloat)
+
And then apply it to the whole set:
+ +grades.map(roundFloat)
+
++ +[80, 100, 90, 99]
+
Note that compose
let us define roundFloat
without any messing around with anonymous functionsA function defined without a name, often used as an argument to other functions. Also known as lambda function.
+ and explicit wiring-up of return values to parameters. We call this tacit or point-free style programming.
Create a compose
function in Javascript that takes a variable number of functions as arguments and composes (chains) them. Using the spread operator (...
) to take a variable number of arguments as an array and the Array.prototype.reduce
method, the function should be very small.
Create a pipe
function which composes its arguments in the opposite order to the compose
function above. That is, left-to-right. Note that in RxJS, such a pipe
function is an important way to create chains of operations (over ObservableA data structure that represents a collection of future values or events, allowing for asynchronous data handling and reactive programming.
+ streams).
The compose
function takes multiple functions and returns a new function that applies these functions from right to left to an initial argument using reduceRight
. In the example, add1
adds 1 to a number, and double
multiplies a number by 2. compose(double, add1)
creates a function that first adds 1 and then doubles the result. Calling this function with 5 results in 12.
const compose = (...funcs) => (initialArg) =>
+ funcs.reduceRight((arg, fn) => fn(arg), initialArg);
+
+// Example usage
+const add1 = x => x + 1;
+const double = x => x * 2;
+
+const add1ThenDouble = compose(double, add1);
+
+console.log(add1ThenDouble(5)); // Output: 12
+
Using TypeScript to create a flexible compose function requires accommodating varying input and output types for each function in the chain. To achieve this flexibility without losing type safety, we need to ensure that the output type of one function matches the input type of the next function. However, if we try to type this correctly without using any, we face significant complexity with the lack of expressiveness in typescripts type system. pipe
in RxJS solves this issue by hardcoding up to 9 functions inside the pipe, any functions larger then this will not be typed correctly.
const pipe = (...funcs) => (initialArg) =>
+ funcs.reduce((arg, fn) => fn(arg), initialArg);
+
The pipe function is similar to the compose function, but it applies its functions in the opposite order—from left to right.
+ + +CombinatorsA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments.
+ are higher-order functionsA function that takes other functions as arguments or returns a function as its result.
+ which perform pure operations on their arguments to perform a result. They may seem very basic, but as their name suggests, they provide useful building blocks for manipulating and composing functions to create new functions. The compose
function is a combinatorA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments.
+. Some more examples follow.
The following may seem trivial:
+ +function identity<T>(value: T): T {
+ return value;
+}
+
But it has some important applications:
+ +identity
to restore the default behaviour.identity
into map).identity
operator really give us back what we started with?”.The curried K-CombinatorA combinator that takes two arguments and returns the first one. + looks like:
+ +const K = x=> y=> x
+
So it is a function that ignores its second argument and returns its first argument directly. Note the similarity to the head
function of our cons list. In fact, we can derive curried versions of both the head
and rest
functions used earlier from K
and I
combinatorsA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments.
+ (renaming rest
to tail
; see below):
const
+ K = x=> y=> x,
+ I = x=> x,
+ cons = x=> y=> f=> f(x)(y),
+ head = l=> l(K),
+ tail = l=> l(K(I)),
+ forEach = f=> l=> l?(f(head(l)),forEach(f)(tail(l))):null;
+
+const l = cons(1)(cons(2)(cons(3)(null)));
+
+forEach(console.log)(l)
+
++ +1
+
+2
+3
The definition of head
is by straightforward, like-for-like substitution of K
into a curried version of our previous definition for head
. Note, the following is not code, just statements of equivalence (≡):
head ≡ l=>l((h,_)=>h) -- previous uncurried definition of head
+ ≡ l=>l(curry2((h,_)=>h))
+ ≡ l=>l(h=>_=>h)
+
Where the expression in brackets above we notice is equivalent to K
:
K ≡ x=> y=> x ≡ h=> _=> h
+
Of course, this definition is not unique to Javascript, we mainly use this language to explore this idea, but the equivalent can be completed in Python, with a slightly more verbose syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. +
+ +K = lambda x : lambda y : x
+I = lambda x : x
+cons = lambda x: lambda y : lambda f : f(x)(y)
+head = lambda l : l(K)
+tail = lambda l : l(K(I))
+forEach = lambda f : lambda l : (f(head(l)),forEach(f)(tail(l))) if l is not None else None
+l = cons(1)(cons(2)(cons(3)(None)))
+
+forEach(print)(l)
+
++ +1 +2 +3
+
In the context of the Lambda CalculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s. +, we will see that such a renaming is called Alpha Conversion.
+ +We are gradually changing our terminology to be more Haskell-like, so we have named the curried version of rest
to tail
. The new definition of tail
, compared to our previous definition of rest
, is derived as follows:
K(I) ≡ K(i=> i) -- expand I := i=> i
+ ≡ (x=> y=> x)(i=> i) -- expand K := x=> y=> x
+ ≡ y=> i=> i
+
Where the last line above is the result of applying x=>y=>x
to i=>i
. Thus, we substitute x:=i=>i
in the body of the first function (the expansion of K
). When we explore the Lambda CalculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s.
+, we will see that this operation (simple evaluation of function application by substitution of expressions) is called Beta reduction.
Now we could derive tail
from rest
using our curry2
function:
rest ≡ l=>l((_,r)=>r)
+tail ≡ l=>l(curry2((_,r)=>r))
+ ≡ l=>l(_=>r=>r)
+
Where _=> r=> r ≡ y=> i=> i
and therefore tail ≡ l=>l(K(i))
. QED!!!
FYI it has been shown that simple combinatorsA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments. + like K and I (at least one other is required) are sufficient to create languages as powerful as lambda calculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s. + without the need for lambdas, e.g. see SKI CombinatorA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments. + Calculus.
+ +In previous sections we have seen a number of versions of functions which transform lists (or other containers) into new lists like map
, filter
and so on. We have also introduced the reduce function as a way to compute a single value over a list. If we realise that the value we produce from reduce can also be a list, we can actually use reduce to implement all of the other lists transformations. Instead of returning a value from a reduce, we could apply a function which produces only side effectsAny state change that occurs outside of a function’s local environment or any observable interaction with the outside world, such as modifying a global variable, writing to a file, or printing to a console.
+, thus, performing a forEach
. We’ll use this as an example momentarily.
First, here’s another implementation of reduce
for the above formulation of cons lists - but we rename it fold
(again, as our JavaScript becomes more and more Haskell like we are beginning to adopt Haskell terminology).
const fold = f=> i=> l=> l ? fold(f)(f(i)(head(l)))(tail(l)) : i
+
Now, for example, we can define forEach
in terms of fold
:
const forEach = f=>l=>fold(_=>v=>f(v))(null)(l)
+
Now, the function f
takes one parameter and we don’t do anything with its return type (in TypeScript we could enforce the return type to be void
).
+However, fold
is expecting as its first argument a curried function of two parameters (the accumulator and the list element). Since in forEach
we are not actually accumulating a value, we can ignore the first parameter, hence we give fold
the function _=>v=>f(v)
, to apply f
to each value v
from the list.
But note that v=>f(v)
is precisely the same as just f
.
+So we can simplify forEach a bit further:
const forEach = f=>l=>fold(_=>f)(null)(l)
+
But check out these equivalences:
+ +K(f) ≡ (x=> y=> x)(f) -- expand K
+ ≡ y=> f -- apply the outer function to f, hence we substitute x:= f
+ ≡ _=> f -- rename y to _
+
Where, in the last line above, since y doesn’t appear anywhere in the body of the function we don’t care what it’s called anymore and rename it to _
.
Therefore, we can use our K
combinatorA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments.
+ to entirely avoid defining any functions in the body of forEach
:
const forEach = f=>l=>fold(K(f))(null)(l)
+
map
and filter
for the above cons list definition in terms of fold
A naive implementation of map using fold, would be:
+ +const map = f => l =>
+ fold(acc => v => cons(f(v))(acc))(null)(l);
+
+const l = cons(1)(cons(2)(cons(3)(null)));
+const mappedList = map(x => x * 2)(l);
+forEach(console.log)(mappedList);
+
+
++ +6
+ +4
+ +2
+
This construct a new cons every time, applying the function f
to the current item in v
. However, this will reverse the list because fold processes the list from head to tail, and constructs the new list by prepending each element to the accumulator. Hence, reversing the list.
const reverse = l =>
+ fold(acc => v => cons(v)(acc))(null)(l);
+
+const map = f => l =>
+ fold(acc => v => cons(f(v))(acc))(null)(reverse(l));
+
+const filter = pred => l =>
+ fold(acc => v => pred(v) ? cons(v)(acc) : acc)(null)(reverse(l));
+
Therefore, we need to reverse the list, using a separate function, to ensure that we apply the functions in the correct order. However, the preferred way around this, would be to reduce in the other direction, e.g., using reduceRight, to fold through the list tail to head.
+ + +A function that applies a first function. If the first function fails (returns undefined, false or null), it applies the second function. The result is the first function that succeeded.
+ +const or = f=> g=> v=> f(v) || g(v)
+
Basically, it’s a curried if-then-else function with continuations. Imagine something like the following data for student names in a unit, then a dictionary of the ids of students in each class:
+ +const students = ['tim','sally','sam','cindy'],
+ class1 = { 'tim':123, 'cindy':456},
+ class2 = { 'sally':234, 'sam':345};
+
We have a function that lets us lookup the id for a student in a particular class:
+ +// 'class' is a reserved keyword in JavaScript
+const lookup = class_=> name=> class_[name]
+
Now we can try to find an id for each student, first from class1
but fall back to class2
if it isn’t there:
const ids = students.map(or(lookup(class1))(lookup(class2)))
+
The following is cute:
+ +function fork(join, f, g) {
+ return value => join(f(value), g(value));
+}
+
The fork
function is a higher-order combinatorA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments.
+ that combines two functions, f
and g
, and then merges their results using a join
function.
Use the fork-join combinatorA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments. + to compute the average over a sequence of numeric values.
+Add type annotationsA syntax in TypeScript where types are explicitly specified for variables, function parameters, and return types to ensure type safety and correctness. + to the above definition of the fork function. How many distinct type variables do you need?
+We can use the fork-join combinatorA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments.
+ by considering the sum and count of the sequence as the two branches (f
and g
), and then using a join function to divide the sum by the count to compute the average.
const sum = arr => arr.reduce((acc, x) => acc + x, 0);
+const count = arr => arr.length;
+
+const average = (sum, count) => sum / count;
+
+// Create the average function using fork
+const computeAverage = fork(average, sum, count);
+
+const values = [1, 2, 3, 4, 5];
+console.log(computeAverage(values));
+
++ +3
+
We would need four distinct genertic types for this function
+ +function fork<T, U, V, R>(join: (a: U, b: V) => R, f: (value: T) => U, g: (value: T) => V): (value: T) => R {
+ return (value: T) => join(f(value), g(value));
+}
+
T
for the type of the input value.U
for the type of the result of function f
.V
for the type of the result of function g
.R
for the type of the result of the join
function.Uncurried functions of two parameters can be called Binary functions. Functions of only one parameter can therefore be called Unary functions. Note that all of our curried functionsFunctions that take multiple arguments one at a time and return a series of functions. + are unary functions, which return other unary functions. +We’ve seen situations now where curried functionsFunctions that take multiple arguments one at a time and return a series of functions. + are flexibly combined to be used in different situations.
+ +Note that in JavaScript you sometimes see casual calls to binary functions but with only one parameter specified. Inside the called function the unspecified parameter will simply be undefined
which is fine if the case of that parameter being undefined
is handled in a way that does not cause an error or unexpected results, e.g.:
function binaryFunc(x,y) {console.log(`${x} ${y}`) }
+binaryFunc("Hello", "World")
+binaryFunc("Hello")
+
++ +Hello World
+
+Hello undefined
Conversely, javascript allows additional parameters to be passed to unary functions which will then simply be unused, e.g.:
+ +function unaryFunc(x) { console.log(x) }
+unaryFunc("Hello")
+unaryFunc("Hello", "World")
+
++ +Hello
+
+Hello
But, here’s an interesting example where mixing up unary and binary functions in JavaScript’s very forgiving environment can go wrong.
+ +['1','2','3'].map(parseInt);
+
We are converting an array of strings into an array of int. The output will be [1,2,3]
right? WRONG!
['1','2','3'].map(parseInt);
+
++ +[1, NaN, NaN]
+
What the …!
+ +But:
+ +parseInt('2')
+
++ +2
+
What’s going on? +HINT: parseInt is not actually a unary function.
+ +The point of this demonstration is that curried functionsFunctions that take multiple arguments one at a time and return a series of functions. + are a more principled way to support partial function application, and also much safer and easier to use when the types guard against improper use. Thus, this is about as sophisticated as we are going to try to get with FunctionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + Programming in JavaScript, and we will pick up our discussion further exploring the power of FP in the context of the Haskell functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + programming language. However, libraries do exist that provide quite flexible functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + programming abstractions in JavaScript. For example, you might like to investigate Ramda.
+ +Array.map
and parseInt
can you figure out why the above is happening?map(Iterable,fn)
to create mapApplyFn(Iterable)
.When parseInt
is used as the callback for map
, it is called with three arguments: currentValue, index, and array. parseInt
expects the second argument to be the radix, but map
provides the index of the current element as the second argument. This leads to incorrect parsing.
The unary function is defined as:
+function unary<T, U, V>(binaryFunc: (arg1: T, arg2: U) => V, boundValue: T): (arg2: U) => V {
+ return function(secondValue: U): V {
+ return binaryFunc(boundValue, secondValue);
+ };
+ }
+
T
: Type of the first argument of the binary function (the value to bind).U
: Type of the second argument of the binary function.V
: Return type of the binary function.function flip<T, U, V>(binaryFunc: (arg1: T, arg2: U) => V): (arg2: U, arg1: T) => V {
+ return function(arg2: U, arg1: T): V {
+ return binaryFunc(arg1, arg2);
+ };
+}
+
Combinator: A higher-order function that uses only function application and earlier defined combinators to define a result from its arguments.
+ +Function composition: The process of combining two or more functions to produce a new function.
+ +Point-free style: A way of defining functions without mentioning their arguments.
+ +Curried functions: Functions that take multiple arguments one at a time and return a series of functions.
+ +Partial application: The process of fixing a number of arguments to a function, producing another function of smaller arity.
+ +Identity function (I-combinator): A function that returns its argument unchanged.
+ +K-combinator: A combinator that takes two arguments and returns the first one.
+ ++ + + 12 + + min read
+HTMLHyper-Text Markup Language - the declarative language for specifying web page content. +, or HyperText Markup Language, is the standard markup language used to create and design web pages. It provides the structure and content of a web page by using a system of markup tags and attributes. HTMLHyper-Text Markup Language - the declarative language for specifying web page content. + documents are interpreted by web browsers to render the content visually for users.
+ +HTMLHyper-Text Markup Language - the declarative language for specifying web page content. + is considered a declarativeDeclarative languages focus on declaring what a procedure (or function) should do rather than how it should do it. + language because it focuses on describing the structure and content of a web page without specifying how to achieve it. Instead of giving step-by-step instructions for rendering elements, HTMLHyper-Text Markup Language - the declarative language for specifying web page content. + allows developers to declare the desired structure and let the browser handle the rendering process.
+ +Descriptive Tags: HTMLHyper-Text Markup Language - the declarative language for specifying web page content.
+ tags are descriptive elements that define the purpose and meaning of content. For example, <p>
tags indicate a paragraph, <h1>
to <h6>
tags denote headings of varying levels, <ul>
and <ol>
represent unordered and ordered lists respectively. These tags describe the content they enclose rather than instructing how it should be displayed. Pairs of opening and closing HTMLHyper-Text Markup Language - the declarative language for specifying web page content.
+ tags (e.g. <h1>
defines the start of a heading, </h1>
marks the end) define elements in a Document Object Model (DOM). Elements can be nested within each other such that the DOM is a hierarchical (tree) structure.
Attribute-Based: HTMLHyper-Text Markup Language - the declarative language for specifying web page content. + elements can have attributes that provide additional information or functionality. Attributes like class, id, src, href, etc., provide hooks for styling, scripting, or specifying behavior. However, these attributes do not dictate how elements are displayed; they simply provide metadata or instructions to browsers.
+Separation of Concerns: Modern HTML5 is actually a suite of languages which encourages a separation of concerns by delineating structure (HTMLHyper-Text Markup Language - the declarative language for specifying web page content. +), presentation (CSSCascading Style Sheets - another declarative (part of the HTML5 standard) for specifying reusable styles for web page rendering. +), and behavior (JavaScript). This promotes maintainability and scalability by allowing each aspect of web development to be managed independently.
+Browser Interpretation: HTMLHyper-Text Markup Language - the declarative language for specifying web page content. + documents are interpreted by web browsers, which render the content based on the instructions provided in the markup. Browsers apply default styles and layout algorithms to HTMLHyper-Text Markup Language - the declarative language for specifying web page content. + elements, ensuring consistency across different platforms and devices.
+A live version of the following code is available together with an online editor for you to experiment with. Or you can create the files locally on your computer, all in the same directory, and drag-and-drop the index.html
file into your browser to see the animation.
+We are going to build an HTMLHyper-Text Markup Language - the declarative language for specifying web page content.
+ page that looks something like this:
+
First, create a new HTMLHyper-Text Markup Language - the declarative language for specifying web page content.
+ file (name it index.html
) and define the basic structure of the document:
<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Moving Rectangle Tutorial</title>
+</head>
+<body>
+
+</body>
+</html>
+
In this step, we’ve set up the basic HTMLHyper-Text Markup Language - the declarative language for specifying web page content.
+ structure with a <!DOCTYPE>
declaration, <html>
, <head>
, and <body>
tags. We’ve also included meta tags for character encoding and viewport settings, as well as a title for the page.
Next, let’s add an SVGScalable Vector Graphics - another part of the HTML standard for specifying images declaratively as sets of shapes and paths. + element to the body of our HTMLHyper-Text Markup Language - the declarative language for specifying web page content. + document. This SVGScalable Vector Graphics - another part of the HTML standard for specifying images declaratively as sets of shapes and paths. + element will contain the rectangle that we’ll animate later:
+ +<body>
+ <svg width="100" height="30">
+ <!-- SVG content will go here -->
+ </svg>
+</body>
+
We’ve added an SVGScalable Vector Graphics - another part of the HTML standard for specifying images declaratively as sets of shapes and paths. + element with a width and height of 100 units each. This provides a canvas for our SVGScalable Vector Graphics - another part of the HTML standard for specifying images declaratively as sets of shapes and paths. + graphics.
+ +Now, let’s add a rectangle <rect>
element inside the SVGScalable Vector Graphics - another part of the HTML standard for specifying images declaratively as sets of shapes and paths.
+ to represent the moving rectangle:
<svg width="100" height="30">
+ <rect id="blueRectangle" x="10" y="5" width="20" height="20" fill="blue"/>
+</svg>
+
In this step, we’ve defined a rectangle with a starting position at coordinates (10, 10) and a width and height of 20 units each. The rectangle is filled with a blue color. Importantly, we’ve given the <rect>
element a unique id “blueRectangle” by which we can refer to it elsewhere, below we’ll demonstrate adding an animation behaviour to this rectangle using this id from CSSCascading Style Sheets - another declarative (part of the HTML5 standard) for specifying reusable styles for web page rendering.
+ or JavaScript.
Most HTMLHyper-Text Markup Language - the declarative language for specifying web page content. + elements, including SVGScalable Vector Graphics - another part of the HTML standard for specifying images declaratively as sets of shapes and paths. + elements have certain attributes according to their documentation, which determine how they are rendered and behave in the browser. MDN is normally a good reference for what is available to use.
+ +There are many ways to achieve the same thing in HTMLHyper-Text Markup Language - the declarative language for specifying web page content.
+. We will now look at how an animation may be added declaratively to our SVGScalable Vector Graphics - another part of the HTML standard for specifying images declaratively as sets of shapes and paths.
+ rectangle using CSSCascading Style Sheets - another declarative (part of the HTML5 standard) for specifying reusable styles for web page rendering.
+. First, we need to tell the browser where to find our CSSCascading Style Sheets - another declarative (part of the HTML5 standard) for specifying reusable styles for web page rendering.
+ file from the (<head>
) element of our HTMLHyper-Text Markup Language - the declarative language for specifying web page content.
+.
<head>
+...
+ <link rel="stylesheet" href="style.css" >
+</head>
+
Now we create the style.css
file as follows:
#blueRectangle {
+ animation-name: moveX;
+ animation-duration: 5s;
+ animation-timing-function: linear;
+ animation-fill-mode: forwards;
+}
+
+@keyframes moveX {
+ 0% {
+ x: 0;
+ }
+ 100% {
+ x: 370;
+ }
+}
+
This first clause selects the rectangle by the unique id we gave it: blueRectangle
, and then declares some style attributes for animation that specify:
moveX
;In the keyframes
declaration we declare style properties which should be applied at different percentages of completion of the moveX
animation. The browser will interpolate between them according to the other style settings we specified. In this case, we have simply set an initial and final x
position for the rectangle.
This is a program of sorts (in that it causes a lot of computation to happen in the browser with outputs that we can see on our webpage), but it’s declarativeDeclarative languages focus on declaring what a procedure (or function) should do rather than how it should do it. + in the sense that we did not tell the browser how to perform the animation. Rather we declared what we wanted the rectangle to look like at the start and end of the animation and let the browser figure out how to perform the transition.
+ +By contrast, we can create an imperativeImperative programs are a sequence of statements that change a programs state. This is probably the dominant paradigm for programming languages today. Languages from Assembler to Python are built around this concept and most modern languages still allow you to program in this style.
+ JavaScript program which explicitly gives the list of instructions for how to move the rectangle.
+We’ll create another rectangle with the id “redRectangle” which we can manipulate from javascript by including a reference to a js file, e.g., script.js
<body>
+ ...
+ <svg width="100" height="30" id="svg">
+ <rect id="redRectangle" x="10" y="5" width="20" height="20" fill="red"/>
+ </svg>
+ <script src="script.js"></script>
+</body>
+
We now create a function which encodes the precise steps to animate the rectangle at 60 FPS (the setTimeout
call queues up each successive frame of animation). If you have experience with other languages like python hopefully this will be understandable even if the syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language.
+ looks a bit unfamiliar. If it’s not completely clear yet don’t worry. We’ll point out the things that are important to note for now below, but you can refer to our intro to JavaScript for the basics.
// Define an animation function
+function animate(rect, startX, finalX, duration) {
+ const
+ startTime = performance.now(),
+ endTime = startTime + duration;
+ function nextFrame() {
+ // Calculate elapsed time
+ const
+ currentTime = performance.now(),
+ elapsedTime = currentTime - startTime;
+
+ // Check if animation duration has elapsed
+ if (elapsedTime >= duration) {
+ // Set the final position of the rectangle.
+ // We can use `setAttribute` to modify the HTML Element. In this case, we are changing the x attribute.
+ rect.setAttribute('x', finalX);
+ return; // Stop the animation
+ }
+
+ // Calculate position based on elapsed time
+ const x = startX + (finalX - startX) * elapsedTime / duration;
+
+ // Set the intermediate position of the rectangle.
+ rect.setAttribute('x', x);
+
+ // Call the nextFrame function again after a delay of 1000/60 milliseconds
+ setTimeout(nextFrame, 1000 / 60); // 60 FPS
+ }
+ nextFrame();
+}
+
+const rectangle = document.getElementById('redRectangle')
+const duration = 5000; // 5 seconds in milliseconds
+animate(rectangle, 0, 370, duration);
+
However, there are some serious issues with this code.
+ +animate
function updates the state of the DOM (the x
position of the rectangle) from deep inside it’s logic. Normally, we look for outputs of functions in the value that they return
, but this function has no explicit return value. To see what it does, we have to carefully inspect the code to identify the line which causes the side effect of moving the rectangle (the rect.setAttribute
calls).setTimeout
to queue up the successive frames of animation. Such hidden side effectsAny state change that occurs outside of a function’s local environment or any observable interaction with the outside world, such as modifying a global variable, writing to a file, or printing to a console.
+ and complexity are the opposite of the intention of declarativeDeclarative languages focus on declaring what a procedure (or function) should do rather than how it should do it.
+-style programming.Later, we will see how functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + reactive programming techniques can be used to separate code with such side effectsAny state change that occurs outside of a function’s local environment or any observable interaction with the outside world, such as modifying a global variable, writing to a file, or printing to a console. + on global state from code that implements behavioural logic in interactive web pages.
+ +We saw that the philosophy of HTMLHyper-Text Markup Language - the declarative language for specifying web page content. + programming is primarily declarativeDeclarative languages focus on declaring what a procedure (or function) should do rather than how it should do it. + in the sense that the programmer/designer tells the browser what they want to see and rely on the browser’s sophisticated rendering engine to figure out how to display the content. This extends to adding dynamic behaviours such as animation declaratively through CSSCascading Style Sheets - another declarative (part of the HTML5 standard) for specifying reusable styles for web page rendering. +.
+ +By contrast, we saw a different imperativeImperative programs are a sequence of statements that change a programs state. This is probably the dominant paradigm for programming languages today. Languages from Assembler to Python are built around this concept and most modern languages still allow you to program in this style. + approach to adding an animation to our web page using JavaScript. There are pros and cons to each:
+ + + +DeclarativeDeclarative languages focus on declaring what a procedure (or function) should do rather than how it should do it. +
+ +ImperativeImperative programs are a sequence of statements that change a programs state. This is probably the dominant paradigm for programming languages today. Languages from Assembler to Python are built around this concept and most modern languages still allow you to program in this style. +
+ +A major theme of this course will be seeing how pure functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + programming techniques give us a coding style that:
+ +HTML: Hyper-Text Markup Language - the declarative language for specifying web page content.
+ +CSS: Cascading Style Sheets - another declarative (part of the HTML5 standard) for specifying reusable styles for web page rendering.
+ +SVG: Scalable Vector Graphics - another part of the HTML standard for specifying images declaratively as sets of shapes and paths.
+ +A fully worked example of FRP using RxJS Observables to create an in-browser version of a classic arcade game.
+ + +where
and let
clausesfmap
or (<$>)
operation.(<*>)
operator) to containers of values.foldl
and foldr
for left and right folds respectivelyMonoid
values trivial to fold
(>>=)
operation which allows us to sequence effectful operations such that their effects are flattened or joined into a single effect.Maybe
, IO
, List and Function instances of Monad.do
notation.Either
type handles values with two possibilities, typically used for error handling and success cases.Functor
, Applicative
, and Monad
type classes to the Either
type, learning how to implement instances for each.do
blocks in simplifying code and handling complex workflows.+ + + 54 + + min read
+In the late 90s the mood was right for a language that was small and simple and with executable files small enough to be distributed over the web. Originally Java was meant to be that language but, while it quickly gained traction as a language for building general purpose applications and server-side middleware, it never really took off in the browser. Something even simpler, and better integrated with the Document Object Model (DOM) of HTML pages was required to add basic interaction to web pages.
+ +Brendan Eich was hired by Netscape in 1995 to integrate a Scheme interpreter into their browser for this purpose. No messy deployment of Java bytecode bundles—the browser would have been able to run Scheme scripts embedded directly into web pages. This would have been awesome. Unfortunately, for reasons that were largely political and marketing related, it was felt that something more superficially resembling Java was required. Thus, Eich created a prototype scripting language in 2 weeks that eventually became JavaScript. As we will see, it is syntactically familiar for Java developers. Under the hood, however, it follows quite a different paradigm.
+ +The fact it was initially rushed to market, the fact that browser makers seemingly had difficulty early on making standards-compliant implementations, and a couple of regrettable decisions at first regarding things like scoping semanticsThe processes a computer follows when executing a program in a given language. +, meant that JavaScript developed something of a bad name. It’s also possible that there was some inherent snobbiness amongst computer science types that, since JavaScript was not a compiled language, it must inevitably lead to armageddon. Somehow, however, it survived and began the “web 2.0” phenomenon of what we now refer to as rich, client-side “web apps”. It has also matured and, with the EcmaScript 6 (ES6) and up versions, has actually become quite an elegant little multi paradigm language.
+ +The following introduction to JavaScript assumes a reasonable knowledge of programming in another imperativeImperative programs are a sequence of statements that change a programs state. This is probably the dominant paradigm for programming languages today. Languages from Assembler to Python are built around this concept and most modern languages still allow you to program in this style. + language such as Python or Java.
+ +We declare constant variables in JavaScript with the const
keyword:
const z = 1 // constant (immutable variable) at global scope
+
You can try this in the debug console in a browser such as Chrome. At the console, you enter JavaScript directly at the prompt (>
). If we try to change the value of such a const
variable, we get a run-time error:
> const z = 1
+> z = 2
+
++ +Uncaught TypeError: Assignment to constant variable.
+
We define mutable variablesA variable declared with let that can be reassigned to different values.
+ in JavaScript with the let
keyword:
let w = 1
+
The console replies with the value returned by the let
statement (in Chrome console the result value is prefixed by ⋖
), as follows:
⋖ undefined
+
But fear not, w
was assigned the correct value which you can confirm by typing just w
into the console:
> w
+⋖ 1
+
Now if we assign a new value to w it succeeds:
+ +> w = 2 // note that without a let or const keyword before it, this assignment an expression which returns a value:
+⋖ 2
+
(Note: The original JavaScript syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language.
+ for declaring a variable used the var
keyword. However, the scoping of variables declared in this way was strange for people familiar with C and Java scoping rules, and caused much angst. It has been fixed since ES6 with the let
and const
keywords; we prefer these to var
.)
We refer to any JavaScript code which evaluates to a value as an expression.
+For example, the expression 1+1
evaluates to 2
.
+We can see in the Chrome console:
> 1 + 1
+⋖ 2
+
By contrast a statement performs computation without returning a value. More precisely, statements evaluate to undefined
. For example, a variable declaration is a statement:
> const x = 1 + 1
+⋖ undefined
+
Statements have to have some side effect to be useful (in this case creating a variable in the current scope). Code blocks defined with curly braces are also statements:
+ +> {
+ const x = 1+1
+ const y = 2
+ console.log(x+y)
+ }
+4
+⋖ undefined
+
Note that 4
is printed to the console by the console.log
(a side effect of the statement), but the value returned by the code block is undefined
.
Unlike languages which use indentation to define code blocks and scopes (like Python and Haskell), JavaScript generally ignores multiple spaces, indentation and linebreaks. So you can spread an expression across multiple lines and indent each line however you like:
+ +> 1 +
+ 1 +
+ 1
+⋖ 3
+
You can also put multiple statements on one line by separating them with ‘;
’:
> const x = 1+1; const y = 2; console.log(x+y)
+4
+⋖ undefined
+
Of course, you should not abuse the ability to layout code in different ways and make unreadable code. Rather, it is good practice to be consistent with formatting, use indentation to highlight nested scopes and spread things out as necessary for readability.
+ +JavaScript has several “primitive types” (simple types that are not Objects). These include:
+ +number
: any numeric value, integer or decimalstring
: delineated like "hello"
or 'hello'
or even `hello`
.boolean
: can be only true
or false
undefined
: is a special type with only one value which is undefined
.and a couple of others that we won’t worry about here.
+ +JavaScript is loosely typed in the sense that a mutable variableA variable declared with let that can be reassigned to different values. + can be assigned (and re-assigned) different values of different types.
+ +> let x
+⋖ undefined
+
> x = 3
+⋖ 3
+
> x = 'hello'
+⋖ "hello"
+
A variable’s scope is the region of the program where it is visible, i.e. it can be referenced. +You can limit the scope of a variable by declaring it inside a block of code delineated by curly braces:
+ + {
+ let x = 1
+ console.log(x)
+ }
+
Console prints 1
from the console.log
:
++ +1
+
But if we try to get the value of x
:
x
+
++ +Uncaught ReferenceError: x is not defined
+
The above console.log statement
successfully output the value of x
because it was inside the same scope (the same set of curly braces). The subsequent error occurs because we tried to look at x outside the scope of its definition. Variables declared outside of any scope are said to be “global” and will be visible to any code loaded on the same page and could clobber or be clobbered by other global definitions—so take care!
Be especially careful to always declare variables with either let
or const
keywords. If you omit these keywords, a variable will be created at the global scope even though it is inside a { … }
delimited scope, like so:
{
+ x = 1
+ }
+
+ x
+
++ +1
+
We are going to start to use a few operators that may be familiar from C or Java, some are JS specific.
+Here’s a cheatsheet:
x % y // modulo
+x == y // loose* equality
+x != y // loose* inequality
+x === y // strict+ equality
+x !== y // strict+ inequality
+
+a && b // logical and
+a || b // logical or
+
+a & b // bitwise and
+a | b // bitwise or
+
* Loose (in)equality means type conversion may occur
+ ++ Use strict (in)equality if type is expected to be same
+ +i++ // post-increment
+++i // pre-increment
+i-- // post-decrement
+--i // pre-decrement
+!x // not x
+
<condition> ? <true result> : <false result>
+
x += <expr>
+// add result of expr to x
+// also -=, *=, /=, |=, &=.
+
Functions are declared with the function
keyword. You can give the function a name followed by a tuple of zero or more parameters. Variables declared within the function’s body (marked by a matching pair of curly braces { … }
) are limited to the scope of that function body, but of course the parameters to the function are also in scope within the function body. You return the result with the return
keyword.
/**
+* define a function called “myFunction” with two parameters, x and y
+* which does some silly math, prints something and returns the result
+*/
+function myFunction(x, y) {
+ let t = x + y; // t is mutable
+ t += z; // += adds the result of the expression on the right to the value of t
+ const result = t; // semicolons are not essential (but can help to catch errors)
+ console.log("hello world"); // prints to the console
+ return result; // returns the result to the caller
+}
+
+const z = 1; // z is immutable and defined in the global scope
+
You invoke (or “call”, or “apply”) a function like so:
+ +myFunction(1,2)
+
++ +hello world
+
+4
An if-else
statement looks like so:
/**
+* get the greater of x and y
+*/
+function maxVal(x, y) {
+ if (x >= y) {
+ return x;
+ } else {
+ return y;
+ }
+}
+
This is semantically equivalent to familiar Python if/else construct, however, using {
rather than indentation rules to establish scoping rules:
def maxVal(x, y):
+ if (x >= y):
+ return x
+ else:
+ return y
+
There is also a useful ternary expression syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. + for if-then-else:
+ +function maxVal(x, y) {
+ return x >= y ? x : y;
+}
+
We can loop with while
:
/**
+* sum the numbers up to and including n
+*/
+function sumTo(n) {
+ let sum = 0;
+ while (n) { // when n is 0 this evaluates to false ending the loop
+ // add n to sum then decrement n
+ sum += n--; // see operator cheatsheet above
+ }
+ return sum;
+}
+sumTo(10)
+
++ +55
+
Or for
:
function sumTo(n) {
+ let sum = 0;
+ for (let i = 1; i <= n; i++) {
+ sum += i;
+ }
+ return sum;
+}
+
This looks slightly different to a for
loop in Python, since in Javascript we normally use 3 parts to our for loop definition (initialization, end, update)
def sumTo(n):
+ sum = 0
+ for i in range(1, n + 1):
+ sum += i
+ return sum
+
Or we could perform the same computation using a recursive loop:
+ +function sumTo(n) {
+ if (n === 0) return 0; // base case
+ return n + sumTo(n-1); // inductive step
+}
+
We can make this really succinct with a ternary if-then-else expression:
+ +function sumTo(n) {
+ return n ? n + sumTo(n-1) : 0;
+}
+
We consider this recursive loop a more “declarativeDeclarative languages focus on declaring what a procedure (or function) should do rather than how it should do it. +” coding style than the imperativeImperative programs are a sequence of statements that change a programs state. This is probably the dominant paradigm for programming languages today. Languages from Assembler to Python are built around this concept and most modern languages still allow you to program in this style. + loops.
+ +It is closer to the inductive definition of sum than a series of steps for how to compute it.
+ +Each time a function is invoked, the interpreter (or ultimately the CPU) will allocate another chunk of memory to a special area of memory set aside for such use called the stack. The stack is finite. A recursive function that calls itself too many times will consume all the available stack memory.
+ +Therefore, too many levels of recursion will cause a stack overflow.
+ +sumTo(1000000)
+
++ +Uncaught RangeError: Maximum call stack size exceeded
+
However, functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus.
+ languages (like Haskell) rely on recursion because they have no other way to create loops without mutable variablesA variable declared with let that can be reassigned to different values.
+—so they must have a way to make this scale to real-world computations. When a recursive function is written in a special way, such that the recursive call is in tail position, a lot of modern compilers are able to transform the recursion into a loop with constant memory use (commonly a while
loop)—this is called tail call optimisation.
Let’s see what a tail recursive version of the sumTo
function looks like:
function sumTo(n, sum = 0) {
+ return n ? sumTo(n-1, sum + n)
+ : sum;
+}
+
We have added a second parameter sum to store the computation as recursion proceeds. Such parameters are called accumulators. The = 0
in the parameter definition provides a default value in case the caller does not specify an argument. Thus, this new version can be called the same way as before:
sumTo(10)
+
++ +55
+
The important change is that the recursive call (on the branch of execution that requires it) is now the very last operation to be executed before the function returns. The computation (sum + n
) occurs before the recursive call. Therefore, no local state needs to be stored on the stack.
Note: although it has been proposed for the EcmaScript standard, as of 2024, not all JavaScript engines support tail call optimisation (only Safari/WebKit AFAIK).
+ +We can make functions more versatile by parameterising them with other functions:
+ +function sumTo(n, f = x => x) {
+ return n ? f(n) + sumTo(n-1, f) : 0;
+}
+
Note that the new parameter f
defaults to a simple function that directly returns its argument. Thus, called without a second parameter sumTo has the same behavior as before:
sumTo(10)
+
++ +55
+
But, we can now specify a non-trivial function to be applied to the numbers before they are summed. For example, a function to square a number:
+ +function square(x) {
+ return x * x;
+}
+
can be passed into sumTo
to compute a sum of squares:
sumTo(10, square)
+> 385
+
Like Java, everything in JavaScript is an object. You can construct an object populated with some data, essentially just with JSON syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. +:
+ +const myObj = {
+ aProperty: 123,
+ anotherProperty: "tim was here"
+};
+
However, in JavaScript, objects are simply property bags, indexed by a hashtable:
+ +// the following are equivalent and both involve a hashtable lookup:
+console.log(myObj.aProperty);
+console.log(myObj['aProperty']);
+
Note that when we declare an object with the const
keyword as above, it is only weakly immutableA property of const-declared objects in JavaScript, where the variable reference is immutable, but the object’s properties can still be changed.
+. This means that we cannot reassign myObj
to refer to a different object, however, we can change the properties inside myObj
. Thus, the myObj
variable is constant/immutable, but the object created by the declaration is mutable. So, after making the above const
declaration, if we try the following reassignment of myObj
we receive an error:
myObj = {
+ aProperty: 0,
+ anotherProperty: "tim wasn’t here"
+};
+
++ +VM48:1 Uncaught TypeError: Assignment to constant variable.
+
But the immutability due to const
is weak or shallow in the sense that while the myObj
variable which references the object is immutable, the properties of the object are mutable, i.e. we can reassign properties on myObj
with no error:
myObj.aProperty = 0;
+
We can also quickly declare variables that take the values of properties of an object, through destructuring syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. +:
+ +const {aProperty} = myObj;
+console.log(aProperty);
+123
+
Which is equivalent to:
+ +const aProperty = myObj.aProperty;
+
This is most convenient to destructure objects passed as arguments to functions. It makes it clear from the function definition precisely which properties are going to be accessed. Consider:
+ +const point = {x:123, y:456};
+function showX({x}) {
+ console.log(x);
+}
+
You can also initialise an object’s properties directly with variables. Unless a new property name is specified, the variable names become property names, like so:
+ +const x = 123, tempY = 456;
+const point = {x /* variable name used as property name */,
+ y:tempY /* value from variable but new property name */};
+point
+
++ +{x: 123, y: 456}
+
JavaScript has Python-like syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. + for array objects:
+ +const a = [1,2,3];
+a.length
+
++ +3
+
a[0]
+
++ +1
+
a[2]
+
++ +3
+
You can also destructure arrays into local variables:
+ +const [x,y,z] = a;
+z
+
++ +3
+
Below, we see how Anonymous FunctionsA function defined without a name, often used as an argument to other functions. Also known as lambda function. + can be applied to transform arrays, and a cheatsheet summary for further functions for working with arrays.
+ +The members of myObj
are implicitly typed as number
and string
respectively, and as we see in the console.log
, conversion to string happens automatically. JavaScript is interpreted by a JavaScript engine rather than compiled into a static executable format. Originally, this had implications on execution speed, as interpreting the program line by line at run time could be slow. Modern JavaScript engines, however, feature Just in Time (JIT) compilation and optimisation—and speed can sometimes be comparable to execution of C++ code that is compiled in advance to native machine code. However, another implication remains. JavaScript is not type checked by a compiler. Thus, type errors cause run-time failures rather than being caught at compile time. JavaScript is dynamically typed in that types are associated with values rather than variables. That is, a variable that is initially bound to one type, can later be rebound to a different type, e.g.:
let i = 123; // a numeric literal has type number
+i = 'a string'; // a string literal has type string, but no error here!
+
The C compiler would spit the dummy when trying to reassign i
with a value of a different type, but the JavaScript interpreter is quite happy to go along with your decision to change your mind about the type of i
.
The nifty thing about JavaScript—one Scheme’ish thing that presumably survived from Eich’s original plan—is that functions are also just objects. That is, given the following function:
+ +function sayHello(person) {
+ console.log('hello ' + person);
+}
+sayHello('tim');
+
++ +“hello tim”
+
We can easily bind a function to a variable:
+ +const hi = sayHello;
+hi('tim');
+
++ +“hello tim”
+
You can actually do the same in Python
+ +def sayHello(person):
+ print('hello ' + person)
+sayHello('tim')
+hi = sayHello
+hi('tim')
+
++ +“hello tim” +“hello tim”
+
The sayHello
function is called a named function. We can also create an anonymous functionA function defined without a name, often used as an argument to other functions. Also known as lambda function.
+ to be bound immediately to a variable:
const hi = function(person) {
+ console.log("hello " + person);
+};
+
or to pass as a parameter into another function. For example, Array
objects have a forEach
member that expects a function as an argument, which is then applied to every member of the array:
['tim', 'sally', 'anne'].forEach(function(person) {
+ console.log('hello ' + person);
+})
+
++ +“hello tim”
+
+“hello sally”
+“hello anne”
This pattern of passing functions as parameters to other functions is now so common in JavaScript that the EcmaScript 6 standard introduced some new arrow syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. + (with slightly different semanticsThe processes a computer follows when executing a program in a given language. +, as explained below) for anonymous functionsA function defined without a name, often used as an argument to other functions. Also known as lambda function. +:
+ +['tim', 'sally', 'anne'].forEach(person=> console.log('hello ' + person))
+
Note that whatever value the expression on the right-hand side of the arrow evaluates to is implicitly returned. Here, we use the map
array method to create a new array from applying the provided function to each element:
['tim', 'sally', 'anne'].map(person=> "hello " + person)
+
++ +[“hello tim”, “hello sally”, “hello anne”]
+
Multiple statements (either split across lines or separated with ;
s) including local variable declarations can be enclosed in brackets with arrow syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language.
+, but then an explicit return
statement is required to return a value:
['tim', 'sally', 'anne'].map(person=> {
+ const message = "hello " + person;
+ console.log(message);
+ return message;
+})
+
As mentioned above, ES6 introduced compact notation for anonymous functionsA function defined without a name, often used as an argument to other functions. Also known as lambda function. +:
+ +const greeting = person=> 'hello' + person;
+
Which is completely equivalent to the long form:
+ +const greeting = function(person) {
+ return 'hello ' + person;
+}
+
You can also have functions with a list of arguments, just put the list in brackets as for usual function definitions:
+ +const greeting = (greeting, person)=> greeting + ' ' + person
+
This is equivalent to using lambda
in Python:
greeting = lambda greeting, person: greeting + ' ' + person
+
The body of the above functions are simple expressions. If you need a more complex, multiline body (e.g. with local variables) you can do this but you need to surround the code block with curly braces {}
and use an explicit return
statement:
const greeting = (greeting, person)=> {
+ const msg = greeting + ' ' + person
+ console.log(msg)
+ return msg
+};
+
We can use multi-parameter anonymous functionsA function defined without a name, often used as an argument to other functions. Also known as lambda function.
+ with another nifty method on Array
objects which allows us to reduce
them to a single value.
[5,8,3,1,7,6,2].reduce((accumulator,x)=>accumulator+x,0)
+
++ +32
+
The reduce
method applies a function to each of the elements in the array in order to compute an aggregated value for the whole array. The nature of the aggregate depends on the function you pass in. Here we just sum the elements in the array. The function we pass in has two parameters, the second is the array element (which we refer to here as x
), the first parameter accumulator
is either:
reduce
(which in our case is 0), if this is the first call to the function,reduce
is incredibly versatile and can be used to do much more than sum numbers. For example, say we want to see whether all the elements of an array pass some test.
const all = (test, array) => array.reduce(
+ (accumulator, x) => accumulator && test(x),
+ true);
+
We call test
a predicate function, i.e., a function which returns true or false. Here the accumulator
is a boolean with initial value true
. If an element of the array fails the test the accumulator
becomes false
and stays false
, using the &&
operator.
all(x => x < 5, [1, 2, 3]);
+all(x => x < 5, [1, 3, 5]);
+
++ +true
+
++ +false
+
Note: Instead of writing our own all
function, we could have used the builtin every
array method instead:
[1, 2, 3].every(x => x < 5);
+[1, 3, 5].every(x => x < 5);
+
Can you write a function any
(without using the builtin some
array method) that returns true if any of the tests pass?
What if we wanted to see how many times each word appears in a list?
+any
is similar to reduce
, however, we use the or operator (||
):
const any = (test, array) => array.reduce(
+ (accumulator, x) => accumulator || test(x),
+ false);
+
const wordCount = (array) => array.reduce(
+ (accumulator, word) => {
+ if (word in accumulator) {
+ accumulator[word] += 1
+ } else {
+ accumulator[word] = 1
+ }
+ return accumulator
+ },
+ {}
+)
+
Here the accumulator
is an object which is initially empty. For each word in the list the word count is either updated or created in the accumulator
object. Note however that this implementation is not pure; the aggregator function modifies accumulator
in place before returning it.
wordCount(['tim', 'sally', 'tim'])
+
++ +{ tim: 2, sally: 1 }
+
We can modify this to be pure:
+ +const wordCount = (array) => array.reduce(
+ (accumulator, word) => ({
+ ...accumulator,
+ [word]: (word in accumulator ? accumulator[word] : 0) + 1
+ }),
+ {}
+);
+
[word]
when used inside an object literal is an example of a computed property name. It allows you to set an object’s property name based on the value of a variable.
In the following, the annotations beginning with :
describe the type of each parameter and the return type of the function. The array a
has elements of type U
, and U=>V
is the type of a function with input parameter type U
and return type V
.
+(Note: these are not correct TS annotations, but an informal “shorthand”.)
a.forEach(f: U=> void): void // apply the function f to each element of the array
+
Although it does not typically mutate a
, forEach
is impure if f
has any side effect (which it most likely will because otherwise why would you bother!).
// Copy the whole array
+a.slice(): U[]
+// Copy from the specified index to the end of the array
+a.slice(start: number): U[]
+// Copy from start index up to (but not including) end index
+a.slice(start: number, end: number): U[]
+
+// Returns a new array with the elements of b concatenated after the
+// elements of a
+a.concat(b: U[]): U[]
+// Returns b and c appended to a. Further arrays can be passed after c
+a.concat(b: U[], c: U[]): U[]
+
+// Apply f to elements of array and return result in new array of type
+// V
+a.map(f: U=> V): V[]
+
+// Returns a new array of the elements of a for which f returns true
+a.filter(f: U=> boolean): U[]
+// Returns the first element of a for which f returns true, or
+// undefined if there is no such element
+a.find(f: U=> boolean): U | undefined
+// Returns whether all elements satisfy the function f
+a.every(f: U=> boolean): boolean
+// Returns whether at least one element satisfies the function f
+a.some(f: U=> boolean): boolean
+
+// Uses f to combine elements of the array into a single result of
+// type V
+a.reduce(f: (V, U)=> V, V): V
+
All of the above are pure in the sense that they do not mutate a
, but return the result in a new object.
Note: the function passed to forEach
takes an optional second parameter (not shown above) which is the index of the element being visited (see docs). While map
, filter
, reduce
, every
, & some
have a similar optional index parameter, I suggest to avoid using it because it leads to hacky imperativeImperative programs are a sequence of statements that change a programs state. This is probably the dominant paradigm for programming languages today. Languages from Assembler to Python are built around this concept and most modern languages still allow you to program in this style.
+ style thinking about loops.
Functions can be nested inside other function definitions and can access variables from the enclosing scope.
+ +Definitions:
+ +You can also have a function that creates and returns a closureA function and the set of variables it accesses from its enclosing scope. + that can be applied later:
+ +function add(x) {
+ return y => y+x; // we are going to return a function which includes
+ // the variable x from its enclosing scope
+ // - “a closure”
+}
+const addNine = add(9)
+addNine(10)
+
++ +19
+
In the above example, the parameter x
of the add
function is captured by the anonymous functionA function defined without a name, often used as an argument to other functions. Also known as lambda function.
+ that is returned, which forms a closureA function and the set of variables it accesses from its enclosing scope.
+. Thus, the binding of x
to a value persists beyond the scope of the add
function itself. Effectively, we have used the add
function to create a new function: y=>y+9
—without actually writing the code ourselves.
addNine(1)
+
++ +10
+
We can also call the add function with two arguments at once:
+ +add(1)(2)
+
++ +3
+
Functions like add
, which operate on multiple parameters but which split the parameters across multiple nested single parameter functions, are said to be Curried. Compare to a traditional function of two parameters:
function plus(x,y) { return x + y }
+plus(1,2)
+
++ +3
+
The add
function above is a curried version of the plus
function. We will discuss curried functions more when we more formally introduce higher-order functionsA function that takes other functions as arguments or returns a function as its result.
+ in a later chapter.
Note that functions that are curried can be written in either arrow syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language.
+ or using the function
keyword or a mix, as above. The following versions of add
are completely equivalent to the one above:
function add(x) {
+ return function(y) {
+ return x+y
+ }
+}
+
+const add = x=>y=>x+y
+
As another example, consider a curried wrapper for our sumTo
from before:
function sumOf(f) {
+ return n => sumTo(n, f)
+}
+
Now, we can create custom functions that compute sums over arbitrary sequences:
+ +const sumOfSquares = sumOf(square)
+sumOfSquares(10)
+
++ +385
+
sumOfSquares(20)
+
++ +2870
+
Note: the following way to achieve class encapsulation is deprecated by ES6 syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. +—skip to the next section to see the modern way to do it.
+ +In JavaScript you can also create functions as members of objects:
+ +const say = {
+ hello: person => console.log('hello ' + person)
+}
+say.hello("tim")
+
++ +“hello tim”
+
However, these objects are only single instances.
+JavaScript supports creating object instances of a certain type (i.e. having a set of archetypical members, like a Java class) through a function prototype mechanism. You create a constructor function:
function Person(name, surname) {
+ this.name = name
+ this.surname = surname
+}
+const author = new Person('tim', 'dwyer')
+sayHello(author.name)
+
++ +“hello tim”
+
You can also add method functions to the prototype, that are then available from any objects of that type:
+ +Person.prototype.hello = function() { console.log("hello " + this.name) }
+author.hello()
+
++ +“hello tim”
+
Note that above we use the old-style verbose JavaScript anonymous functionA function defined without a name, often used as an argument to other functions. Also known as lambda function.
+ syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language.
+ instead of the arrow form. This is because there is a difference in the way the two different forms treat the this
symbol. In the arrow syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language.
+, this
refers to the enclosing execution context. In the verbose syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language.
+, this
resolves to the object the method was called on.
It’s very tempting to use the prototype editing mechanism for evil. For example, I’ve always wished that JS had a function to create arrays initialised over a range:
+ +Array.prototype.range =
+ (from, to)=>Array(to) // allocate space for an array of size `to`
+ .fill() // populate the array (with `undefined`s)
+ .map((_,i)=>i) // set each element of the array to its index
+ .filter(v=> v >= from) // filter out values below from
+
+[].range(3,9)
+
++ +[3,4,5,6,7,8]
+
Of course, if you do something like this in your JS library, and it pollutes the global namespace, and one day EcmaScript 9 introduces an actual range
function with slightly different semanticsThe processes a computer follows when executing a program in a given language.
+, and someone else goes to use the [].range
function expecting the official semanticsThe processes a computer follows when executing a program in a given language.
+—well, you may lose a friend or two.
Some notes about this implementation of range:
+ +Array(n)
function allocates space for n elements, the result is still “empty” so fill()
is necessary to actually create the entries.map
is using an optional second argument which receives the index of the current element. See note in the Array Cheatsheat suggesting not to use this._
is not special syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language.
+, it’s a valid variable name. _
is a common convention for a parameter that is not used. This is seen throughout various languages such as Python, Javascript and Haskell.to - from
from the start, eliminating the need for filter
.Array.prototype
(you’ll need to use an old style anonymous functionA function defined without a name, often used as an argument to other functions. Also known as lambda function.
+ to access the array through this
).Array.prototype.range = (from, to) =>
+ Array(to - from) // Correctly size the array
+ .fill()
+ .map((_, i) => i + from);
+
Adding a sum function on Array.prototype
can be done using an old style anonymous functionA function defined without a name, often used as an argument to other functions. Also known as lambda function.
+ to access this
which refers to the array instance:
Array.prototype.sum = function() {
+ return this.reduce((acc, val) => acc + val, 0);
+};
+
Modifying built-in types, like adding functions to Array.prototype
, can lead to several issues, e.g., compatibility issues—if future versions of JavaScript add a method with the same name but different behavior, it can break your or others’ code unexpectedly, and conflicts—if different libraries try to modify the same prototype with different implementations, it can lead to conflicts that are hard to diagnose.
One possible implementation of a linked list, is storing two values in an array, the current value and the next value.
+ +function createNode(value, next = null) {
+ return () => next ? [value, next] : [value];
+}
+
+const list = createNode(1, createNode(2, createNode(3)))
+
+function length(list) {
+ if (!list) return 0;
+ const result = list();
+ return 1 + (result[1] ? length(result[1]) : 0);
+}
+
+function map(f, list) {
+ if (!list) return null;
+ const [value, next] = list();
+ return createNode(f(value), next ? map(next, f) : null);
+}
+
+
Consider another class created with a function and a method added to the prototype:
+ +function Person(name, occupation) {
+ this.name = name
+ this.occupation = occupation
+}
+Person.prototype.sayHello = function() {
+ console.log(`Hi, my name’s ${this.name} and I ${this.occupation}!`)
+}
+const tim = new Person("Tim","lecture Programming Paradigms")
+tim.sayHello()
+
++ +Hi, my name’s Tim and I lecture Programming Paradigms!
+
ES6 introduced a new syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. + for classes that will be more familiar to Java programmers:
+ +class Person {
+ constructor(name, occupation) {
+ this.name = name
+ this.occupation = occupation
+ }
+ sayHello() {
+ console.log(`Hi, my name’s ${this.name} and I ${this.occupation}!`)
+ }
+}
+
The equivalent in Java is:
+ +public class Person {
+ private String name;
+ private String occupation;
+
+ public Person(String name, String occupation) {
+ this.name = name;
+ this.occupation = occupation;
+ }
+
+ public void sayHello() {
+ System.out.println("Hi, my name's " + this.name + " and I " + this.occupation + "!");
+ }
+}
+
And Python:
+ +class Person:
+ def __init__(self, name, occupation):
+ self.name = name
+ self.occupation = occupation
+
+ def say_hello(self):
+ print(f"Hi, my name's {self.name} and I {self.occupation}!")
+
There is also now syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language.
+ for “getter properties”: functions which can be invoked without ()
, i.e. to look more like properties:
class Person {
+ constructor(name, occupation) {
+ this.name = name
+ this.occupation = occupation
+ }
+ get greeting() {
+ return `Hi, my name’s ${this.name} and I ${this.occupation}!`
+ }
+ sayHello() {
+ console.log(this.greeting)
+ }
+}
+
This is similar to the @property
decorator you may have seen in Python:
class Person:
+ def __init__(self, name, occupation):
+ self.name = name
+ self.occupation = occupation
+
+ @property
+ def greeting():
+ return f"Hi, my name's {self.name} and I {self.occupation}!"
+
+ def say_hello(self):
+ print(self.greeting)
+
And Javascript classes support single-inheritance to achieve polymorphism:
+ +class LoudPerson extends Person {
+ sayHello() {
+ console.log(this.greeting.toUpperCase())
+ }
+}
+
+const tims = [
+ new Person("Tim","lecture Programming Paradigms"),
+ new LoudPerson("Tim","shout about Programming Paradigms")
+]
+
+tims.forEach(t => t.sayHello())
+
++ +Hi, my name’s Tim and I lecture Programming Paradigms!
+
+HI, MY NAME’S TIM AND I SHOUT ABOUT PROGRAMMING PARADIGMS!
According to Cartelli et al., “Polymorphic types are types whose operations are applicable to values of more than one type.” Thus, although Person
and LoudPerson
are different types, since LoudPerson
is a sub-type of Person
, they present a common sayHello
method allowing operations like forEach
to operate over an array of the base class. In a traditional Object Oriented language like Java, the compiler enforces that objects must be instances of a common base class or interface to be treated as such. This type of polymorphism is called subtyping polymorphism.
In JavaScript, with no compile-time typecheck, a kind of polymorphism is possible such that if two objects both present a similarly named method that is callable in the same way, of course there is nothing preventing you simply using that method on each object as if it is the same:
+ +const a = {f: ()=>console.log("a")}
+const b = {f: ()=>console.log("b")}
+[a,b].forEach(o=>o.f())
+
++ +a
+
+b
Informally, this type of polymorphism is called “Duck Typing” (i.e. “If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck”).
+ +Another type of polymorphism which is key to strongly typed functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + programming languages (like Haskell), but also a feature of many modern OO languages is parametric polymorphismA type of polymorphism where functions or data types can be written generically so that they can handle values uniformly without depending on their type. +. We will see this in action when we introduce TypeScript generics.
+ +Reference: Cardelli, Luca, and Peter Wegner. “On understanding types, data abstraction, and polymorphism.” ACM Computing Surveys (CSUR) 17.4 (1985): 471-523.
+ +It’s useful to compare the above style of polymorphism to a functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + approach to dependency injection:
+ +class Person {
+ constructor(name, occupation, voiceTransform = g => g) {
+ this.name = name
+ this.occupation = occupation
+ this.voiceTransform = voiceTransform
+ }
+ get greeting() {
+ return `Hi, my name’s ${this.name} and I ${this.occupation}!`
+ }
+ sayHello() {
+ console.log(this.voiceTransform(this.greeting))
+ }
+}
+const tims = [
+ new Person("Tim", "lecture Programming Paradigms"),
+ new Person("Tim", "shout about Programming Paradigms", g => g.toUpperCase())
+]
+tims.forEach(t => t.sayHello())
+
++ +Hi, my name’s Tim and I lecture Programming Paradigms!
+
+HI, MY NAME’S TIM AND I SHOUT ABOUT PROGRAMMING PARADIGMS!
So the filter property defaults to the identity function (a function which simply returns its argument), but a user of the Person
class can inject a dependency on another function from outside the class when they construct an instance.
This is a “lighter-weight” style of code reuse or specialisation.
+ +Anonymous Function: A function defined without a name, often used as an argument to other functions. Also known as lambda function.
+ +Closure: A function and the set of variables it accesses from its enclosing scope.
+ +Currying: The process of transforming a function that takes multiple arguments into a sequence of functions that each take a single argument.
+ +Higher-Order Function: A function that takes other functions as arguments or returns a function as its result.
+ +Immutable Variable: A variable declared with const whose value cannot be reassigned.
+ +Mutable Variable: A variable declared with let that can be reassigned to different values.
+ +Parametric Polymorphism: A type of polymorphism where functions or data types can be written generically so that they can handle values uniformly without depending on their type.
+ +Weakly Immutable: A property of const-declared objects in JavaScript, where the variable reference is immutable, but the object’s properties can still be changed.
+ +Pure Function: A function that always produces the same output for the same input and has no side effects.
+ +Referential Transparency: An expression that can be replaced with its value without changing the program’s behavior, indicating no side effects and consistent results.
+ +Side Effects: Any state change that occurs outside of a function’s local environment or any observable interaction with the outside world, such as modifying a global variable, writing to a file, or printing to a console.
+ ++ + + 24 + + min read
+The Lambda CalculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s. + is a model of computation developed in the 1930s by the mathematician Alonzo Church. You are probably aware of the more famous model for computation developed around the same time by Alan Turing: the Turing MachineA model of computation based on a hypothetical machine reading or writing instructions on a tape, which decides how to proceed based on the symbols it reads from the tape. +. However, while the Turing MachineA model of computation based on a hypothetical machine reading or writing instructions on a tape, which decides how to proceed based on the symbols it reads from the tape. + is based on a hypothetical physical machine (involving tapes from which instructions are read and written) the Lambda CalculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s. + was conceived as a set of rules and operations for function abstraction and application. It has been proven that, as a model of computation, the Lambda CalculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s. + is just as powerful as Turing MachinesA model of computation based on a hypothetical machine reading or writing instructions on a tape, which decides how to proceed based on the symbols it reads from the tape. +, that is, any computation that can be modelled with a Turing MachineA model of computation based on a hypothetical machine reading or writing instructions on a tape, which decides how to proceed based on the symbols it reads from the tape. + can also be modeled with the Lambda CalculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s. +.
+ +The Lambda CalculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s. + is also important to study as it is the basis of functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + programming. The operations we can apply to Lambda CalculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s. + expressions to simplify (or reduce) them, or to prove equivalence, can also be applied to pure functionsA function that always produces the same output for the same input and has no side effects. + in a programming language that supports higher-order functionsA function that takes other functions as arguments or returns a function as its result. +.
+ +Lambda CalculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s. + expressions are written with a standard system of notation. It is worth looking at this notation before studying haskell-like languages because it was the inspiration for Haskell syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. +. Here is a simple Lambda Abstraction of a function:
+ +λx.x
+
+
+The λ
(Greek letter Lambda) simply denotes the start of a function expression. Then follows a list of parameters (in this case we have only a single parameter called x
) terminated by .
. After the .
is the function body, an expression returned by the function when it is applied. A variable like x
that appears in the function body and also the parameter list is said to be bound to the parameter. Variables that appear in the function body but not in the parameter list are said to be free. The above lambda expression is equivalent to the JavaScript expression:
x => x
+
When we discussed combinatorsA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments. + in JavaScript, we gave this function a name. What was it?
+ +Some things to note about such lambda expressionsFunctions written using the λ notation, e.g., λx.x, which are anonymous and can only take on other functions as values. +:
+ +λx.x
is semantically equivalent (or alpha equivalent) to λy.y
or any other possible renaming of the variable.λxy. x y
, but they are implicitly curried (e.g. a sequence of nested univariate functions). Thus the following are all equivalent:λxy.xy
+= λx.λy.xy
+= λx.(λy.xy)
+
+
+We have already discussed combinatorsA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments. + in JavaScript, now we can give them a more formal definition:
+ +Thus, the expression λx.x
is a combinatorA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments.
+ because the variable x
is bound to the parameter. The expression λx.xy
is not a combinatorA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments.
+, because y
is not bound to any parameter, it is free.
The K
combinatorA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments.
+ which we wrote as x=>y=>x
in JavaScript, is written λxy.x
.
What can we do with such a lambda expression? Well we can apply it to another expression (The same way we can apply anonymous functionsA function defined without a name, often used as an argument to other functions. Also known as lambda function.
+ to an argument in JavaScript). Here, we apply the lambda (λx.x)
to the variable y
:
(λx.x)y
+
+
+Note that while in JavaScript application of a function (x=>x)
to an argument y
requires brackets around the argument: (x=>x)(y)
, in the Lambda CalculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s.
+ application of some expression f
to some other expression x
is indicated simply fx
. Brackets are required to delineate the start and end of an expression, e.g. in (λx.x)y
, the brackets make it clear that y
is not part of the lambda λx.x
, but rather the lambda is being applied to y
.
We can reduce this expression to a simpler form by a substitution, indicated by a bit of intermediate notation. Two types of annotations are commonly seen, you can use either (or both!):
+ +x [x:=y] -- an annotation on the right of the lambda body showing the substitution that will be applied to the expression on the left
+(λx [x:=y].x) -- an annotation inside the parameter list showing the substitution that will be performed inside the body (arguments have already been removed)
+
+
+Now we perform the substitution in the body of the expression and throw away the head, since all the bound variables are substituted, leaving only:
+ +y
+
+
+This first reduction rule, substituting the arguments of a function application to all occurrences of that parameter inside the function body, is called beta reductionSubstituting the arguments of a function application into the function body. +.
+ +The next rule arises from the observation that, for some lambda term M
that does not involve x
:
λx.Mx
+
+
+is just the same as M. This last rule is called eta conversionSubstituting functions that simply apply another expression to their argument with the expression in their body. This is a technique in Haskell and Lambda Calculus where a function f x is simplified to f, removing the explicit mention of the parameter when it is not needed. +.
+ +Function application is left-associative except where terms are grouped together by brackets. This means that when a Lambda expression involves more than two terms, BETA reductionSubstituting the arguments of a function application into the function body. + is applied left to right, i.e.,
+ +(λz.z) (λa.a a) (λz.z b) = ( (λz.z) (λa.a a) ) (λz.z b)
.
Three operations can be applied to lambda expressionsFunctions written using the λ notation, e.g., λx.x, which are anonymous and can only take on other functions as values. +:
+ +Alpha EquivalenceRenaming variables in lambda expressions as long as the names remain consistent within the scope. +: variables can be arbitrarily renamed as long as the names remain consistent within the scope of the expression.
+ +λxy.yx = λwv.vw
+
+
+ Beta ReductionSubstituting the arguments of a function application into the function body. +: functions are applied to their arguments by substituting the text of the argument in the body of the function.
+ +(λx. x) y
+= (λx [x:=y]. x) - we indicate the substitution that is going to occur inside []
+= x [x:=y] - an alternative way to show the substitution
+= y
+
+
+ Eta ConversionSubstituting functions that simply apply another expression to their argument with the expression in their body. This is a technique in Haskell and Lambda Calculus where a function f x is simplified to f, removing the explicit mention of the parameter when it is not needed. +: functions that simply apply another expression to their argument can be substituted with the expression in their body.
+ +λx.Mx
+= M
+
+
+One thing to note about the lambda calculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s. + is that it does not have any such thing as a global namespace. All variables must be:
+ +This makes the language and its evaluation very simple. All we (or any hypothetical machine for evaluating lambda expressionsFunctions written using the λ notation, e.g., λx.x, which are anonymous and can only take on other functions as values. +) can do with a lambda is apply the three basic alpha, beta and eta reduction and conversion rules. Here’s a fully worked example of applying the different rules to reduce an expression until no more Beta reductionSubstituting the arguments of a function application into the function body. + is possible, at which time we say it is in beta normal form:
+ +(λz.z) (λa.a a) (λz.z b)
+⇒
+((λz.z) (λa.a a)) (λz.z b) => Function application is left-associative
+⇒
+(z [z:=λa.a a]) (λz.z b) => BETA Reduction
+⇒
+(λa.a a) (λz.z b)
+⇒
+a a [a:=λz.z b] => BETA Reduction
+⇒
+(λz.z b) (λz.z b)
+⇒
+z b [z:=(λz.z b)] => BETA Reduction
+⇒
+(λz.z b) b
+⇒
+z b [z:=b] => BETA Reduction
+⇒
+b b => Beta normal form, cannot be reduced again.
+
+
+Note, sometimes I add extra spaces as above just to make things a little more readable - but it doesn’t change the order of application, indicate a variable is not part of a lambda to its left (unless there is a bracket) or have any other special meaning.
+ +And yet, this simple calculus is sufficient to perform computation. Alonzo Church demonstrated that we can model any of the familiar programming language constructs with lambda expressionsFunctions written using the λ notation, e.g., λx.x, which are anonymous and can only take on other functions as values. +. For example, Booleans:
+ +TRUE = λxy.x = K-combinator
+FALSE = λxy.y = K I
+
+
+Note that we are making use of the K and I combinatorsA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments. + here as we did for the head and rest functions for our cons list, i.e. returning either the first or second parameter to make a choice between two options. Now we can make an IF expression:
+ +IF = λbtf.b t f
+
+
+IF TRUE
returns the expression passed in as t
and IF FALSE
returns the expression passed in as f
. Now we can make Boolean operators:
AND = λxy. IF x y FALSE
+OR = λxy. IF x TRUE y
+NOT = λx. IF x FALSE TRUE
+
+
+And now we can evaluate logical expressions with beta reductionSubstituting the arguments of a function application into the function body. +:
+ +NOT TRUE
+= (λx. IF x FALSE TRUE) TRUE - expand NOT
+= IF x FALSE TRUE [x:=TRUE] - beta reduction
+= IF TRUE FALSE TRUE
+= (λbtf.b t f) TRUE FALSE TRUE - expand IF
+= b t f [b:=TRUE,t:=FALSE,f:=TRUE] - beta reduction
+= TRUE FALSE TRUE
+= (λxy.x) FALSE TRUE - expand TRUE
+= x [x:=FALSE] - beta reduction
+= FALSE
+
+
+Alonzo Church also demonstrated an encoding for natural numbers:
+ +0 = λfx.x = K I
+1 = λfx.f x
+2 = λfx.f (f x)
+
+
+In general a natural number n
has two arguments f
and x
, and iterates f
n
times. The successor of a natural number n
can also be computed:
SUCC = λnfx.f (n f x)
+
+SUCC 2
+= (λnfx.f (n f x)) 2
+= (λfx.f (n f x)) [n:=2]
+= (λfx.f (2 f x))
+= (λfx.f ((λfx.f (f x)) f x))
+= (λfx.f ((f (f x)) [f:=f,x:=x]))
+= (λfx.f (f (f x)))
+= 3
+
+
+cons
, head
and rest
functions as Lambda expressionsFunctions written using the λ notation, e.g., λx.x, which are anonymous and can only take on other functions as values.
+.First, let’s recall the definition of XOR: If either, but not both, of the inputs is true, then the output is true.
+ + XOR = λxy. IF x (NOT y) y
+
+
+ XOR TRUE FALSE
+
+ = (λxy. IF x (NOT y) y) TRUE FALSE - expand XOR
+ = IF x (NOT y) y [x:=TRUE, y:=FALSE] - beta reduction
+ = IF (TRUE) (NOT FALSE) FALSE
+ = (λbtf.b t f) TRUE (NOT FALSE) FALSE - expand IF
+ = b t f [b:=TRUE,t:=(NOT FALSE),f:=FALSE] - beta reduction
+ = TRUE (NOT FALSE) FALSE
+ = (λxy.x) (NOT FALSE) FALSE - expand TRUE
+ = x [x:=(NOT FALSE), y:=FALSE] - beta reduction
+ = NOT FALSE
+ = (λx. IF x FALSE TRUE) FALSE - expand NOT
+ = IF x FALSE TRUE (x:=FALSE)
+ = IF FALSE FALSE TRUE
+ = (λbtf.b t f) FALSE FALSE TRUE - expand IF
+ = b t f [b:=FALSE,t:=FALSE,f:=TRUE] - beta reduction
+ = FALSE FALSE TRUE
+ = (λxy.y) FALSE TRUE - expand FALSE
+ = y [x:=FALSE, y:=TRUE] - beta reduction
+ = TRUE
+
+
+ XOR TRUE TRUE
+
+ = (λxy. IF x (NOT y) y) TRUE TRUE - expand XOR
+ = IF x (NOT y) y [x:=TRUE, y:=TRUE] - beta reduction
+ = IF (TRUE) (NOT TRUE) TRUE
+ = (λbtf.b t f) TRUE (NOT TRUE) TRUE - expand IF
+ = b t f [b:=TRUE,t:=(NOT TRUE),f:=TRUE] - beta reduction
+ = TRUE (NOT TRUE) TRUE
+ = (λxy.x) (NOT TRUE) TRUE - expand TRUE
+ = x [x:=(NOT TRUE), y:=TRUE] - beta reduction
+ = NOT TRUE
+ = (λx. IF x FALSE TRUE) TRUE - expand NOT
+ = IF x FALSE TRUE (x:=TRUE)
+ = IF TRUE FALSE TRUE
+ = (λbtf.b t f) TRUE FALSE TRUE - expand IF
+ = b t f [b:=TRUE,t:=FALSE,f:=TRUE] - beta reduction
+ = TRUE FALSE TRUE
+ = (λxy.x) FALSE TRUE - expand TRUE
+ = x [x:=FALSE, y:=TRUE] - beta reduction
+ = FALSE
+
+ We can define these just like we did in JavaScript:
+ + CONS = λhrf.f h r
+ HEAD = λl.l(λhr.h)
+ REST = λl.l(λhr.r)
+
+ Recall that a number n
in lambda calculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s.
+ can be represented by a function that takes in a function f
and applies it to a value n
times. Hence, a number m+n
in lambda calculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s.
+ should apply a function f
m+n
times.
We can implement ADD
by applying f
n
times and then applying f
m
times:
ADD = λmnfx.m f (n f x)
+
+
+ For example,
+ + ADD 1 2
+ = (λmnfx.m f (n f x)) (λfx.f x) (λfx. f (f x))
+ = (λfx.m f (n f x)) [m:=(λfx.f x), n:=(λfx. f (f x))]
+ = λfx.(λfx.f x) f ((λfx. f (f x)) f x) - this is the same as λfx.1 f (2 f x)
+ = λfx.(λfx.f x) f ((λx. f (f x)) x)
+ = λfx.(λfx.f x) f (f (f x))
+ = λfx.(λx.f x) (f (f x))
+ = λfx.f (f (f x))
+ = 3
+
+
+ For multiplication, we need a function that applies f
m*n
times. We can do this by creating a function that applies f
n
times (n f
), and then applying that function m
times (m (n f)
):
MULTIPLY = λmnfx.m (n f) x
+
+
+ For example,
+ + MULTIPLY 2 3
+ = (λmnfx.m (n f) x) (λfx. f (f x)) (λfx. f (f (f x)))
+ = (λfx.m (n f) x) [m:=(λfx. f (f x)), n:=(λfx. f (f (f x)))]
+ = λfx.(λfx. f (f x)) ((λfx. f (f (f x))) f) x
+ = λfx.(λfx. f (f x)) (λx. f (f (f x))) x - this is the same as λfx.2 3 x
+ = λfx.(λx. (λx. f (f (f x))) ((λx. f (f (f x))) x)) x
+ = λfx.(λx. f (f (f x)) ((λx. f (f (f x))) x))
+ = λfx.(f (f (f ((λx. f (f (f x))) x))))
+ = λfx.(f (f (f (f (f (f x))))))
+ = 6
+
+ Despite the above demonstration of evaluation of logical expressions, the restriction that lambda expressionsFunctions written using the λ notation, e.g., λx.x, which are anonymous and can only take on other functions as values. + are anonymous makes it a bit difficult to see how lambda calculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s. + can be a general model for useful computation. For example, how can we have a loop? How can we have recursion if a lambda expression does not have any way to refer to itself?
+ +The first hint to how loops might be possible with lambda calculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s. + is the observation that some expressions do not simplify when beta reduced. For example:
+ +( λx . x x) ( λy. y y) - (1)
+x x [x:= y. y y]
+( λy . y y) ( λy. y y) - which is alpha equivalent to what we started with, so goto (1)
+
+
+Thus, the reduction would go on forever. Such an expression is said to be divergent. However, if a lambda function is not able to refer to itself it is still not obvious how recursion is possible.
+ +The answer is due to the American mathematician Haskell Curry and is called the fixed-point or Y combinatorA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments. +:
+ + Y = λf. ( λx . f (x x) ) ( λx. f (x x) )
+
+
+When we apply Y
to another function g
we see an interesting divergence:
Y g = (λf. ( λx . f (x x) ) ( λx. f (x x) ) ) g + = ( λx . f (x x) ) ( λx. f (x x) ) [f:=g] - beta reduction + = ( λx . g (x x) ) ( λx. g (x x) ) - a partial expansion of Y g, remember this… + = g (x x) [ x:= λx. g (x x)] - beta reduction + = g ( (λx. g (x x) ) (λx. g (x x) ) ) - bold part matches Y g above, so now… + = g (Y g) + … more beta reduction as above + … followed by substitution with Y g when we see the pattern above… + = g (g (Y g)) + = g (g (g (Y g))) + … etc ++ +
If we directly translate the above version of the Y-combinatorA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments. + into JavaScript we get the following:
+ +const Y = f=> (x => f(x(x)))(x=> f(x(x))) // warning infinite recursion ahead!
+
So now Y
is just a function which can be applied to another function, but what sort of function do we pass into Y
? If we are to respect the rules of the Lambda calculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s.
+ we cannot have a function that calls itself directly. That is, because Lambda expressionsFunctions written using the λ notation, e.g., λx.x, which are anonymous and can only take on other functions as values.
+ have no name, they can’t refer to themselves by name.
Therefore, we need to wrap the recursive function in a lambda expression into which a reference to the recursive function itself can be passed as parameter. We can then in the body of the function refer to the parameter function by name. +It’s a bit weird, let me just give you a JavaScript function which fits the bill:
+ +// A function that recursively calculates “n!”
+// - but it needs to be reminded of its own name in the f parameter in order to call itself.
+const FAC = f => n => n>1 ? n * f(n-1) : 1
+
Now we can make this function compute factorials like so:
+ +FAC(FAC(FAC(FAC(FAC(FAC())))))(6)
+
++ +720
+
Because we gave FAC a stopping condition, we can call too many times and it will still terminate:
+ +FAC(FAC(FAC(FAC(FAC(FAC(FAC(FAC(FAC()))))))))(6)
+
++ +720
+
From the expansion of Y g = g (g (g (…)))
it would seem that Y(FAC)
would give us the recurrence we need. But will the JavaScript translation of the Y-combinatorA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments.
+ be able to generate this sequence of calls?
console.log(Y(FAC)(6))
+
++ +stack overflow
+
Well we got a recurrence, but unfortunately the JavaScript engine’s strict (or eager) evaluation means that we must completely evaluate Y(FAC) before we can ever apply the returned function to (6).
+Therefore, we get an infinite loop - and actually it doesn’t matter what function we pass in to Y, it will never actually be called and any stopping condition will never be checked.
+How do we restore the laziness necessary to make progress in this recursion?
(Hint: it involves wrapping some part of Y
in another lambda)
Did you get it? If so, good for you! If not, never mind, it is tricky and in fact was the subject of research papers at one time, so I’ll give you a bigger hint.
+ +Bigger hint: there’s another famous combinatorA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments.
+ called Z
which is basically Y
adapted to work with strict evaluation:
Z=λf.(λx.f(λv.xxv))(λx.f(λv.xxv))
+
+
+Y
and Z
and perform a similar set of Beta reductionsSubstituting the arguments of a function application into the function body.
+ on Z FAC
to see how it forces FAC to be evaluated.Z(FAC)(6)
successfully evaluates to 720
.Key ideas:
+ +λv.xxv
) to delay the evaluation of the recursive call. Note that eta-reducing λv.xxv
into xx
would give us the Y-combinatorA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments.
+.const Z = f => (x => f(v => x(x)(v)))(x => f(v => x(x)(v)));
+const FAC = f => n => n > 1 ? n * f(n - 1) : 1;
+console.log(Z(FAC)(6)); // Should print 720
+
Let’s see how Z FAC 2
would evaluate in lambda calculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s.
+:
Z FAC 2
+= (λf. (λx. f (λv.xxv)) (λx. f (λv.xxv))) FAC 2
+= (λx. FAC (λv.xxv)) (λx. FAC (λv.xxv)) 2
+= FAC (λv. (λx. FAC (λv.xxv)) (λx. FAC (λv.xxv)) v) 2
+
+
+Notice how the (λx. FAC (λv.xxv)) (λx. FAC (λv.xxv))
has not been evaluated yet since it is inside the λv
lambda. (If we had used the Y-combinatorA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments.
+, we would have infinite recursion here.)
Since 2 > 1, FAC
returns n * f(n - 1)
, and it is only in the f(n - 1)
call that the (λx. FAC (λv.xxv)) (λx. FAC (λv.xxv))
is evaluated to FAC (λv.(λx. FAC (λv.xxv)) (λx. FAC (λv.xxv)) v)
:
= 2 * (λv. (λx. FAC (λv.xxv)) (λx. FAC (λv.xxv)) v) 1
+= 2 * (λx. FAC (λv.xxv)) (λx. FAC (λv.xxv)) 1
+= 2 * FAC (λv.(λx. FAC (λv.xxv)) (λx. FAC (λv.xxv)) v) 1
+
+
+Now, since we have reached the base case (n ≤ 1), FAC
simply returns 1. (λx. FAC (λv.xxv)) (λx. FAC (λv.xxv))
will not evaluate here since FAC
returns 1 immediately:
= 2 * 1
+= 2
+
+
+Here’s the same thing in JavaScript:
+ +Z(FAC)(2)
+= (f => (x => f(v => x(x)(v)))(x => f(v => x(x)(v))))(FAC)(2)
+= (x => FAC(v => x(x)(v)))(x => FAC(v => x(x)(v)))(2)
+= FAC(v => (x => FAC(v => x(x)(v)))(x => FAC(v => x(x)(v)))(v))(2)
+= 2 * (v => (x => FAC(v => x(x)(v)))(x => FAC(v => x(x)(v)))(v))(1)
+= 2 * (x => FAC(v => x(x)(v)))(x => FAC(v => x(x)(v)))(1)
+= 2 * FAC(v => (x => FAC(v => x(x)(v)))(x => FAC(v => x(x)(v)))(v))(1)
+= 2 * 1
+= 2
+
If you want to dig deeper there is much more written about Lambda CalculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s.
+ encodings of logical expressions, natural numbers, as well as the Y
and Z
combinatorsA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments.
+, and also more about their implementation in JavaScript.
However, the above description should be enough to give you a working knowledge of how to apply the three operations to manipulate Lambda CalculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s. + expressions, as well as an appreciation for how they can be used to reason about combinatorsA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments. + in real-world functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + style curried code. The other important take away is that the Lambda CalculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s. + is a turing-complete model of computation, with Church encodings demonstrating how beta-reduction can evaluate church-encoded logical and numerical expressions and the trick of the Y-combinatorA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments. + giving us a way to perform loops.
+ +Lambda Calculus: Model of computation developed in the 1930s by Alonzo Church, providing a complete model of computation similar to Turing Machines.
+ +Lambda expressions: Functions written using the λ notation, e.g., λx.x, which are anonymous and can only take on other functions as values.
+ +Alpha equivalence: Renaming variables in lambda expressions as long as the names remain consistent within the scope.
+ +Beta reduction: Substituting the arguments of a function application into the function body.
+ +Eta conversion: Substituting functions that simply apply another expression to their argument with the expression in their body. This is a technique in Haskell and Lambda Calculus where a function f x is simplified to f, removing the explicit mention of the parameter when it is not needed.
+ +Combinator: A lambda expression with no free variables.
+ +Divergent lambda expressions: Expressions that do not simplify when beta reduced, leading to infinite loops.
+ ++ + + 9 + + min read
+Usually, expressions in imperativeImperative programs are a sequence of statements that change a programs state. This is probably the dominant paradigm for programming languages today. Languages from Assembler to Python are built around this concept and most modern languages still allow you to program in this style.
+ languages are fully evaluated, each step immediately after the previous step. This is called strict or eager evaluation. Functions, however, give us a way to execute code (or evaluate expressions) only when they are really required. This is called lazy evaluation. As an eager student at Monash University you will be unfamiliar with the concept of laziness, since you always do all your work as soon as possible because you love it so much. This is obviously the best strategy for most things in life (and definitely for studying for this course), but laziness as a programming paradigm can sometimes enable different ways to model problems. Early in our introduction to TypeScript we created a function setLeftPadding
that took as argument, either an immediate value or a function that returns a value (simplifying a bit):
function setLeftPadding(elem: Element, value: string | (()=>string)) {
+ if (typeof value === "string")
+ elem.setAttribute("style", `padding-left:${value}`)
+ else // value must be a function
+ elem.setAttribute("style", `padding-left:${value()}`)
+}
+
We didn’t discuss this much at the time, but this potentially lets us delay the evaluation of an expression. We don’t have to have the value ready at the time of the function call, rather we can provide a computation to obtain the value at a later time.
+ +This is the essence of how functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + languages can elevate laziness to a whole paradigm, and it is a powerful concept. For one thing, it allows us to define infinite lists. Previously we defined an interfaceA TypeScript construct that defines the shape of an object, specifying the types of its properties and methods. + for List nodes:
+ +interface IListNode<T> {
+ data: T;
+ next?: IListNode<T>;
+}
+
Compare the following definition which uses a function to access the next element in the list instead of a simple property:
+ +interface LazySequence<T> {
+ value: T;
+ next():LazySequence<T>;
+}
+
Now we can define infinite sequences:
+ +function naturalNumbers() {
+ return function _next(v:number):LazySequence<number> {
+ return {
+ value: v,
+ next: ()=>_next(v+1)
+ }
+ }(1)
+}
+
Note that _next
is immediately invoked in the return. If you are like me you will need to look at this for a little to get your head around it. To summarise: we are defining a function in a returned expression and immediately invoking it with 1 as the starting value. This pattern is used so often in JavaScript it has an acronym: IIFE or Immediately Invoked Function Expression. It was one way of achieving encapsulation before ES6 introduced proper classes.
const n = naturalNumbers();
+n.value
+
++ +1
+
n.next().value
+
++ +2
+
n.next().next().value
+
++ +3
+
take(n,seq)
which returns a LazySequence
of the first n
elements of an infinite LazySequence
of the sort generated by naturalNumbers
. After returning the n
th element, take
should return undefined
to indicate the end.map
, filter
and reduce
functions (similar to those defined on Array.prototype
) for such a sequence and use them along with take
to create a solution for Project Euler Problem 1 (encountered earlier): sum of first n natural numbers divisible by 3 or 5.naturalNumbers
. Thus, if we call our function initSequence
, then initSequence(n=>n+1)
will return a function equivalent to naturalNumbers
.A live version of the solutions can be accessed here. However, let’s walk through it. Consider the definition of a LazySequence
interface LazySequence<T> {
+ value: T;
+ next():LazySequence<T>;
+}
+
The take function creates a LazySequence of the first n
elements by returning the current value and recursively calling itself with n
decremented and the next element of the sequence until n
reaches 0, at which point it returns undefined
to signify the end.
function take<T>(n: number, seq: LazySequence<T>): LazySequence<T> | undefined {
+ if (n) {
+ return {
+ value: seq.value,
+ next: () => take(n - 1, seq.next()),
+ } as LazySequence<T>;
+ }
+ return undefined;
+}
+
We can define the three map/filter/reduce functions with similar logic.
+ +function map<T, U>(
+ f: (value: T) => U,
+ seq: LazySequence<T> | undefined
+): LazySequence<U> | undefined {
+ if (!seq) {
+ return undefined; // If the sequence is undefined, return undefined
+ }
+ return {
+ value: f(seq.value), // Apply the function to the current value
+ next: () => map(f, seq.next()), // Recursively apply the function to the next elements
+ };
+}
+
function filter<T>(
+ predicate: (value: T) => boolean,
+ seq: LazySequence<T> | undefined
+): LazySequence<T> | undefined {
+ if (!seq) {
+ return undefined; // If the sequence is undefined, return undefined
+ }
+ if (predicate(seq.value)) {
+ return {
+ value: seq.value, // If the current value matches the predicate, include it
+ next: () => filter(predicate, seq.next()), // Recursively filter the next elements
+ };
+ } else {
+ return filter(predicate, seq.next()); // Skip the current value and filter the next elements
+ }
+}
+
function reduce<T, U>(
+ f: (accumulator: U, value: T) => U,
+ seq: LazySequence<T> | undefined,
+ initialValue: U
+): U {
+ if (!seq) {
+ return initialValue; // If the sequence is undefined, return the initial value
+ }
+ // Recursively apply the accumulator function to the next elements and the current value
+ return reduce(f, seq.next(), f(initialValue, seq.value));
+}
+
Using this code, we can solve the first euler problem
+ +function sumOfFirstNNaturalsNotDivisibleBy3Or5(n: number): number {
+ const naturals = naturalNumbers(); // Generate the natural numbers sequence
+ // Take the first n elements and filter out those not divisible by 3 or 5
+ const filtered = filter(
+ (x) => x % 3 === 0 || x % 5 === 0,
+ take(n - 1, naturals)
+ );
+
+ // Sum the remaining elements using reduce
+ return reduce((acc, x) => acc + x, filtered, 0);
+}
+
+// Example usage
+console.log(sumOfFirstNNaturalsNotDivisibleBy3Or5(1000));
+
++ + +233168
+
Eager Evaluation: A strategy where expressions are evaluated immediately as they are bound to variables.
+ +IIFE (Immediately Invoked Function Expression): A JavaScript function that runs as soon as it is defined, used to create local scopes and encapsulate code.
+ +Lazy Evaluation: A strategy where expressions are not evaluated until their values are needed, allowing for the creation of infinite sequences and delayed computations.
+ ++ + + 15 + + min read
+As a branch of Computer Science, the theory and practice of programming has grown from a very practical need: to create tools to help us get computers to perform useful and often complex tasks. Programming languages are tools, that are designed and built by people to make life easier in this endeavour. Furthermore, they are rapidly evolving tools that have grown in subtlety and complexity over the many decades to take advantage of changing computer hardware and to take advantage of different ways to model computation.
+ +An important distinction to make when considering different programming languages, is between syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. + and semanticsThe processes a computer follows when executing a program in a given language. +. The syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. + of a programming language is the set of symbols and rules for combining them (the grammar) into a correctly structured program. These rules are often arbitrary and chosen for historical reasons, ease of implementation or even aesthetics. An example of a syntactic design choice is the use of indentation to denote block structure or symbols such as “BEGIN” and “END” or “{” and “}”.
+ +Example: functions in python and C that are syntactically different, but semantically identical:
+ +# python code
+def sumTo(n):
+ sum = 0
+ i = 0
+ while i < n:
+ sum += i
+ i += 1
+ return sum
+
// C code:
+int sumTo(int n) {
+ int sum = 0;
+ for(int i = 0; i < n; i++) {
+ sum += i;
+ }
+ return sum;
+}
+
By contrast, the “semanticsThe processes a computer follows when executing a program in a given language. +” of a programming language relate to the meaning of a program: how does it structure data? How does it execute or evaluate?
+ +In this course we will certainly be studying the syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. + of the different languages we encounter. It is necessary to understand syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. + in order to correctly compose programs that a compiler or interpreter can make sense of. The syntactic choices made by language designers are also often interesting in their own right and can have significant impact on the ease with which we can create or read programs. However, arguably the more profound learning outcome from this course should be an appreciation for the semanticsThe processes a computer follows when executing a program in a given language. + of programming and how different languages lend themselves to fundamentally different approaches to programming, with different abstractions for modelling problems and different ways of executing and evaluating. Hence, we will be considering several languages that support quite different programming paradigms.
+ +For example, as we move forward we will see that C programs vary from the underlying machine language mostly syntactically. C abstracts certain details and particulars of the machine architecture and has much more efficient syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. + than Assembly language, but the semanticsThe processes a computer follows when executing a program in a given language. + of C are not so far removed from Assembler - especially modern assembler that supports procedures and conveniences for dealing with arrays.
+ +By contrast, Haskell and MiniZinc (which we will be exploring in the second half of this course) represent quite different paradigms where the underlying machine architecture, and even the mode of execution, is significantly abstracted away. MiniZinc, in particular, is completely declarativeDeclarative languages focus on declaring what a procedure (or function) should do rather than how it should do it. + in the sense that the programmer’s job is to define and model the constraints of a problem. The approach to finding the solution, in fact any algorithmic details, are completely hidden.
+ +One other important concept that we try to convey in this course is that while some languages are engineered to support particular paradigms (such as functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + programming or logic programming), the ideas can be brought to many different programming languages. For example, we begin learning about functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + programming in JavaScript (actually TypeScript and EcmaScript 2017) and we will demonstrate that functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + programming style is not only possible in this language, but brings with it a number of benefits.
+ +Later, we will pivot from JavaScript (briefly) to PureScript, a haskell-like language that compiles to JavaScript. We will see that, while PureScript syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. + is very different to Javascript, the JavaScript generated by the PureScript compiler is not so different to the way we implemented functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + paradigms manually in JavaScript.
+ +Then we will dive a little-more deeply into a language that more completely “buys into” the functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + paradigm: Haskell. As well as having a syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. + that makes functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + programming very clean, the haskell compiler strictly enforces purity and makes the interesting choice of being lazy by default.
+ +In summary, the languages we will study (in varying degrees of depth) will be Assembler, C/C++, JavaScript (ES2017 and TypeScript), PureScript, Haskell and MiniZinc, with JavaScript/TypeScript and Haskell being the main languages explored in problem sets and assignments. Thus, this course will be a tour through programming paradigms that represent different levels of abstraction from the underlying machine architecture. To begin, we spend just a little time at the level of least abstraction: the hardware itself.
+ +Conceptually, modern computer architecture deviates little from the von Neumann modelA model of computation which is the basis for most modern computer architectures. Proposed by John von Neumann in 1945.
+ proposed in 1945 by Hungarian-American computer scientist John von Neumann.
+The von Neumann architecture was among the first to unify the concepts of data and programs. That is, in a von Neumann architecture, a program is just data that can be loaded into memory. A program is a list of instructions that read data from memory, manipulate it, and then write it back to memory. This is much more flexible than previous computer designs which had stored the programs separately in a fixed (read-only) manner.
The classic von Neumann modelA model of computation which is the basis for most modern computer architectures. Proposed by John von Neumann in 1945. + looks like so:
+ +At a high-level, standard modern computer architecture still fits within this model:
+ +Programs run on an x86 machine according to the Instruction Execution Cycle:
+ +Registers are locations on the CPU with very low-latency access due to their physical proximity to the execution engine. Modern x86 processors also have 2 or 3 levels of cache memory physically on-board the CPU. Accessing cache memory is slower than accessing registers (with all sorts of very complicated special cases) but still many times faster than accessing main memory. The CPU handles the movement of instructions and data between levels of cache memory and main memory automatically, cleverly and—for the most part—transparently. To cut a long story short, it can be very difficult to predict how long a particular memory access will take. Probably, accessing small amounts of memory repeatedly will be cached at a high-level and therefore fast.
+ +Turing MachinesA model of computation based on a hypothetical machine reading or writing instructions on a tape, which decides how to proceed based on the symbols it reads from the tape. + are a conceptual model of computation based on a physical analogy of tapes being written to and read from as they feed through a machine. The operations written on the tape determine the machine’s operation. The von Neumann modelA model of computation which is the basis for most modern computer architectures. Proposed by John von Neumann in 1945. + of computation has in common with Turing MachinesA model of computation based on a hypothetical machine reading or writing instructions on a tape, which decides how to proceed based on the symbols it reads from the tape. + that it follows an imperativeImperative programs are a sequence of statements that change a programs state. This is probably the dominant paradigm for programming languages today. Languages from Assembler to Python are built around this concept and most modern languages still allow you to program in this style. + paradigm of sequential instruction execution, but it is a practical model upon which most modern computers base their architecture.
+ +There are other models of computation that are also useful. In particular, we will look at the Lambda CalculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s. +, which was roughly a contemporary of these other models but based on mathematical functions, their application and composition. We will see that while imperativeImperative programs are a sequence of statements that change a programs state. This is probably the dominant paradigm for programming languages today. Languages from Assembler to Python are built around this concept and most modern languages still allow you to program in this style. + programming languages are roughly abstractions of machine architecture, functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + programming languages provide an alternative abstraction built upon the rules of the lambda calculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s. +.
+ +Humans think about programs in a different way to machines. +Machines can read thousands or millions of instructions per second and execute them tirelessly and with precision. +Humans need to understand programs at a higher level, and relate the elements of the program back to the semanticsThe processes a computer follows when executing a program in a given language. + of the task and the ultimate goal. +There is a clear and overwhelmingly agreed-upon need to create human-readable and writable languages which abstract away the details of the underlying computation into chunks that people can reason about. However, there are many ways to create these abstractions.
+ +First some definitions which help to describe the different families of programming languages that we will be considering in this course. It is assumed that the reader already has experience with the first three paradigms (imperativeImperative programs are a sequence of statements that change a programs state. This is probably the dominant paradigm for programming languages today. Languages from Assembler to Python are built around this concept and most modern languages still allow you to program in this style. +, proceduralProcedural languages are basically imperative in nature, but add the concept of named procedures, i.e. named subroutines that may be invoked from elsewhere in the program, with parameterised variables that may be passed in. + and objected-oriented) since these are taught in most introductory programming courses. The rest are discussed further in later chapters in these notes.
+ +Until relatively recently there was a fairly clear distinction between languages which followed the FunctionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + Programming (FP) paradigm versus the Object Oriented (OO) paradigm. From the 1990s to… well it still is… Object Oriented programming has been arguably the dominant paradigm. Programmers and system architects have found organising their programs and data into class hierarchies a natural way to model everything from simulations, to games, to business rules. But this focus on creating rigid structures around data is a static view of the world which becomes messy when dealing with dynamic state. FunctionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + programming, however, focuses on functions as transformations of data. These two alternatives, Object-OrientedObject-oriented languages are built around the concept of objects where an objects captures the set of data (state) and behaviours (methods) associated with entities in the system. + versus FunctionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. +, have been described as a Kingdom of Nouns versus a Kingdom of Verbs, respectively, where nouns are descriptions of data and verbs are functions.
+ +Historically these were seen as competing paradigms, leading to quite different languages such as C++ and Haskell. However, modern languages like TypeScript, Scala, C++17, Java >8, Swift, Rust, Python3 etc, provide facilities for both paradigms for you to use (or abuse) as you please.
+ +Thus, these are no longer competing paradigms. Rather they are complementary, or at-least they can be depending on the skill of the programmer (we hope you come away with the skill to recognize the advantages of both paradigms). They provide programmers with a choice of abstractions to apply as necessary to better manage complexity and achieve robust, reusable, scalable and maintainable code.
+ +Furthermore, the use of functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + style code has become more common place throughout both industry and academia, and it is important you become literate in understanding this style of coding and how you can use it as tool to make your code more robust.
+ +These notes focus on introducing programmers who are familiar with the OO paradigm to FP concepts via the ubiquitous multiparadigm language of JavaScript. The following table functions as a summary but also an overview of the contents of these notes with links to relevant sections.
+ ++ | FunctionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + | +Object-OrientedObject-oriented languages are built around the concept of objects where an objects captures the set of data (state) and behaviours (methods) associated with entities in the system. + | +
---|---|---|
Unit of Composition | +Functions | +Objects (classes) | +
Programming Style | +DeclarativeDeclarative languages focus on declaring what a procedure (or function) should do rather than how it should do it. + | +ImperativeImperative programs are a sequence of statements that change a programs state. This is probably the dominant paradigm for programming languages today. Languages from Assembler to Python are built around this concept and most modern languages still allow you to program in this style. + | +
Control Flow | +Functions, recursion and chaining | +Loops and conditionals | +
Polymorphism | +Parametric | +Sub-Typing | +
Data and Behaviour | +Loosely coupled through pure, generic functions | +Tightly coupled in classes with methods | +
State Management | +Treats objects as immutable | +Favours mutation of objects through instance methods | +
Thread Safety | +Pure functions easily used concurrently | +Can be difficult to manage | +
Encapsulation | +Less essential | +Needed to protect data integrity | +
Model of Computation | +Lambda CalculusA model of computation based on mathematical functions proposed by Alonzo Church in the 1930s. + | +ImperativeImperative programs are a sequence of statements that change a programs state. This is probably the dominant paradigm for programming languages today. Languages from Assembler to Python are built around this concept and most modern languages still allow you to program in this style. + (von Neumann/Turing) | +
Syntax: The set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language.
+ +Semantics: The processes a computer follows when executing a program in a given language.
+ +Imperative: Imperative programs are a sequence of statements that change a programs state. This is probably the dominant paradigm for programming languages today. Languages from Assembler to Python are built around this concept and most modern languages still allow you to program in this style.
+ +Procedural: Procedural languages are basically imperative in nature, but add the concept of named procedures, i.e. named subroutines that may be invoked from elsewhere in the program, with parameterised variables that may be passed in.
+ +Object-oriented: Object-oriented languages are built around the concept of objects where an objects captures the set of data (state) and behaviours (methods) associated with entities in the system.
+ +Declarative: Declarative languages focus on declaring what a procedure (or function) should do rather than how it should do it.
+ +Functional: Functional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus.
+ +Lambda Calculus: A model of computation based on mathematical functions proposed by Alonzo Church in the 1930s.
+ +Turing Machine: A model of computation based on a hypothetical machine reading or writing instructions on a tape, which decides how to proceed based on the symbols it reads from the tape.
+ +von Neumann model: A model of computation which is the basis for most modern computer architectures. Proposed by John von Neumann in 1945.
+ ++ + + 28 + + min read
+(>>=)
operation which allows us to sequence effectful operations such that their effects are flattened or joined into a single effect.Maybe
, IO
, List and Function instances of MonadA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context.
+.do
notation.As with FunctorA type class in Haskell that represents types that can be mapped over. Instances of Functor must define the fmap function, which applies a function to every element in a structure.
+ and ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ the name MonadA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context.
+ comes from Category Theory. Although the names sound mathematical they are abstractions of relatively simple concepts. A FunctorA type class in Haskell that represents types that can be mapped over. Instances of Functor must define the fmap function, which applies a function to every element in a structure.
+ allowed unary functions to be applied (mapped/fmap
ed) over a context. ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ allowed us to apply a function in a context to values in a context. So too, MonadA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context.
+ has a characteristic function called “bindthe defining function which all monads must implement.
+”, which allows us to perform another type of function application over values in a context.
The special thing about bindthe defining function which all monads must implement.
+ is that it allows us to chain functions which have an effect without creating additional layers of nesting inside effect contexts. People often try to describe MonadsA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context.
+ in metaphors, which are not always helpful. The essence of MonadA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context.
+ really is bindthe defining function which all monads must implement.
+ and there is no getting around looking at its type signature and seeing what it does in different instances, which we will get to shortly. However, one analogy that resonated for me was the idea of bindthe defining function which all monads must implement.
+ as a “programmable semicolon”. That is, imagine a language like JavaScript which uses semicolons (;
) as a statement separator:
// some javascript you can try in a browser console:
+const x = prompt("Name?"); console.log("Hello "+x)
+
As we will see shortly, the Haskell bindthe defining function which all monads must implement.
+ operator >>=
can also be used to sequence expressions with an IO effect:
getLine >>= \x -> putStrLn("hello "++x)
+
However, it not only separates the two expressions, it is safely handling the IO
type within which all code with IO side-effects in Haskell must operate. But as well as allowing us to chain effectful operations, bindthe defining function which all monads must implement.
+ is defined to do different and useful things for different MonadA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context.
+ instances, as we shall see.
As always, we can interrogate GHCi to get a basic synopsis of the MonadA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context. + typeclass:
+ +> :i Monad
+class Applicative m => Monad (m :: * -> *) where
+ (>>=) :: m a -> (a -> m b) -> m b
+ (>>) :: m a -> m b -> m b
+ return :: a -> m a
+...
+ {-# MINIMAL (>>=) #-}
+ -- Defined in `GHC.Base'
+instance Monad (Either e) -- Defined in `Data.Either'
+instance [safe] Monad m => Monad (ReaderT r m)
+ -- Defined in `transformers-0.5.2.0:Control.Monad.Trans.Reader'
+instance Monad [] -- Defined in `GHC.Base'
+instance Monad Maybe -- Defined in `GHC.Base'
+instance Monad IO -- Defined in `GHC.Base'
+instance Monad ((->) r) -- Defined in `GHC.Base'
+instance Monoid a => Monad ((,) a) -- Defined in `GHC.Base'
+
Things to notice:
+ +Monad
is a subclass of Applicative
(and therefore also a Functor
)return
= pure
, from Applicative
. The return
function exists for historical reasons and you can safely use only pure
(PureScript has only pure
).(>>=)
(pronounced “bindthe defining function which all monads must implement.
+”) is the minimal definition (the one function you must create—in addition to the functions also required for Functor
and Applicative
—to make a new Monad
instance).>>
is a special case of bindthe defining function which all monads must implement.
+ (described below)There also exists a flipped version of bindthe defining function which all monads must implement. +:
+ +(=<<) = flip (>>=)
+
The type of the flipped bindthe defining function which all monads must implement.
+ (=<<)
has a nice correspondence to the other operators we have already seen for function application in various contexts:
(=<<) :: Monad m => (a -> m b) -> m a -> m b
+(<*>) :: Applicative f => f (a -> b) -> f a -> f b
+(<$>) :: Functor f => (a -> b) -> f a -> f b
+($) :: (a -> b) -> a -> b
+
So the bindthe defining function which all monads must implement.
+ function (>>=)
(and equally its flipped version (=<<)
) gives us another way to map functions over contexts—but why do we need another way?
As an example we’ll consider computation using the Maybe
type, which we said is useful for partial functionsFunctions that do not have a mapping for every input, potentially failing for some inputs.
+, that is functions which are not sensibly defined over all of their inputs. A more complex example of such a function than we have seen before is the quadratic formula which, for quadratic functions of the form:
determines two roots:
+ +\[x_1 = \frac{-b + \sqrt{b^2 - 4ac}}{2a} +\quad \text{and} \quad +x_2 = \frac{-b - \sqrt{b^2 - 4ac}}{2a}\] + +This may fail in two ways:
+ +Therefore, let’s define a little library of math functions which encapsulate the possibility of failure in a Maybe
:
safeDiv :: Float -> Float -> Maybe Float
+safeDiv _ 0 = Nothing
+safeDiv numerator denominator = Just $ numerator / denominator
+
+safeSqrt :: Float -> Maybe Float
+safeSqrt x
+ | x < 0 = Nothing
+ | otherwise = Just $ sqrt x -- the built-in square root function
+
Great! Now we can use case
and pattern matchingA mechanism in functional programming languages to check a value against a pattern and to deconstruct data.
+ to make a safe solver of quadratic equations:
safeSolve :: Float -> Float -> Float -> Maybe (Float, Float)
+safeSolve a b c =
+ case safeSqrt $ b*b - 4 * a * c of
+ Just s ->
+ let x1 = safeDiv (-b + s) (2*a)
+ x2 = safeDiv (-b - s) (2*a)
+ in case (x1,x2) of
+ (Just x1', Just x2') -> Just (x1',x2')
+ _ -> Nothing
+ Nothing -> Nothing
+
+> safeSolve 1 3 2
+Just (-1.0,-2.0)
+> safeSolve 1 1 2
+Nothing
+
Actually, not so great, we are having to unpack MaybesA built-in type in Haskell used to represent optional values, allowing functions to return either Just a value or Nothing to handle cases where no value is available.
+ multiple times, leading to nested case
s. This is just two levels of nesting; what happens if we need to work in additional computations that can fail?
The general problem is that we need to chain multiple functions of the form Float -> Maybe Float
. Let’s look again at the type of bindthe defining function which all monads must implement.
+:
> :t (>>=)
+(>>=) :: Monad m => m a -> (a -> m b) -> m b
+
The first argument it expects is a value in a context m a
. What if that we apply it to a Maybe Float
?
> x = 1::Float
+> :t (Just x>>=)
+(Just x>>=) :: (Float -> Maybe b) -> Maybe b
+
So GHCi is telling us that the next argument has to be a function that takes a Float
as input, and gives back anything in a Maybe
. Our safeSqrt
definitely fits this description, as does safeDiv
partially applied to a Float
. So, here’s a safeSolve
which uses (>>=)
to remove the need for case
s:
safeSolve :: Float -> Float -> Float -> Maybe (Float, Float)
+safeSolve a b c =
+ safeSqrt (b*b - 4 * a * c) >>= \s ->
+ safeDiv (-b + s) (2*a) >>= \x1 ->
+ safeDiv (-b - s) (2*a) >>= \x2 ->
+ pure (x1,x2)
+
+> safeSolve 1 3 2
+Just (-1.0,-2.0)
+> safeSolve 1 1 2
+Nothing
+
Note that Haskell has a special notation for such multi-line use of bindthe defining function which all monads must implement.
+, called “do
notation”. The above code in a do
block looks like:
safeSolve a b c = do
+ s <- safeSqrt (b*b - 4 * a * c)
+ x1 <- safeDiv (-b + s) (2*a)
+ x2 <- safeDiv (-b - s) (2*a)
+ pure (x1,x2)
+
So inside a do
-block y<-x
is completely equivalent to x >>= \y -> …
, where in both cases the variable y
is in scope for the rest of the expression. We’ll see more explanation and examples of do
notation below.
How is a Nothing
result from either of our safe
functions handled? Well, the MaybeA built-in type in Haskell used to represent optional values, allowing functions to return either Just a value or Nothing to handle cases where no value is available.
+ instance of MonadA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context.
+ defines bindthe defining function which all monads must implement.
+ like so:
instance Monad Maybe where
+ (Just x) >>= k = k x
+ Nothing >>= _ = Nothing
+
Meaning that anything on the right-hand side of a Nothing>>=
will be left unevaluated and Nothing
returned.
So that’s one instance of Monad
; let’s look at some more…
The Haskell type which captures Input/Output effects is called IO
. As we demonstrated with the traverse
function, it is possible to perform IO
actions using fmap
(<$>
) and applicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ (<*>
)—for example printing to the console. The challenge is taking values out of an IO
context and using them to create further IO
effects.
Here are some simple IO
“actions”:
sayHi :: IO ()
+sayHi = putStrLn "Hi, what’s your name?"
+readName :: IO String
+readName = getLine
+greet :: String -> IO ()
+greet name = putStrLn ("Nice to meet you " ++ name ++ "!")
+
The following typechecks:
+ +main = greet <$> readName
+
When you run it from either GHCi or an executable compiled with ghc, it will pause and wait for input, but you will not see the subsequent greeting. +This is because the type of the expression is:
+ +> :t greet <$> readName
+greet <$> readName :: IO (IO ())
+
The IO
action we want (greet
) is nested inside another IO
action. When it is run, only the outer IO
action is actually executed. The inner IO
computation (action) is not evaluated.
+To see an output we somehow need to flatten the IO (IO ())
into just a single level: IO ()
.
+(>>=)
gives us this ability:
> :t readName >>= greet
+readName >>= greet :: IO ()
+
+> readName >>= greet
+
++ +Tim
+
+Nice to meet you Tim!
The special case of bindthe defining function which all monads must implement.
+ (>>)
allows us to chain actions without passing through a value:
> :t (>>)
+(>>) :: Monad m => m a -> m b -> m b
+
+> sayHi >> readName >>= greet
+
++ +Hi, what’s your name?
+
+Tim
+Nice to meet you Tim!
Haskell gives us syntactic sugar for bindthe defining function which all monads must implement. + in the form of “do blocks”:
+ +main :: IO ()
+main = do
+ sayHi
+ name <- readName
+ greet name
+
Which is entirely equivalent to the above code, or more explicitly:
+ +main =
+ sayHi >>
+ readName >>=
+ \name -> greet name
+
Note that although <-
looks like assignment to a variable name
, it actually expands to a parameter name for a lambda expression following the bindthe defining function which all monads must implement.
+. Thus, the way I read the line with the <-
in the following do expression:
do
+ name <- readName
+ greet name
+
is:
+ +Take the value (a String
in this case)
+out of the MonadA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context.
+ context resulting from the expression on the right-hand side of the <-
(i.e. readName
) and assign it to the symbol on the left-hand side (i.e. name
) which remains in scope until the end of the do
block:
You can also mix in variable assignments from pure expressions using let:
+ +do
+ name <- readName
+ let greeting = "Hello " ++ name
+ putStrLn greeting
+
A function called “join
” from Control.Monad
also distills the essence of Monad
nicely. Its type and definition in terms of bindthe defining function which all monads must implement.
+ is:
join :: Monad m => m (m a) -> m a
+join = (>>= id)
+
We can use join
to flatten nested Maybe
s:
>>> join (Just Nothing)
+Nothing
+>>> join (Just (Just 7))
+Just 7
+
We can apply join to “flatten” the nested IO
contexts from the earlier fmap
example:
> :t join $ greet <$> readName
+join $ greet <$> readName :: IO ()
+
Which will now execute as expected:
+ +join $ greet <$> readName
+
++ +Tim
+
+Nice to meet you Tim!
As with the ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>). + list instance, the list implementation of bindthe defining function which all monads must implement. + is defined with comprehension syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. +:
+ +instance Monad [] where
+ xs >>= f = [y | x <- xs, y <- f x]
+
Where xs
is a list and f
is a function which returns a list. f
is applied to each element of xs
and the result concatenated. Actually, list comprehensions are just syntactic sugar for the list monadA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context.
+ do
notation, for example, [(i,j)|i<-['a'..'d'],j<-[1..4]]
is equivalent to:
do
+ i <- ['a'..'d']
+ j <- [1..4]
+ pure (i,j)
+
++ +[(‘a’,1),(‘a’,2),(‘a’,3),(‘a’,4),(‘b’,1),(‘b’,2),(‘b’,3),(‘b’,4),(‘c’,1),(‘c’,2),(‘c’,3),(‘c’,4),(‘d’,1),(‘d’,2),(‘d’,3),(‘d’,4)]
+
Which is itself syntactic sugar for:
+ +['a'..'d'] >>= \i -> [1..4] >>= \j -> pure (i,j)
+
List comprehensions can also include conditional expressions which must evaluate to true for terms to be included in the list result. For example, we can limit the above comprehension to only pairs with even j
:
[(i,j) | i<-['a'..'d'], j<-[1..4], j `mod` 2 == 0]
+
This comprehension syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language.
+ desugars to a do
-block using the guard
function from Control.Monad
like so:
import Control.Monad (guard)
+
+do
+ i <- ['a'..'d']
+ j <- [1..4]
+ guard $ j `mod` 2 == 0
+ pure (i,j)
+
++ +[(‘a’,2),(‘a’,4),(‘b’,2),(‘b’,4),(‘c’,2),(‘c’,4),(‘d’,2),(‘d’,4)]
+
Our friend join
in the list MonadA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context.
+ is simply concatenation:
>>> join [[1, 2, 3], [1, 2]]
+[1,2,3,1,2]
+
You can think of the way that results in the List monadA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context.
+ are chained as being a logical extension of the way Maybe
monadA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context.
+ operations are chained. That is, a Maybe
returns either zero or one result, while a list returns an arbitrary number of results. The results of two list operations chained with (>>=)
is the cartesian product in a flat list.
We saw previously that functions are instances of Functor
, such that fmap = (.)
. We also saw that functions are Applicative
such that a binary function (such as (+)
) can be lifted over multiple functions that need to be applied to the same argument, e.g.:
totalMark :: Student -> Int
+totalMark = liftA2 (+) exam nonExam
+
So it shouldn’t really be any surprise that functions of the same input type can also be composed with monadic bindthe defining function which all monads must implement.
+.
+The right-to-left bindthe defining function which all monads must implement.
+ (=<<)
takes a binary function f
and a unary function g
and
+creates a new unary function.
+The new function will apply g
to its argument, then give the result as well as the
+original argument to f
.
+OK, that might seem a bit esoteric, but it lets us achieve some nifty things.
For example, below we compute (3*2) + 3
, but we did it by using the argument 3
+in two different functions without explicitly passing it to either!
>>> ((+) =<< (*2)) 3
+9
+
You can imagine a situation where you need to chain together a bunch of functions, but +they all take a common parameter, e.g. a line break character.
+ +greet linebreak = "Dear Gentleperson,"++linebreak
+body sofar linebreak = sofar ++ linebreak ++ "It has come to my attention that… " ++ linebreak
+signoff sofar linebreak = sofar ++ linebreak ++ "Your’s truly," ++ linebreak ++ "Tim" ++ linebreak
+putStrLn $ (greet >>= body >>= signoff) "\r\n"
+
++ +Dear Gentleperson,
+ +It has come to my attention that…
+ +Your’s truly,
+
+Tim
In the next example we use the argument 3
in three different functions without passing it directly to any of them.
+Note the pattern is that the right-most function is unary (taking only the specified argument), and subsequent functions in the chain are binary, their first argument being the result of the previous function application, and the second argument being the given 3
.
>>> ((*) =<< (-) =<< (2*)) 3
+9
+
We can use the flipped bindthe defining function which all monads must implement. + so it can be read left-to-right, if that’s more your thing:
+ +>>> ((2*) >>= (-) >>= (*)) 3
+9
+
The join
function passes one argument to a binary function twice which can be a useful trick:
>>> (join (,)) 3
+(3,3)
+
+>>> (join (+)) 3
+6
+
The very observant reader may recognize above construct of passing one argument to a binary function twice. We previously called this apply
, when discussing Function instances for applicativesA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+. This can be a very useful pattern when making code point free.
We previously gave you an exercise, and labeled it as a scary extension, but now with more tools, we can make this much less scary:
+ +f a b = a*a + b*b
+
First, lets use join to apply the binary function (*
) to the same argument twice
f :: Num a => a -> a -> a
+f a b = (join (*) a) + (join (*) b)
+
One function, you may have been introduced to in your travels is on
:
on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
+
The on
function in Haskell takes a binary function, a unary function, and two arguments. It first applies the unary function to each argument separately, and then applies the binary function to the results of these applications. Using some operator sectioningThe process of partially applying an infix operator in Haskell by specifying one of its arguments. For example, (+1) is a section of the addition operator with 1 as the second argument.
+, we can see our code fits exactly that pattern.
f a b = (+) ((join (*)) a) ((join (*)) b)
+f a b = on (+) (join (*)) a b
+
We can now do two rounds of eta-conversion to make our code point free.
+ +f = on (+) (join (*))
+
By convention, the on
function is normally written as an infix operation, i.e., surrounded by backticks
f :: Num a => a -> a -> a
+f = (+) `on` join (*)
+
This is quite a common pattern in Haskell code, where this code says we apply the +
operation, after applying join (*)
(multiplying by itself) to each argument.
There are also various functions in Control.Monad
for looping functions with monadic effectsOperations that produce side effects and are managed within a monadic context, ensuring that the effects are sequenced and controlled.
+ (functions that return a result inside a MonadA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context.
+) over containers that are Foldable
and Traversable
.
First there’s mapM
which is effectively the same as traverse
(but requires the function to have a monadic effect, not just applicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+):
doubleIfNotBig n = if n < 3 then Just (n+n) else Nothing
+>>> mapM doubleIfNotBig [1,2,3,4]
+Nothing
+>>> mapM doubleIfNotBig [1,2,1,2]
+Just [2,4,2,4]
+
Such monadic looping functions also have versions with a trailing _
in their name, which throw away the actual results computed and just accumulate the effect (internally they use >>
instead of >>=
):
>>> mapM_ doubleIfNotBig [1,2,3,4]
+Nothing
+>>> mapM_ doubleIfNotBig [1,2,1,2]
+Just ()
+
For folks who have been missing imperativeImperative programs are a sequence of statements that change a programs state. This is probably the dominant paradigm for programming languages today. Languages from Assembler to Python are built around this concept and most modern languages still allow you to program in this style.
+ for loops, there is a flipped version of mapM_
, called forM_
:
>>> forM_ ["It","looks","like","JavaScript!"] putStrLn
+It
+looks
+like
+JavaScript!
+
And of course we can fold using functions with Monadic effectsOperations that produce side effects and are managed within a monadic context, ensuring that the effects are sequenced and controlled. +:
+ +small acc x
+ | x < 10 = Just (acc + x)
+ | otherwise = Nothing
+
+>>> foldM small 0 [1..9]
+Just 45
+
+>>> foldM small 0 [1..100]
+Nothing
+
MonadsA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context.
+ really round out Haskell, making it a very powerful language with elegant ways to abstract common programming patterns. So far, we have looked at the Maybe
, IO
, and List
monadA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context.
+ instances. The Maybe
monadA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context.
+ allowed us to chain operations which may fail; IO
allowed us to chain operations which perform input and output; and the List
instance of monadA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context.
+ allows us to sequence operations that may have multiple results (flattening the cartesian product of the results).
We’ll see MonadsA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context.
+ at work again in the next chapter when we build more sophisticated parserA function or program that interprets structured input, often used to convert strings into data structures.
+ combinatorsA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments.
+. Additionally, here’s a discussion about how to thread state such as random seeds through functions using a custom monadic context which serves as an introduction to the builtin State
monadA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context.
+.
With everything we’ve covered so far you should now be empowered to go out and write real-world programs. A slightly more advanced topic which you would soon encounter in the wild would be working within multiple monadic contexts at once. The most standard way to do this is using MonadA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context. + Transformers, but there are other approaches emerging, such as algebraic effects libraries. We’ll leave these for future self exploration though.
+ +Monad: A type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context.
+ +Do Notation: A syntactic sugar in Haskell for chaining monadic operations. It makes the code more readable by hiding the explicit use of bind (»=).
+ +Monadic Effects: Operations that produce side effects and are managed within a monadic context, ensuring that the effects are sequenced and controlled.
+ +bind: the defining function which all monads must implement.
+ ++ + + 43 + + min read
+In this section we will see how the various Haskell language features we have explored allow us to solve real-world problems. In particular, we will develop a simple but powerful library for building parsersA function or program that interprets structured input, often used to convert strings into data structures. + that is compositional through FunctorA type class in Haskell that represents types that can be mapped over. Instances of Functor must define the fmap function, which applies a function to every element in a structure. +, ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>). + and MonadA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context. + interfacesA TypeScript construct that defines the shape of an object, specifying the types of its properties and methods. +. Before this, though, we will learn the basics of parsing text, including a high-level understanding that parsersA function or program that interprets structured input, often used to convert strings into data structures. + are state-machines which realise a context-free grammarA type of formal grammar that is used to define the syntax of programming languages and data formats. CFGs consist of a set of production rules that define how terminals and non-terminals can be combined to produce strings in the language. + over a textual language.
+ +Previously, we glimpsed a very simplistic ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>). + parserA function or program that interprets structured input, often used to convert strings into data structures. +. +In this chapter, a parserA function or program that interprets structured input, often used to convert strings into data structures. + is still simply a function which takes a string as input and produces some structure or computation as output, but now we extend the parserA function or program that interprets structured input, often used to convert strings into data structures. + with monadic “bindthe defining function which all monads must implement. +” definitions, richer error handling and the ability to handle non-trivial grammars with alternativeA type class in Haskell that extends Applicative, introducing the empty and <|> functions for representing computations that can fail or have multiple outcomes. + inputs.
+ +Parsing has a long history and parserA function or program that interprets structured input, often used to convert strings into data structures.
+ combinatorsA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments.
+ are a relatively recent approach made popular by modern functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus.
+ programming techniques.
+A parserA function or program that interprets structured input, often used to convert strings into data structures.
+ combinatorA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments.
+ is a higher-order functionA function that takes other functions as arguments or returns a function as its result.
+ that accepts parsersA function or program that interprets structured input, often used to convert strings into data structures.
+ as input and combines them somehow into a new parserA function or program that interprets structured input, often used to convert strings into data structures.
+.
More traditional approaches to parsing typically involve special purpose programs called parserA function or program that interprets structured input, often used to convert strings into data structures. + generators, which take as input a grammar defined in a special language (usually some derivation of BNF as described below) and generate the partial program in the desired programming language which must then be completed by the programmer to parse such input. ParserA function or program that interprets structured input, often used to convert strings into data structures. + combinatorsA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments. + have the advantage that they are entirely written in the one language. ParserA function or program that interprets structured input, often used to convert strings into data structures. + combinatorsA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments. + written in Haskell take advantage of the expressiveness of the Haskell language such that the finished parserA function or program that interprets structured input, often used to convert strings into data structures. + can look a lot like a BNF grammar definition, as we shall see.
+ +The parserA function or program that interprets structured input, often used to convert strings into data structures. + combinatorA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments. + discussed here is based on one developed by Tony Morris and Mark Hibberd as part of their “System F” FunctionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + Programming Course, which in turn is a simplified version of official Haskell parserA function or program that interprets structured input, often used to convert strings into data structures. + combinatorsA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments. + such as parsec by Daan Leijen.
+ +You can play with the example and the various parserA function or program that interprets structured input, often used to convert strings into data structures. + bits and pieces in this on-line playground.
+ +Fundamental to analysis of human natural language but also to the design of programming languages is the idea of a grammar, or a set of rules for how elements of the language may be composed. A context-free grammarA type of formal grammar that is used to define the syntax of programming languages and data formats. CFGs consist of a set of production rules that define how terminals and non-terminals can be combined to produce strings in the language.
+ (CFG) is one in which the set of rules for what is produced for a given input (production rules) completely cover the set of possible input symbols (i.e. there is no additional context required to parse the input). Backus-Naur FormA notation for expressing context-free grammars. It is used to formally describe the syntax of programming languages.
+ (or BNF) is a notation that has become standard for writing CFGs since the 1960s. We will use BNF notation from now on. There are two types of symbols in a CFG: terminalIn the context of grammars, a terminal is a symbol that appears in the strings generated by the grammar. Terminals are the actual characters or tokens of the language.
+ and non-terminalA symbol in a grammar that can be replaced by a sequence of terminals and non-terminals according to the production rules of the grammar.
+. In BNF non-terminalA symbol in a grammar that can be replaced by a sequence of terminals and non-terminals according to the production rules of the grammar.
+ symbols are <nameInsideAngleBrackets>
and can be converted into a mixture of terminalsIn the context of grammars, a terminal is a symbol that appears in the strings generated by the grammar. Terminals are the actual characters or tokens of the language.
+ and/or nonterminals by production rules:
<nonterminal> ::= a mixture of terminals and <nonterminal>s, alternatives separated by |
+
Thus, terminalsIn the context of grammars, a terminal is a symbol that appears in the strings generated by the grammar. Terminals are the actual characters or tokens of the language.
+ may only appear on the right-hand side of a production rule, non-terminalsA symbol in a grammar that can be replaced by a sequence of terminals and non-terminals according to the production rules of the grammar.
+ on either side. In BNF each non-terminalA symbol in a grammar that can be replaced by a sequence of terminals and non-terminals according to the production rules of the grammar.
+ symbol appears on the left-hand side of exactly one production rule, and there may be several possible alternativesA type class in Haskell that extends Applicative, introducing the empty and <|> functions for representing computations that can fail or have multiple outcomes.
+ for each non-terminalA symbol in a grammar that can be replaced by a sequence of terminals and non-terminals according to the production rules of the grammar.
+ specified on the right-hand side. These are separated by a |
(in this regard they look a bit like the syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language.
+ for algebraic data type definitions).
Note that production rules of the form above are for context-free grammarsA type of formal grammar that is used to define the syntax of programming languages and data formats. CFGs consist of a set of production rules that define how terminals and non-terminals can be combined to produce strings in the language. +. As a definition by counter-example, context sensitive grammars allow terminalsIn the context of grammars, a terminal is a symbol that appears in the strings generated by the grammar. Terminals are the actual characters or tokens of the language. + and more than one non-terminalA symbol in a grammar that can be replaced by a sequence of terminals and non-terminals according to the production rules of the grammar. + on the left hand side.
+ +Here’s an example BNF grammar for parsing Australian land-line phone numbers, which may optionally include a two-digit area code in brackets, and then two groups of four digits, with an arbitrary number of spaces separating each of these, e.g.:
+ +++ +(03) 9583 1762
+
+9583 1762
Here’s the BNF grammar:
+ +<phoneNumber> ::= <fullNumber> | <basicNumber>
+<fullNumber> ::= <areaCode> <basicNumber>
+<basicNumber> ::= <spaces> <fourDigits> <spaces> <fourDigits>
+<fourDigits> ::= <digit> <digit> <digit> <digit>
+<areaCode> ::= "(" <digit> <digit> ")"
+<digit> ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
+<spaces> ::= " " <spaces> | ""
+
So "0"
-"9"
, "("
, ")"
, and " "
are the full set of terminalsIn the context of grammars, a terminal is a symbol that appears in the strings generated by the grammar. Terminals are the actual characters or tokens of the language.
+.
Now here’s a sneak peak at a simple parserA function or program that interprets structured input, often used to convert strings into data structures.
+ for such phone numbers. It succeeds for any input string which satisfies the above grammar, returning a 10-digit string for the full number without spaces and assumes “03” for the area code for numbers with none specified (i.e. it assumes they are local to Victoria). Our Parser
type provides a function parse
which we call like so:
GHCi> parse phoneNumber "(02)9583 1762"
+Result >< "0295831762"
+
+GHCi> parse phoneNumber "9583 1762"
+Result >< "0395831762"
+
+GHCi> parse phoneNumber "9583-1762"
+Unexpected character: "-"
+
We haven’t bothered to show the types for each of the functions in the code below, as they are all ::Parser [Char]
- meaning a ParserA function or program that interprets structured input, often used to convert strings into data structures.
+ that returns a string. We’ll explain all the types and functions used in due course. For now, just notice how similar the code is to the BNF grammar definition:
phoneNumber = fullNumber <|> (("03"++) <$> basicNumber)
+
+fullNumber = do
+ ac <- areaCode
+ n <- basicNumber
+ pure (ac ++ n)
+
+basicNumber = do
+ spaces
+ first <- fourDigits
+ spaces
+ second <- fourDigits
+ pure (first ++ second)
+
+fourDigits = do
+ a <- digit
+ b <- digit
+ c <- digit
+ d <- digit
+ pure [a,b,c,d]
+
+areaCode = do
+ is '('
+ a <- digit
+ b <- digit
+ is ')'
+ pure [a,b]
+
In essence, our parserA function or program that interprets structured input, often used to convert strings into data structures. + is going to be summed up by a couple of types:
+ +type Input = String
+newtype Parser a = P { parse :: Input -> ParseResult a}
+
We assume all Input
is a String
, i.e. Haskell’s basic builtin String
which is a list of Char
.
Then the Parser
type has one field parse
which is a function of type Input -> ParseResult a
. So it parses strings and produces parse results, where a Parse result is:
data ParseResult a = Error ParseError
+ | Result Input a
+ deriving Eq
+
We’ll come back to the ParseError
type - which will be returned in the case of unexpected input, but we can see that a successful Parse is going to produce a Result
which has two fields—more Input
(the part of the input remaining after we took a bit off and parsed it), and an a
, a type parameterA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+ that we may specify for concrete Parser
instances.
The Parser
and the ParseResult
types are pretty abstract. They say nothing about what precise Input
string we are going to parse, or what type a
we are going to return in the result. This is the strength of the parserA function or program that interprets structured input, often used to convert strings into data structures.
+, allowing us to build up sophisticated parsersA function or program that interprets structured input, often used to convert strings into data structures.
+ for different input grammars through composition using instances of FunctorA type class in Haskell that represents types that can be mapped over. Instances of Functor must define the fmap function, which applies a function to every element in a structure.
+, ApplicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ and MonadA type class in Haskell that represents computations as a series of steps. It provides the bind operation (»=) to chain operations and the return (or pure) function to inject values into the monadic context.
+, and the ParseResult
parameter a
allows us to produce whatever we want from the parsersA function or program that interprets structured input, often used to convert strings into data structures.
+ we create.
Error handling is a very important part of any real-world parserA function or program that interprets structured input, often used to convert strings into data structures.
+. Decent error reporting allows us to quickly diagnose problems in our input.
+As we saw above a ParseResult
may be either a successful Result
or an Error
, the latter containing information in a ParseError
data structure about the nature of the error.
data ParseError =
+ UnexpectedEof -- hit end of file when we expected more input
+ | ExpectedEof Input -- should have successfully parsed everything but there’s more!
+ | UnexpectedChar Char
+ | UnexpectedString String
+ deriving (Eq, Show)
+
Naturally it needs to be Show
able, and we’ll throw in an Eq
for good measure.
+
First an instance of Show
to pretty print the ParseResult
s:
instance Show a => Show (ParseResult a) where
+ show (Result i a) = "Result >" ++ i ++ "< " ++ show a
+ show (Error UnexpectedEof) = "Unexpected end of stream"
+ show (Error (UnexpectedChar c)) = "Unexpected character: " ++ show [c]
+ show (Error (UnexpectedString s)) = "Unexpected string: " ++ show s
+ show (Error (ExpectedEof i)) =
+ "Expected end of stream, but got >" ++ show i ++ "<"
+
And ParseResult
is also an instance of Functor
so that we can map functions over the output of a successful parse—or do nothing if the result is an Error
:
instance Functor ParseResult where
+ fmap f (Result i a) = Result i (f a)
+ fmap _ (Error e) = Error e
+
A Parser
itself is also a Functor
. This allows us to create a new Parser
by composing functionality onto the parse
function for a given Parser
:
instance Functor Parser where
+ fmap :: (a -> b) -> Parser a -> Parser b
+ fmap f (P p) = P (fmap f . p)
+
The applicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ pure
creates a Parser
that always succeeds with the given input, and thus forms a basis for composition. We saw it being used in the above example to return the results of a parse back into the Parser
at the end of a do
block.
The (<*>)
allows us to map functions in the Parser
over another Parser
. As with other Applicative
instances, a common use case would be composition with a Parser
that returns a data constructor as we will see in the next example.
instance Applicative Parser where
+ pure :: a -> Parser a
+ pure x = P (`Result` x)
+
+ (<*>) :: Parser (a -> b) -> Parser a -> Parser b
+ (<*>) p q = p >>= (<$> q)
+
The Monad
instance’s bindthe defining function which all monads must implement.
+ function (>>=)
we have already seen in use in the example above, allowing us to sequence Parser
s in do
-blocks to build up the implementation of the BNF grammar.
instance Monad Parser where
+ (>>=) :: Parser a -> (a -> Parser b) -> Parser b
+ (>>=) (P p) f = P (
+ \i -> case p i of
+ Result rest x -> parse (f x) rest
+ Error e -> Error e)
+
The most atomic function for a parserA function or program that interprets structured input, often used to convert strings into data structures.
+ of String
is to pull a single character off the input. The only thing that could go wrong is to find our input is empty.
character :: Parser Char
+character = P parseit
+ where parseit "" = Error UnexpectedEof
+ parseit (c:s) = Result s c
+
The following is how we will report an error when we encounter a character we didn’t expect. This is not the logic for recognising a character, that’s already happened and failed and the unrecognised character is now the parameter. This is just error reporting, and since we have to do it from within the context of a Parser
, we create one using the P
constructor. Then we set up the one field common to any Parser
, a function which returns a ParseResult
no matter the input, hence const
. The rest creates the right type of Error
for the given Char
.
unexpectedCharParser :: Char -> Parser a
+unexpectedCharParser = P . const . Error . UnexpectedChar
+
Now a parserA function or program that interprets structured input, often used to convert strings into data structures.
+ that insists on a certain character being the next one on the input. It’s using the Parser
instance of Monad
’s bindthe defining function which all monads must implement.
+ function (implicitly in a do
block) to sequence first the character
Parser
, then either return the correct character in the Parser
, or the Error
parserA function or program that interprets structured input, often used to convert strings into data structures.
+.
is :: Char -> Parser Char
+is c = do
+ v <- character
+ let next = if v == c
+ then pure
+ else const $ unexpectedCharParser v
+ next c
+
And finally we introduce the Alternative
typeclass for our Parser
for trying to apply a first Parser
, and then an alternate Parser
if the first fails. This allows us to encode the alternativesA type class in Haskell that extends Applicative, introducing the empty and <|> functions for representing computations that can fail or have multiple outcomes.
+ in our BNF grammar rules.
instance Alternative Parser where
+ empty :: Parser a
+ empty = Parser $ const (Error UnexpectedEof)
+
+ p1 <|> p2 = P (\i -> let f (Error _) = parse p2 i
+ f r = r
+ in f $ parse p1 i)
+
The last two pieces of our Phone Numbers grammar we also implement fairly straightforwardly from the BNF. In a real parserA function or program that interprets structured input, often used to convert strings into data structures. + combinatorA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments. + library you’d do it differently, as per our exercises below.
+ +<digit> ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
+<spaces> ::= " " <spaces> | ""
+
Here’s a trivial adaptation of digit
:
digit :: Parser Char
+digit = is '0' <|> is '1' <|> is '2' <|> is '3' <|> is '4' <|> is '5' <|> is '6' <|> is '7' <|> is '8' <|> is '9'
+
Spaces is a bit more interesting because it’s recursive, but still almost identical to the BNF:
+ +spaces :: Parser ()
+spaces = (is ' ' >> spaces) <|> pure ()
+
make a less repetitive digit
parserA function or program that interprets structured input, often used to convert strings into data structures.
+ by creating a function satisfy :: (Char -> Bool) -> Parser Char
which returns a parserA function or program that interprets structured input, often used to convert strings into data structures.
+ that produces a character but fails if the input is empty or the character does not satisfy the given predicate. You can use the isDigit
function from Data.Char
as the predicate.
change the type of spaces
to Parser [Char]
and have it return the appropriately sized string of only spaces.
We can generalise the is
parserA function or program that interprets structured input, often used to convert strings into data structures.
+ to handle a predicate
satisfy :: (Char -> Bool) -> Parser Char
+satisfy predicate = do
+ c <- character
+ let next = if f c then pure else unexpectedCharParser
+ next c
+
+
+digit :: Parser Char
+digit = satisfy isDigit
+
spaces :: Parser [Char]
+spaces = (do
+ _ <- is ' '
+ rest <- spaces
+ pure (' ' : rest)
+ ) <|> pure []
+
We can do this recursively, by trying to parse as many as possible, or we can use the many function to parse many spaces.
+ +spaces :: Parser [Char]
+spaces = many (satisfy isSpace)
+
The return type of the phone number parserA function or program that interprets structured input, often used to convert strings into data structures.
+ above was [Char]
(equivalent to String
). A more typical use case for a parserA function or program that interprets structured input, often used to convert strings into data structures.
+ though is to generate some data structure that we can then process in other ways. In Haskell, this usually means a parserA function or program that interprets structured input, often used to convert strings into data structures.
+ which returns an Algebraic Data Type (ADT). Here is a very simple example.
Let’s imagine we need to parse records from a vets office. It treats only three types of animals. As always, lets start with the BNF:
+ +<Animal> ::= "cat" | "dog" | "camel"
+
So our simple grammar consists of three terminalsIn the context of grammars, a terminal is a symbol that appears in the strings generated by the grammar. Terminals are the actual characters or tokens of the language.
+, each of which is a straightforward string token (a constant string that makes up a primitive word in our language). To parse such a token, we’ll need a parserA function or program that interprets structured input, often used to convert strings into data structures.
+ which succeeds if it finds the specified string next in its input. We’ll use our is
parserA function or program that interprets structured input, often used to convert strings into data structures.
+ from above (which simply confirms a given character is next in its input). The type of is was Char -> Parser Char
. Since Parser
is an instance of Applicative
, we can simply traverse
the is
parserA function or program that interprets structured input, often used to convert strings into data structures.
+ across the given String
(list of Char
) to produce another String
in the Parser
applicativeA type class in Haskell that extends Functor, allowing functions that are within a context to be applied to values that are also within a context. Applicative defines the functions pure and (<*>).
+ context.
string :: String -> Parser String
+string = traverse is
+
Now let’s define an ADT for animals:
+ +data Animal = Cat | Dog | Camel
+ deriving Show
+
A parserA function or program that interprets structured input, often used to convert strings into data structures.
+ for “cat” is rather simple. If we find the string "cat"
we produce a Cat
:
cat :: Parser Animal
+cat = string "cat" >> pure Cat
+
Let’s test it:
+ +> parse cat "cat"
+Result >< Cat
+
Ditto dogs and camels:
+ +dog, camel :: Parser Animal
+dog = string "dog" >> pure Dog
+camel = string "camel" >> pure Camel
+
And now a parserA function or program that interprets structured input, often used to convert strings into data structures. + for our full grammar:
+ +animal :: Parser Animal
+animal = cat <|> dog <|> camel
+
Some tests:
+ +> parse animal "cat"
+Result >< Cat
+> parse animal "dog"
+Result >< Dog
+> parse animal "camel"
+Result >< Camel
+
What’s really cool about this is that obviously the strings “cat” and “camel” overlap at the start. Our alternativeA type class in Haskell that extends Applicative, introducing the empty and <|> functions for representing computations that can fail or have multiple outcomes.
+ parserA function or program that interprets structured input, often used to convert strings into data structures.
+ (<|>)
effectively backtracks when the cat
parserA function or program that interprets structured input, often used to convert strings into data structures.
+ fails before eventually succeeding with the camel
parserA function or program that interprets structured input, often used to convert strings into data structures.
+. In an imperativeImperative programs are a sequence of statements that change a programs state. This is probably the dominant paradigm for programming languages today. Languages from Assembler to Python are built around this concept and most modern languages still allow you to program in this style.
+ style program this kind of logic would result in much messier code.
stringTok
which uses the string
parserA function or program that interprets structured input, often used to convert strings into data structures.
+ to parse a given string, but ignores any spaces
before or after the token.animal
parserA function or program that interprets structured input, often used to convert strings into data structures.
+ combinatorA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments.
+.humpCount
, remainingLives
, barkstyle
, etc.stringTok
, we can make use of <<
or >>
to ignore parts of the result:stringTok :: String -> Parser String
+stringTok s = spaces >> string s << spaces
+
// Define the classes for Cat, Dog, and Camel
+class Cat {
+ constructor() {
+ this.type = 'Cat';
+ }
+}
+
+class Dog {
+ constructor() {
+ this.type = 'Dog';
+ }
+}
+
+class Camel {
+ constructor() {
+ this.type = 'Camel';
+ }
+}
+
+class Dolphin {
+ constructor() {
+ this.type = 'Dolphin';
+ }
+}
+
+// Imperative parser function
+function parseAnimal(input) {
+ let animal = null;
+
+ if (input === 'cat') {
+ animal = new Cat();
+ } else if (input === 'dog') {
+ animal = new Dog();
+ } else if (input === 'dolphin') {
+ animal = new Dolphin();
+ } else if (input === 'camel') {
+ animal = new Camel();
+ } else {
+ throw new Error('Invalid input');
+ }
+
+ return animal;
+}
+
+// Example usage
+try {
+ const animal1 = parseAnimal('cat');
+ console.log(animal1); // Cat { type: 'Cat' }
+
+ const animal2 = parseAnimal('dog');
+ console.log(animal2);
+}
+
Programs are usually parsed into a tree structure called an Abstract Syntax TreeA tree representation of the abstract syntactic structure of a string of text. Each node in the tree represents a construct occurring in the text. + (AST), more generally known as a parse tree. Further processing ultimately into an object file in the appropriate format (whether it’s some sort of machine code directly executable on the machine architecture or some sort of intermediate format—e.g. Java bytecode) then essentially boils down to traversal of this tree to evaluate the statements and expressions there in the appropriate order.
+ +We will not implement a parserA function or program that interprets structured input, often used to convert strings into data structures. + for a full programming language, but to at least demonstrate what this concept looks like in Haskell we will create a simple parserA function or program that interprets structured input, often used to convert strings into data structures. + for simple arithmetic expressions. The parserA function or program that interprets structured input, often used to convert strings into data structures. + generates a tree structure capturing the order of operations, which we may then traverse to perform a calculation.
+ +To start with, here is a BNF grammar for a simple calculator with three operations *
, +
and -
, with *
having higher precedence than +
or -
:
<expr> ::= <term> | <expr> <addop> <term>
+<term> ::= <number> | <number> "*" <number>
+<addop> ::= "+" | "-"
+
+
+An expression <expr>
consists of one or more <term>
s that may be combined with an <addop>
(an addition operation, either "+"
or "-"
). A <term>
involves one or more numbers, multiplied together.
The dependencies between the non-terminalA symbol in a grammar that can be replaced by a sequence of terminals and non-terminals according to the production rules of the grammar. + expressions makes explicit the precedence of multiply operations needing to occur before add (and subtract).
+ +The data structure we will create uses the following Algebraic Datatype:
+ +data Expr = Plus Expr Expr
+ | Minus Expr Expr
+ | Times Expr Expr
+ | Number Integer
+ deriving Show
+
Our top-level function will be called parseCalc
:
parseCalc :: String -> ParseResult Expr
+parseCalc = parse expr
+
And an example use might look like:
+ +> parseCalc " 6 *4 + 3- 8 * 2"
+Result >< Minus (Plus (Times (Number 6) (Number 4)) (Number 3)) (Times (Number 8) (Number 2))
+
Here’s some ASCII art to make the tree structure of the ParseResult Expr
more clear:
Minus
+ ├──Plus
+ | ├──Times
+ | | ├──Number 6
+ | | └──Number 4
+ | └──Number 3
+ └──Times
+ ├──Number 8
+ └──Number 2
+
+
+show
for Expr
which pretty prints such treesExpr
tree like the one above.Obviously we are going to need to parse numbers, so let’s start with a simple parserA function or program that interprets structured input, often used to convert strings into data structures.
+ which creates a Number
.
+Note that whereas our previous parserA function or program that interprets structured input, often used to convert strings into data structures.
+ had type phoneNumber :: Parser [Char]
—i.e. it produced strings—this, and most of the parsersA function or program that interprets structured input, often used to convert strings into data structures.
+ below, produces an Expr
.
number :: Parser Expr
+number = spaces >> Number . read . (:[]) <$> digit
+
We keep things simple for now, make use of our existing digit
parserA function or program that interprets structured input, often used to convert strings into data structures.
+, and limit our input to only single digit numbers.
+The expression Number . read . (:[])
is fmapped over the Parser Char
returned by digit
.
+We use the PreludeThe default library loaded in Haskell that includes basic functions and operators.
+ function read :: Read a => String -> a
to create the Int
expected by Number
. Since read
expects a string, we apply (:[])
to turn the Char
into [Char]
, i.e. a String
.
Next, we’ll need a parserA function or program that interprets structured input, often used to convert strings into data structures.
+ for the various operators (*
,+
and -
). There’s enough of them that we’ll make it a general purpose Parser Char
parameterised by the character we expect:
op :: Char -> Parser Char -- parse a single char operator
+op c = do
+ spaces
+ is c
+ pure c
+
As before, spaces
ignores any number of ' '
characters.
Here’s how we use op
for *
; note that it returns only the Times
constructor. Thus, our return type is an as-yet unapplied binary function (and we see now why (<*>)
is going to be useful).
times :: Parser (Expr -> Expr -> Expr)
+times = op '*' >> pure Times
+
And for +
and -
a straightforward implementation of the <addop>
non-terminalA symbol in a grammar that can be replaced by a sequence of terminals and non-terminals according to the production rules of the grammar.
+ from our grammar:
addop :: Parser (Expr -> Expr -> Expr)
+addop = (op '+' >> pure Plus) <|> (op '-' >> pure Minus)
+
And some more non-terminalsA symbol in a grammar that can be replaced by a sequence of terminals and non-terminals according to the production rules of the grammar. +:
+ +expr :: Parser Expr
+expr = chain term addop
+
+term :: Parser Expr
+term = chain number times
+
These use the chain
function to handle repeated chains of operators (*
, -
, +
) of unknown length. We could make each of these functions recursive with a <|>
to provide an alternativeA type class in Haskell that extends Applicative, introducing the empty and <|> functions for representing computations that can fail or have multiple outcomes.
+ for the base case end-of-chain (as we did for spaces
, above), but we can factor the pattern out into a reusable function, like so:
chain :: Parser a -> Parser (a->a->a) -> Parser a
+chain p op = p >>= rest
+ where
+ rest :: a -> Parser a
+ rest a = (do
+ f <- op
+ b <- p
+ rest (f a b)
+ ) <|> pure a
+
But, how does chain
work?
p >>= rest
: The parserA function or program that interprets structured input, often used to convert strings into data structures.
+ p
is applied, and we pass this parsed value, to the function call rest
rest a
: Within the rest function, the parserA function or program that interprets structured input, often used to convert strings into data structures.
+ op is applied to parse an operator f
, and the parserA function or program that interprets structured input, often used to convert strings into data structures.
+ p
is applied again to parse another value b
. The result is then combined using the function f
applied to both a
and b
to form a new value. The rest function is then called recursively, with this new value.
Recursive calls: The recursive calls continue until there are no more operators op
to parse, at which point the parserA function or program that interprets structured input, often used to convert strings into data structures.
+ returns the last value a
. This is achieved using the pure a
expression. This makes the function tail recursive
This gives us a way to parse expressions of the form “1+2+3+4+5” by parsing “1” initially, using p
then repeatedly parsing something of the form “+2”, where op
would parse the “+” and the p
would then parse the “2”. These are combined using our Plus
constructor to be of form Plus 1 2
, this will then recessively apply the p
and op
parserA function or program that interprets structured input, often used to convert strings into data structures.
+ over the rest of the string: “+3+4+5”.
But, can we re-write this using a fold?
+ +chain :: Parser a -> Parser (a -> a -> a) -> Parser a
+chain p op = foldl applyOp <$> p <*> many (liftA2 (,) op p)
+ where
+ applyOp :: a -> (a->a->a, a) -> a
+ applyOp x (op, y) = op x y
+
foldl applyOp <$> p
: This part uses the FunctorA type class in Haskell that represents types that can be mapped over. Instances of Functor must define the fmap function, which applies a function to every element in a structure.
+ instances to combine the parsed values and apply the operators in a left-associative manner. foldl applyOp
is partially applied to p
, creating a parserA function or program that interprets structured input, often used to convert strings into data structures.
+ that parses an initial value (p
) and then applies the left-associative chain of operators and values.
many ((,) <$> op <*> p)
: This part represents the repetition of pairs (op, p) using the many combinatorA higher-order function that uses only function application and earlier defined combinators to define a result from its arguments.
+. The tuple structure here just allows us to store the pairs of op
and p
. We use liftA2 to lift both parse results in to the tuple constructor. We run this many times, to parse many pairs of op
and p
, and create a list of tuples. As a result, it creates a parserA function or program that interprets structured input, often used to convert strings into data structures.
+ that parses an operator (op) followed by a value (p) and repeats this zero or more times.
applyOp x (op, y)
: This function is used by foldl
to combine the parsed values and operators. It takes an accumulated value x
, an operator op
, and a new value y
, and applies the operator to the accumulated value and the new value.
chain
, factor out the recursion of spaces
into a function which returns a parserA function or program that interprets structured input, often used to convert strings into data structures.
+ that continues producing a list of values from a given parserA function or program that interprets structured input, often used to convert strings into data structures.
+, i.e.
+ list :: Parser a -> Parser [a]
.A common use-case for parsing is deserialising data stored as a string. +Of course, there are general data interchange formats such as JSON and XML for which most languages have parsersA function or program that interprets structured input, often used to convert strings into data structures. + available. However, sometimes you want to store data in your own format for compactness or readability, and when you do, deserialising the data requires a custom parserA function or program that interprets structured input, often used to convert strings into data structures. + +(this example is contributed by Arthur Maheo).
+ +We will explore a small game of Rock-Paper-Scissors using a memory. +The play function will have the following type:
+ +data RockPaperScissors = Rock | Paper | Scissors
+
+-- | Play a round of RPS given the result of the previous round.
+play
+ :: Maybe (RockPaperScissors, RockPaperScissors, String)
+ -- ^ Result of the previous round as: (your choice, opponent choice, your memory)
+ -> (RockPaperScissors, String) -- ^ (Choice, new memory)
+
We will build a simple player which will keep track of the opponent’s previous choices and try to counter the most common one.
+ +We will convert to string using a simple Show
instance:
instance Show RockPaperScissors where
+ show :: RockPaperScissors -> String
+ show Rock = "R"
+ show Paper = "P"
+ show Scissors = "S"
+
(Note, we could also define a Read
instance to deserialise such a simple type but we are going to define a ParserCombinator
for interest and extensibility to much more complex scenarios).
The straightforward way to create the memory is to just store a list of all the choices made by the opponent. +So, for example, if the results from the previous three rounds were:
+ +(Rock, Paper), (Rock, Scissors), (Paper, Scissors)
+
Then, a compact memory representation will be: "PSS"
.
Note: We only store single characters, so we do not need separators, but if you have more complex data, you will want separators.
+ +Now, we want to define a Parser RockPaperScissors
which will turn a string into a choice.
+First, we will define a parserA function or program that interprets structured input, often used to convert strings into data structures.
+ for each of the three choices:
rock :: Parser RockPaperScissors
+rock = is 'R' >> pure Rock
+
+scissors :: Parser RockPaperScissors
+scissors = is 'S' >> pure Scissors
+
+paper :: Parser RockPaperScissors
+paper = is 'P' >> pure Paper
+
This will give:
+ +>>> parse rock "R"
+Result >< R
+>>> parse rock "RR"
+Result >R< R
+>>> parse rock "P"
+Unexpected character: "P"
+
To combine those parsersA function or program that interprets structured input, often used to convert strings into data structures.
+, we will use the option parserA function or program that interprets structured input, often used to convert strings into data structures.
+ (<|>)
.
choice :: Parser RockPaperScissors
+choice = rock <|> paper <|> scissors
+
And, to be able to read a list of choices, we need to use the list
parserA function or program that interprets structured input, often used to convert strings into data structures.
+:
>>> parse choice "PSS"
+Result >SS< P
+>>> parse (list choice) "PSCS"
+Result >CS< [P,S]
+>>> parse (list choice) "PSS"
+Result >< [P,S,S]
+
Our decision function will take a list of RockPaperScissors
and return the move that would win against most of them.
+One question remains: how do we get the memory out of the parserA function or program that interprets structured input, often used to convert strings into data structures.
+?
+The answer is: pattern-matching.
getMem :: ParseResult a -> a
+getMem (Result _ cs) = cs
+getMem (Error _) = error "You should not do that!"
+
Obviously, in a longer program you want to be handling this case better.
+ +Hint: If your parserA function or program that interprets structured input, often used to convert strings into data structures.
+ returns a list of elements, the empty list []
is a good default case.
The first round, our player will just pick a choice at random and return an empty memory.
+ +play Nothing = (Scissors, "") -- Chosen at random!
+
Now, we need to write a couple functions:
+ +winAgainst
that determines which choice wins against a given one.mostCommon
which finds the most common occurrence in a list.With that, we have a full play
function:
play (Just (_, opponent, mem)) = (winning whole, concatMap convert whole)
+ where
+ -- Convert the memory to a list of different choices
+ as_choices = getMem . parse (list choice)
+ -- Get the whole set of moves—all the prev. rounds + last one
+ whole = opponent: as_choices mem
+ winning = winAgainst . mostCommon
+
>>> play Nothing
+(S,"")
+>>> play (Just (Scissors, Scissors, ""))
+(R,"S")
+>>> play (Just (Scissors, Scissors, "RRP"))
+(P,"SRRP")
+
Note: Here we can see the results directly because RockPaperScissors
has an instance of Show
.
+If you want to do the same with a datatype without Show
, you would need to call convert
.
Now, this is a simplistic view of storing information. +We are only concatenating characters because our data is so small. +However, there are better ways to store that data.
+ +One issue with this approach is that we need to process the memory sequentially at each round. +Instead, we could keep track of the number of occurrences of each choice.
+ +Implement a memory for the following datatype.
+ +data Played = Played {rocks, papers, scissors :: Int}
+
+-- | Store a @Played@ as a string in format: @nC@, with @n@ the number of
+-- occurrences and @C@ the choice.
+convert' :: Played -> String
+convert' Played{rocks, papers, scissors} =
+ show rocks ++ "R" ++ show papers ++ "P" ++ show scissors ++ "S"
+
>>> play Nothing
+(S,"0R0P0S")
+>>> play (Just (Scissors, Scissors, "0R0P0S"))
+(R,"0R0P1S")
+>>> play (Just (Scissors, Scissors, "2R1P0S"))
+(P,"2R1P1S")
+
Parser: A program that processes a string of text to extract structured information from it. Parsers are used in interpreting programming languages, data formats, and other structured text formats.
+ +Context-Free Grammar: A type of formal grammar that is used to define the syntax of programming languages and data formats. CFGs consist of a set of production rules that define how terminals and non-terminals can be combined to produce strings in the language.
+ +Backus-Naur Form: A notation for expressing context-free grammars. It is used to formally describe the syntax of programming languages.
+ +Terminal: In the context of grammars, a terminal is a symbol that appears in the strings generated by the grammar. Terminals are the actual characters or tokens of the language.
+ +Non-Terminal: A symbol in a grammar that can be replaced by a sequence of terminals and non-terminals according to the production rules of the grammar.
+ +Parser Combinator: A higher-order function that takes parsers as input and combines them to create new parsers. Parser combinators are used to build complex parsers in a modular and compositional way.
+ +Abstract Syntax Tree: A tree representation of the abstract syntactic structure of a string of text. Each node in the tree represents a construct occurring in the text.
+ ++ + + 15 + + min read
+JavaScript is a multiparadigm language that—due to its support for functions as objects, closuresA function and the set of variables it accesses from its enclosing scope. + and, therefore, higher-order functionsA function that takes other functions as arguments or returns a function as its result. +—is able to be used in a functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + programming style. However, if you are really enamoured with curryingThe process of transforming a function that takes multiple arguments into a sequence of functions that each take a single argument. + and combining higher-order functionsA function that takes other functions as arguments or returns a function as its result. +, then it really makes a lot of sense to use a language that is actually designed for it.
+ +There are a number of purpose-built FunctionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + Programming languages. Lisp (as we have already discussed) is the original, but there are many others. Scheme is a Lisp derivative, as is (more recently) Clojure. SML and its derivatives (e.g. OCaml, F#, etc.) form another family of functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + programming languages. However, the strongest effort to build a language that holds to the principles of lambda-calculus inspired functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + programming such as immutability (purity) is the Haskell family.
+ +There are a number of efforts to bring Haskell-like purity to web programming, inspired by the potential benefits the functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. +-style holds for managing complex state in asynchronousOperations that occur independently of the main program flow, allowing the program to continue executing while waiting for the operation to complete. + and distributed applications. Firstly, it is possible to compile Haskell code directly to JavaScript (using GHCJS) although the generated code is opaque and requires a runtime. Another promising and increasingly popular haskell-inspired language for client-side web development is Elm, although this again requires a runtime. Also, Elm is rather specialised for creating interactive web apps.
+ +The JavaScript-targeting Haskell derivative we are going to look at now is PureScript. The reason for this choice is that PureScript generates standalone and surprisingly readable JavaScript. For a full introduction to the language, the PureScript Book, written by the language’s creator, is available for free. However, in this unit we will only make a brief foray into PureScript as a segue from JavaScript to Haskell. To avoid overwhelming ourselves with minor syntactic differences we will also endeavor to stick to a subset of PureScript that is syntactically the same as Haskell.
+ +Without further ado, here is some PureScript code. Fibonacci number computation is often called the “hello world!” of functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus. + programming:
+ +fibs :: Int -> Int
+fibs 0 = 1
+fibs 1 = 1
+fibs n = fibs (n-1) + fibs (n-2)
+
Woah! A function for Fibonacci numbers that is about as minimal as you can get! And the top line, which just declares the type of the function, is often optional - depending on whether the compiler can infer it from the context. Having said that, it’s good practice to include a type declaration, especially for top-level functions (functions defined without indentation and therefore in-scope everywhere in the file). This function takes an Int
(integer) parameter, and returns an Int
. Note that the arrow shorthand for the function type definition is highly reminiscent of the JavaScript fat-arrow (=>
) though skinnier.
The next three lines define the actual logic of the function, which very simply gives a recursive definition for the n
th Fibonacci number. This definition uses a feature common to many functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus.
+ programming languages: pattern matchingA mechanism in functional programming languages to check a value against a pattern and to deconstruct data.
+. That is, we define the fibs
function three times, with the first two definitions handling the base cases. It says, literally: “the 0th and 1st fibs are both 1”. The last line defines the general case, that the remaining fibonacci numbers are each the sum of their two predecessors. Note, this definition is not perfect. Calling:
fibs -1
+
would be a bad idea. Good practice would be to add some exceptions for incorrect input to our function. In a perfect world we would have a compiler that would check types dependent on values (actually, languages that support dependent types exist, e.g. the Idris language is an interesting possible successor to Haskell in this space).
+ +Python3.10+ has taken inspiration from this pattern, and has its own alternative to pattern matchingA mechanism in functional programming languages to check a value against a pattern and to deconstruct data. +, with a slightly more verbose syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. +. This is semantically identical to the PureScript definition, where we use pattern matchingA mechanism in functional programming languages to check a value against a pattern and to deconstruct data. + against the inputs. For completeness, all functions should aim to provide the type definition, similar to what we did in the PureScript example.
+ +def fibs(n: int) -> int:
+ match n:
+ case 0:
+ return 1
+ case 1:
+ return 1
+ case _:
+ return fibs(n - 1) + fibs(n-2)
+
+print(fibs(12))
+
One thing you will have noticed by now is that Haskell-like languages are light on syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. +, this is obvious when compared next to the Python alternative. Especially, use of brackets is minimal, and typically to be avoided when evaluation order can be inferred correctly by the compiler’s application of lambda-calculus inspired precedence rules for function and operator application.
+ +We can define a main
function for our program, that maps the fibs
function to a (Nil
-terminated) linked-list of numbers and displays them to the console like so:
main = log $ show $ map fibs $ 1..10
+
and here’s the output when you run it from the command line:
+ +++ +(1 : 2 : 3 : 5 : 8 : 13 : 21 : 34 : 55 : 89 : Nil)
+
I’m omitting the type declaration for main
because the type for functions that have input-output side-effects is a little more complicated, differs from haskell - and the compiler doesn’t strictly need it yet anyway.
The above definition for main
is a chain of functions and the order of evaluation (and hence how you should read it) is right-to-left. The $
symbol is actually shorthand for brackets around everything to the symbol’s right. In other words, the above definition for main
is equivalent to:
main = log ( show ( map fibs ( 1..10 )))
+
The $
is not special syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language.
+ (i.e. it is not a keyword in the language definition). Rather, it is an operator defined in the PureScript Prelude like so:
infixr 0 apply as $
+
That is, $
is an infix, right associative operator with binding precedence 0 (the lowest) that invokes the apply function:
apply f x = f x
+
Woah! What is f and what is x
? Well, in PureScript functions are generic by default - but we (and the compiler) can infer, since f x is a function call with argument x, that f is a function and x is… anything. So apply literally applies the function f to the argument x. Since the binding precedence of the $
operator is so low compared to most things that could be placed to its right, brackets are (usually) unnecessary.
So anyway, back to the chain of functions in main
:
main = log $ show $ map fibs $ 1..10
+
log
is a function that wraps JavaScript’s console.log
+show
is a function that is overloaded to convert various types to strings. In this case, we’ll be showing a List of Int.
+map
is (equivalent to our old friend from our JavaScript exercises) a function that applies a function to stuff inside a… let’s call it a container for now… in this case our Container is a List.
+1..10
uses the ..
(range) infix operator to create a List of Int between 1 and 10.
So all this may seem pretty foreign, but actually, since we’ve already covered many of the functionalFunctional languages are built around the concept of composable functions. Such languages support higher order functions which can take other functions as arguments or return new functions as their result, following the rules of the Lambda Calculus.
+ programming fundamentals in JavaScript, let’s take a look at the JavaScript code that the PureScript compiler generates for fibs
and main
and see if anything looks familiar. Here’s fibs
, exactly as it comes out of the compiler:
var fibs = function (v) {
+ if (v === 0) {
+ return 1;
+ };
+ if (v === 1) {
+ return 1;
+ };
+ return fibs(v - 1 | 0) + fibs(v - 2 | 0) | 0;
+};
+
Woah! It’s pretty much the way a savvy JavaScript programmer would write it. The one part that may look a bit unusual are the expressions like v - 1 | 0
. Of course, JavaScript has no Int
type, so this is PureScript trying to sensibly convert to the all-purpose JavaScript number
type. The |
is a bitwise OR, so |0
ensures that resulting expression is an integer which is both a safety measure and a potential optimisation. It’s a situation where the declared types give the PureScript compiler more information about the intent of the code than would otherwise be present in JavaScript, and which it’s able to use to good effect.
At first glance the code generated for main
is a bit denser. Here it is, again as generated by the compiler but I’ve inserted some line breaks so we can see it a little more clearly:
var main = Control_Monad_Eff_Console.log(
+ Data_Show.show(
+ Data_List_Types.showList(Data_Show.showInt)
+ )(
+ Data_Functor.map
+ (Data_List_Types.functorList)(fibs)(Data_List.range(1)(10))
+ )
+);
+
Each of the functions lives in an object that encapsulates the module where it is defined. That’s pretty standard JavaScript practice. The rest is just function calls (application). The call to the range function is interesting:
+ +Data_List.range(1)(10)
+
Woah! It’s a curried function! Data_List.range(1) returns a function that creates lists of numbers starting from 1. The second call specifies the upper bound.
+ +main
are curried? Why?Our definition for fibs
was recursive. This has a nice declarativeDeclarative languages focus on declaring what a procedure (or function) should do rather than how it should do it.
+ style about it. The definition is very close to a mathematical definition. But at some point in your training for imperativeImperative programs are a sequence of statements that change a programs state. This is probably the dominant paradigm for programming languages today. Languages from Assembler to Python are built around this concept and most modern languages still allow you to program in this style.
+ programming you will have most likely been told that recursion is evil and inefficient. Indeed, we’ve seen at the start of this course that there is overhead due to creating new stack frames for each function call. Looping recursively creates a new stack frame for each iteration and so our (finite) stack memory will be consumed linearly with the number of iterations. However, there are certain patterns of recursive function calls that our compiler can easily recognise and replace with an iterative loop. We can see this happening directly in PureScript if we reconfigure our fibs
definition to use a tail call.
fibs n = f n 0 1
+ where
+ f 0 _ b = b
+ f i a b = f (i-1) b (a+b)
+
In general, as we have seen with $
, PureScript (and Haskell) have relatively few keywords, instead preferring functions and operators built with the language itself in the Prelude (the base library functions that are available by default). The where
keyword, however, is one of the exceptions. It allows us to make some local definitions inside the scope of the function. Here we define f
whose first parameter is an iteration counter, whose base case is 0
. The key feature of f
is that its recursive call is the very last thing to happen in the function body. That is, it is in the tail position.
The other important aspect of PureScript that we are encountering for the first time in the above definition is that indentation is used to determine scope (as in python).
+ +Here’s the JavaScript that is generated this time:
+ +var fibs = function (n) {
+ var f = function ($copy_v) {
+ return function ($copy_v1) {
+ return function ($copy_b) {
+ var $tco_var_v = $copy_v;
+ var $tco_var_v1 = $copy_v1;
+ var $tco_done = false;
+ var $tco_result;
+ function $tco_loop(v, v1, b) {
+ if (v === 0) {
+ $tco_done = true;
+ return b;
+ };
+ $tco_var_v = v - 1 | 0;
+ $tco_var_v1 = b;
+ $copy_b = v1 + b | 0;
+ return;
+ };
+ while (!$tco_done) {
+ $tco_result = $tco_loop($tco_var_v, $tco_var_v1, $copy_b);
+ };
+ return $tco_result;
+ };
+ };
+ };
+ return f(n)(0)(1);
+};
+
Obviously, it’s a less direct translation than was generated for our previous version of fibs
. However, you can fairly easily understand it still. Hint, the tco_
prefix in many of the generated variable names stands for “Tail Call OptimisationA compiler feature that optimises tail-recursive functions to prevent additional stack frames from being created, effectively converting recursion into iteration.1
+” and the local function f
is a curried function, as are all functions of more than one argument in PureScript. The important thing is that the recursive call is gone, replaced by a while loop.
We have seen all we need for now of PureScript. It’s a small but nicely put together language. It takes the best features of Haskell and reinterprets some of them quite cleverly to achieve relatively seamless interop with JavaScript. However, it’s still a bit niche. For the remainder of this unit we’ll dive more deeply into Haskell, which has a long history and is supported by a very large and active community across academia and industry.
+ +Pattern Matching: A mechanism in functional programming languages to check a value against a pattern and to deconstruct data.
+ +Tail Call Optimisation: A compiler feature that optimises tail-recursive functions to prevent additional stack frames from being created, effectively converting recursion into iteration.1
+ ++ + + 15 + + min read
+Pseudorandom number generators create a sequence of unpredictable numbers. +The following function generates the next element in a pseudorandom sequence from a previous seed.
+ +type Seed = Int
+
+nextRand :: Seed -> Seed
+nextRand prevSeed = (a*prevSeed + c) `mod` m
+ where -- Parameters for linear congruential RNG.
+ a = 1664525
+ c = 1013904223
+ m = 2^32
+
From a given seed in the pseudorandom sequence we can generate a number in a specified range.
+ +-- | Generate a number between `l` and `u`, inclusive.
+genRand :: Int -> Int -> Seed -> Int
+genRand l u seed = seed `mod` (u-l+1) + l
+
For example:
+ +-- | Roll a six-sided die once.
+-- >>> rollDie1 123
+-- (5,1218640798)
+-- >>> rollDie1 1218640798
+-- (4,1868869221)
+-- >>> rollDie1 1868869221
+-- (1,166005888)
+rollDie1 :: Seed -> (Seed, Int)
+rollDie1 s =
+ let s' = nextRand s
+ n = genRand 1 6 s'
+ in (s', n)
+
And if we want a sequence of dice rolls:
+ +-- | Roll a six-sided die `n` times.
+-- >>> diceRolls1 3 123
+-- ([5,4,1],166005888)
+diceRolls1 :: Int -> Seed -> (Seed, [Int])
+diceRolls1 0 s = ([], s)
+diceRolls1 n s =
+ let (s', r) = rollDie1 s
+ (s'', rolls) = diceRolls1 (n-1) s'
+ in (s'', r:rolls)
+
But keeping track of the various seeds (s
,s'
,s''
) is tedious and error prone. Let’s invent a monad which manages the seed for us. The seed will be threaded through all of our functions implicitly in the monadic return type.
newtype Rand a = Rand { next :: Seed -> (Seed, a) }
+
Rand
is a newtype
wrapper around a function with type Seed -> (Seed, a)
.
+It represents a computation that, given a starting Seed, produces:
Seed
.a
.Here it is in pictures sloppily edited from adit.io’s excellent Functors, Applicatives and Monads in Pictures.
+
Definition of Functor
. The Functor
instance for Rand
allows you to map a function over the result of a random computation.
instance Functor Rand where
+ fmap :: (a -> b) -> Rand a -> Rand b
+ fmap f (Rand g) = Rand h
+ where
+ -- The function inside rand
+ -- Apply f to the `value` a
+ h seed = (newSeed, f a)
+ where
+ (newSeed, a) = g seed
+
fmap constructs a new Rand value, Rand h, by:
+ +f
and a random computation Rand g
.h
that, given an initial Seed, runs g
to get (newSeed, a)
.(newSeed, f a)
, where f a
is the transformed value.After applying fmap f
, we have a new random computation that takes the same Seed as input and produces a transformed value (f a)
, while maintaining the same mechanics of randomness (i.e., correctly passing and updating the Seed state).
We can also be a bit more succinct, by making use of fmap
instances
fmap :: (a -> b) -> Rand a -> Rand b
+fmap f r = Rand $ (f <$>)<$> next r
+
pure :: a -> Rand a
+pure x = Rand (,x) -- Return the input seed and the value
+
+
+(<*>) :: Rand (a -> b) -> Rand a -> Rand b
+left <*> right = Rand h
+where
+ h s = (s'', f v) -- Need to return a function of type (Seed -> (Seed, Value))
+ where
+ (s', f) = next left s -- Get the next seed and function from the left Rand
+ (s'', v) = next right s' -- Get the next seed and value from the right Rand
+
<*>
constructs a new Rand value Rand h by:
f
from the left computation using the initial seed.v
from the right computation.f
to v
.instance Monad Rand where
+ (>>=) :: Rand a -> (a -> Rand b) -> Rand b
+ r >>= f = Rand $ \s ->
+ let (s1, val) = next r s
+ in next (f val) s1
+
r >>= f
creates a new Rand computation by:
r
with the initial seed s
.val
and the new seed s1
.val
to determine the next random computation f val
.f val
with the updated seed s1
to produce the final result.Get
and Put
put
is used to set the internal state (the Seed
) of the Rand
monad. There is no value yet, hence we use the unit (()
)
put
allows us to modify the internal state (Seed) of a random computation.
put :: Seed -> Rand ()
+put newSeed = Rand $ \_ -> (newSeed, ())
+
get
is used to retrieve the current state (the Seed
) from the Rand
monad.
+It does not modify the state but instead returns the current seed as the result. This is achieved by putting the current seed in to the value part of the tuple.
Since, when we apply transformation on the tuple, we apply the transformation according to the value!
+ +get :: Rand Seed
+get = Rand $ \s -> (s, s)
+
Using get
and the monad instance, we can make a function to increase the seed by one.
incrementSeed' :: Rand Seed
+incrementSeed' = get >>= \s -> pure (s + 1)
+
incrementSeed :: Rand Seed
+incrementSeed = do
+ seed <- get -- This gets the current seed
+ return (seed + 1) -- Increment the seed and put it in the 'state', we can do anything with the seed!
+
>>> next incrementSeed' 123
+(123, 124)
+
We want to modify the seed, assuming there is no value. This will simply apply a function f
to the current seed.
modify :: (Seed -> Seed) -> Rand ()
+modify f = Rand $ \s -> (f s, ())
+
We can also write this using our get
and put
modify :: (Seed -> Seed) -> Rand ()
+modify f = get >>= \s -> put (f s)
+
This function:
+ +get
).f
to modify the seed.put
.This computation returns () as its result, indicating that its purpose is to update the state, not to produce a value.
+ +We can now write our incrementSeed
in terms of modify
incrementSeed :: Rand Seed
+incrementSeed = do
+ modify (+1) -- Use `modify` to increment the seed by 1
+ get -- Return the updated seed
+
Let’s revisit the dice rolling example, but use the Rand
monad to thread the seed through all of our functions without us having to pass it around as a separate parameter. First recall our nextRand
and genRand
functions:
nextRand :: Seed -> Seed
+nextRand prevSeed = (a*prevSeed + c) `mod` m
+ where -- Parameters for linear congruential RNG.
+ a = 1664525
+ c = 1013904223
+ m = 2^32
+
+-- | Generate a number between `l` and `u`.
+genRand :: Int -> Int -> Seed -> Int
+genRand l u seed = seed `mod` (u-l+1) + l
+
Using the above two functions and our knowledge, we can make a function which rolls a dice. This will require 3 parts.
+ +nextRand
to update the current seedgenRand
to get the integer.rollDie :: Rand Int
+rollDie = do
+ modify nextRand -- update the current seed
+ s <- get -- get retrieves the updated seed value s from the Rand monad's state.
+ pure (genRand 1 6 s) -- computes a random number and puts back in the context
+
We can also write this using bind notation, where we modify nextRand
to update the seed. We then use >>
to ignore the result (i.e., the ()
). We use get to put the seed as the value, which is then binded on to s
and used to generate a random number. We then use pure to update the value, the seed updating is handled by our bind!
rollDie :: Rand Int
+rollDie = modify nextRand >> get >>= \s -> pure (genRand 1 6 s)
+
Finally, how we can use this?
+ +>>> next rollDie 123
+(1218640798,5)
+
next
is used on rollDie
to get the function of type Seed -> (Seed, a)
. We then call this function with a seed value of 123
, to get a new seed and a dice roll.
Now, here’s how we get a list of dice rolls using a direct adaptation of our previous code, but trusting the Rand
monad to thread the Seed
through for us. No more messy wiring up of parameters and inventing arbitrary variable names.
-- | Roll a six-sided die `n` times.
+-- >>> runState (diceRolls 3) 123
+-- ([5,4,1],166005888)
+diceRolls :: Int -> Rand [Int]
+diceRolls 0 = pure []
+diceRolls n = do
+ r <- rollDie
+ rest <- diceRolls (n-1)
+ pure (r:rest)
+
Of course, Haskell libraries are extensive, and if you can think of useful code that’s generalisable, there’s probably a version of it already in the libraries somewhere.
+ +Actually, we’ll use two libraries.
+ +From System.Random
, we’ll replace our Seed
type with StdGen
and nextRand
/genRand
with randomR
.
We’ll use Control.Monad.State
to replace our Rand
monad. The State
monad provides a context in-which data can be threaded through function calls without additional parameters. Similar to our Rand
monad the data can be accessed with a get
function, replaced with put
, or updated with modify
.
In diceRolls
, we’ll also replace the recursive list construction, with replicateM
, which just runs a function with a monadic effect n
times, placing the results in a list.
module StateDie
+where
+
+import System.Random
+import Control.Monad.State
+
+-- | Here's a starting seed for our tests.
+-- In System.Random seeds have type StdGen.
+seed :: StdGen
+seed = mkStdGen 123
+
+-- | Remake the Rand monad, but using the State monad to store the seed
+type Rand a = State StdGen a
+
+-- | A function that simulates rolling a six-sided dice
+-- >>> runState rollDie seed
+-- (1,StdGen ...)
+rollDie :: Rand Int
+rollDie = state (randomR (1,6))
+
+-- | Roll a six-sided die `n` times.
+-- >>> runState (diceRolls 3) seed
+-- ([1,5,6],StdGen ...)
+diceRolls :: Int -> Rand [Int]
+diceRolls n = replicateM n rollDie
+
As you can see, there is now very little custom code required for this functionality. Note that there is also a readonly version of the State
monad, called Reader
, as well as a write-only version (e.g. for tasks like logging) called Writer
.
+ + + 43 + + min read
+|
symbol.
+ and Optional Properties allow us to model types in common JavaScript coding patterns with precision.As the Web 2.0 revolution hit in 2000s web apps built on JavaScript grew increasingly complex and today, applications like GoogleDocs are as intricate as anything built over the decades in C++. In the 90s I for one (though I don’t think I was alone) thought that this would be impossible in a dynamically typed language. It is just too easy to make simple mistakes (as simple as typos) that won’t be caught until run time. It’s likely only been made possible due to increased rigour in testing. That is, instead of relying on a compiler to catch mistakes, you rely on a comprehensive suite of tests that evaluate running code before it goes live.
+ +Part of the appeal of JavaScript is that being able to run the source code directly in a production environment gives an immediacy and attractive simplicity to software deployment. However, in recent years more and more tools have been developed that introduce a build-chain into the web development stack. Examples include: minifiers, which compact and obfuscate JavaScript code before it is deployed; bundlers, which merge different JavaScript files and libraries into a single file to (again) simplify deployment; and also, new languages that compile to JavaScript, which seek to fix the JavaScript language’s shortcomings and compatibility issues in different browsers (although modern ECMAScript has less of these issues). Examples of languages that compile to JavaScript include CoffeeScript, ClojureScript and (more recently) PureScript (which we will visit later in this unit). Right now, however, we will take a closer look at another language in this family called TypeScript. See the official TypeScript documentation for some tutorials and deeper reference.
+ +TypeScript is interesting because it forms a relatively minimal augmentation, or superset, of ECMAScript syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. + that simply adds type annotationsA syntax in TypeScript where types are explicitly specified for variables, function parameters, and return types to ensure type safety and correctness. +. For the most part, the compilation process just performs validation on the declared types and strips away the type annotationsA syntax in TypeScript where types are explicitly specified for variables, function parameters, and return types to ensure type safety and correctness. + rendering just the legal JavaScript ready for deployment. This lightweight compilation into a language with a similar level of abstraction to the source is also known as transpilingA process where source code in one programming language is translated into another language with a similar level of abstraction, typically preserving the original code’s structure and functionality. + (as opposed to C++ or Java where the compiled code is much closer to the machine execution model).
+ +The following is intended as a minimally sufficient intro to TypeScript features such that we can type some fairly rich data structures and higher-order functionsA function that takes other functions as arguments or returns a function as its result. +. +An excellent free resource for learning the TypeScript language in depth is the TypeScript Deep-dive book.
+ +Type annotationsA syntax in TypeScript where types are explicitly specified for variables, function parameters, and return types to ensure type safety and correctness. + in TypeScript come after the variable name’s declaration, like so:
+ +let i: number = 123;
+
Actually, in this case the type annotationA syntax in TypeScript where types are explicitly specified for variables, function parameters, and return types to ensure type safety and correctness. + is completely redundant. The TypeScript compiler features sophisticated type inference. In this case it can trivially infer the type from the type of the literal.
+ +Previously, we showed how rebinding such a variable to a string in JavaScript is perfectly fine by the JavaScript interpreter. However, such a change of type in a variable is a dangerous pattern that is likely an error on the programmer’s part. The TypeScript compiler will generate an error:
+ +let i = 123;
+i = 'hello!';
+
++ +[TS compiler says] Type ‘string’ is not assignable to type ‘number’.
+
(Each of these features is described in more detail in subsequent sections—this is just a summary and roadmap)
+ +A type annotationA syntax in TypeScript where types are explicitly specified for variables, function parameters, and return types to ensure type safety and correctness.
+ begins with :
and goes after the variable name, but before any assignment.
+Primitive types include number
, string
, boolean
.
let x: number, s: string, b: boolean = false;
+x = "hello" // type error: x can only be assigned numbers!
+
Note: the primitive types begin with a lower-case letter and are not to be mistaken for Number
, String
and Boolean
which are not types at all but Object wrappers with some handy properties and methods. Don’t try to use these Object wrappers in your type definitions.
Union typesA TypeScript construct that allows a variable to hold values of multiple specified types, separated by the |
symbol.
+ allow more than one option for the type of value that can be assigned to a variable:
let x: number | string;
+x = 123;
+x = "hello" // no problem now
+x = false; // type error! only numbers or strings allowed.
+
Function parameter types are declared similarly, the return type of the function goes after the )
of the parameter list:
function theFunction(x: number, y: number): number {
+ return x + y; // returns a number
+}
+
When working with higher-order functionsA function that takes other functions as arguments or returns a function as its result. + you’ll need to pass functions into and/or return them from other functions. Function types use the fat-arrow syntaxThe set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in a computer language. +:
+ +function curry(f: (x:number, y:number)=>number): (x:number)=>(y:number)=>number {
+ return x=>y=>f(x,y)
+}
+
The curry
function above only works for functions that are operations on two numbers. We can make it generic by parameterising the argument types.
function curry<U,V,W>(f:(x:U,y:V)=>W): (x:U)=>(y:V)=>W {
+ return x=>y=>f(x,y)
+}
+
We can declare types for objects with multiple properties using interfacesA TypeScript construct that defines the shape of an object, specifying the types of its properties and methods. +
+ +interface Student {
+ name: string
+ mark: number
+}
+
We can use the Readonly
type to make immutable interfacesA TypeScript construct that defines the shape of an object, specifying the types of its properties and methods.
+, either by passing an existing interfaceA TypeScript construct that defines the shape of an object, specifying the types of its properties and methods.
+ as the type parameterA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+:
type ImmutableStudent = Readonly<Student>;
+
or all in one:
+ +type ImmutableStudent = Readonly<{
+ name: string
+ mark: number
+}>
+
When type annotationsA syntax in TypeScript where types are explicitly specified for variables, function parameters, and return types to ensure type safety and correctness.
+ get long and complex we can declare aliases for them using the type
keyword:
type CurriedFunc<U,V,W> = (x:U)=>(y:V)=>W
+
+function curry<U,V,W>(f:(x:U,y:V)=>W): CurriedFunc<U,V,W> {
+ return x=>y=>f(x,y)
+}
+
Declaring types for variables, functions and their parameters, and so on, provides more information to the person reading and using the code, but also to the compiler, which will check that you are using them consistently and correctly. This prevents a lot of errors.
+ +Consider that JavaScript is commonly used to manipulate HTMLHyper-Text Markup Language - the declarative language for specifying web page content. + pages. For example, I can get the top level headings in the current page, like so:
+ +const headings = document.getElementsByTagName("h1")
+
In the browser, the global document
variable always points to the currently loaded HTMLHyper-Text Markup Language - the declarative language for specifying web page content.
+ document. Its method getElementsByTagName
returns a collection of elements with the specified tag, in this case <h1>
.
Let’s say I want to indent the first heading by 100 pixels, I could do this by manipulating the “style” attribute of the element:
+ +headings[0].setAttribute("style","padding-left:100px")
+
Now let’s say I do a lot of indenting and I want to build a library function to simplify manipulating padding of HTMLHyper-Text Markup Language - the declarative language for specifying web page content. + elements.
+ +function setLeftPadding(elem, value) {
+ elem.setAttribute("style", `padding-left:${value}`)
+}
+
setLeftPadding(headings[0], "100px")
+
But how will a user of this function (other than myself, or myself in three weeks when I can’t remember how I wrote this code) know what to pass in as parameters? elem
is a pretty good clue that it needs to be an instance of an HTMLHyper-Text Markup Language - the declarative language for specifying web page content.
+ element, but is value a number or a string?
In TypeScript we can make the expectation explicit:
+ +function setLeftPadding(elem: Element, value: string) {
+ elem.setAttribute("style", `padding-left:${value}`)
+}
+
If I try to pass something else in, the TypeScript compiler will complain:
+ +setLeftPadding(headings[0],100)
+
++ +Argument of type ‘100’ is not assignable to parameter of type ‘string’.ts(2345)
+
In JavaScript, the interpreter would silently convert the number to a string and set padding-left:100
—which wouldn’t actually cause the element to be indented because CSSCascading Style Sheets - another declarative (part of the HTML5 standard) for specifying reusable styles for web page rendering.
+ expects px
(short for pixel) at the end of the value.
Potentially worse, I might forget to add the index after headings:
+ +setLeftPadding(headings,100)
+
This would cause a run-time error in the browser:
+ +++ +VM360:1 Uncaught TypeError: headings.setAttribute is not a function
+
This is because headings is not an HTMLHyper-Text Markup Language - the declarative language for specifying web page content.
+ Element but an HTMLCollection, with no method setAttribute
. Note that if I try to debug it, the error will be reported from a line of code inside the definition of setLeftPadding
. I don’t know if the problem is in the function itself or in my call.
The same call inside a TypeScript program would trigger a compile-time error. In a fancy editor with compiler services like VSCode I’d know about it immediately because the call would get a red squiggly underline immediately after I type it, I can hover over the squiggly to get a detailed error message, and certainly, the generated broken JavaScript would never make it into production.
+
Above we see how TypeScript prevents a variable from being assigned values of different types. +However, it is a fairly common practice in JavaScript to implicitly create overloaded functions by accepting arguments of different types and resolving them at run-time.
+ +The following will append the “px” after the value if a number is passed in, or simply use the given string (assuming the user added their own “px”) otherwise.
+ +function setLeftPadding(elem, value) {
+ if (typeof value === "number")
+ elem.setAttribute("style", `padding-left:${value}px`)
+ else
+ elem.setAttribute("style", `padding-left:${value}`)
+}
+
So this function accepts either a string or a number for the value
parameter—but to find that out we need to dig into the code. The “Union TypeA TypeScript construct that allows a variable to hold values of multiple specified types, separated by the |
symbol.
+” facility in TypeScript allows us to specify the multiple options directly in the function definition, with a list of types separated by |
:
function setLeftPadding(elem: Element, value: string | number) {...
+
Going further, the following allows either a number, or a string, or a function that needs to be called to retrieve the string
. It uses typeof
to query the type of the parameter and do the right thing in each case.
function setLeftPadding(elem: Element, value: number | string | (()=>string)) {
+ if (typeof value === "number")
+ elem.setAttribute("style", `padding-left:${value}px`)
+ else if (typeof value === "function")
+ elem.setAttribute("style", `padding-left:${value()}`)
+ else
+ elem.setAttribute("style", `padding-left:${value}`)
+}
+
The TypeScript typechecker also knows about typeof expressions (as used above) and will also typecheck the different clauses of if statements that use them for consistency with the expected types.
+ +We can now use this function to indent our heading in various ways:
+ +setLeftPadding(headings[0],100); // This will be converted to 100px
+setLeftPadding(headings[0],"100px"); // This will be used directly
+setLeftPadding(headings[0], () => "100px"); // This will call the function which returns 100px
+
It is common in TypeScript to have functions with union typeA TypeScript construct that allows a variable to hold values of multiple specified types, separated by the |
symbol.
+ parameters where each of the possible types need to be handled separately. There are several ways to test types of variables.
Primitive types: typeof v
gets the type of variable v
as a string. This returns ‘number’, ‘string’ or ‘boolean’ (and a couple of others that we won’t worry about) for the primitive types. It can also differentiate objects and functions, e.g.:
const x = 1,
+ s = "hello",
+ b = true,
+ o = {prop1:1, prop2:"hi"},
+ f = x=>x+1
+
+typeof x // 'number'
+typeof s // 'string'
+typeof b // 'boolean'
+typeof o // 'object'
+typeof f // 'function'
+
Object types: null values and arrays are considered objects:
+ +const o={prop1:1,prop2:"hi"},
+ n=null,
+ a=[1,2,3]
+
+typeof o // 'object'
+typeof n // 'object'
+typeof a // 'object'
+
To differentiate null and arrays from other objects, we need different tests:
+ +n===null // true
+a instanceof Array // true
+
Union typesA TypeScript construct that allows a variable to hold values of multiple specified types, separated by the |
symbol.
+ can be quite complex. Here is a type for JSON objects which can hold primitive values (string
,boolean
,number
), or null
, or Arrays containing elements of JsonVal
, or an object with named properties, each of which is also a JsonVal
.
type JsonVal =
+ | string
+ | boolean
+ | number
+ | null
+ | Array<JsonVal>
+ | { [key: string]: JsonVal }
+
Given such a union typeA TypeScript construct that allows a variable to hold values of multiple specified types, separated by the |
symbol.
+, we can differentiate types using the above tests in if
or switch
statements or ternary if-else expressions (?:
), for example to convert a JsonVal
to string:
const jsonToString = (json: JsonVal): string => {
+ if(json===null) return 'null';
+ switch(typeof json) {
+ case 'string':
+ case 'boolean':
+ case 'number':
+ return String(json)
+ }
+ const [brackets, entries]
+ = json instanceof Array
+ ? ['[]', json.map(jsonToString)]
+ : ['{}', Object.entries(json)
+ .map(/* exercise: what goes here? */)];
+ return `${brackets[0]} ${entries.join(', ')} ${brackets[1]}`
+}
+
Notice in the final ternary if expression (?:
), by the time we have determined that json
is not an Array, the only thing left that it could be is an Object with [key,value]
pairs that we can iterate over using Object.entries(json)
.
Note also that TypeScript is smart about checking types inside these kinds of conditional statements. So, for example, inside the last if expression, where we know json instanceof Array
is true
, we can immediately treat json
as an array and call its map
method.
Finally, don’t be confused by the use of array destructuring ([brackets,entries] = ...
) to get more than one value as the result of an expression. In this case we get the correct set of brackets to enclose elements of arrays [...]
versus objects {...}
. This avoids having more conditional logic than necessary. Like for-loops, ifs are easy to mess up, so less is generally safer.
In TypeScript you can declare an interface
which defines the set of properties and their types, that I expect to be available for certain objects.
For example, when tallying scores at the end of semester, I will need to work with collections of students that have a name, assignment and exam marks. There might even be some special cases which require mark adjustments, the details of which I don’t particularly care about but that I will need to be able to access, e.g. through a function particular to that student. The student objects would need to provide an interfaceA TypeScript construct that defines the shape of an object, specifying the types of its properties and methods. + that looks like this:
+ +interface Student {
+ name: string
+ assignmentMark: number
+ examMark: number
+ markAdjustment(): number
+}
+
Note that this interfaceA TypeScript construct that defines the shape of an object, specifying the types of its properties and methods.
+ guarantees that there is a function returning a number called markAdjustment
available on any object implementing the Student
interfaceA TypeScript construct that defines the shape of an object, specifying the types of its properties and methods.
+, but it says nothing about how the adjustment is calculated. Software engineers like to talk about Separation of Concerns (SoC). To me, the implementation of markAdjustment
is Someone Else’s Problem (SEP).
Now I can define functions which work with students, for example to calculate the average score for the class:
+ +function averageScore(students: Student[]): number {
+ return students.reduce((total, s) =>
+ total + s.assignmentMark + s.examMark + s.markAdjustment(), 0)
+ / students.length
+}
+
Other parts of my program might work with richer interfacesA TypeScript construct that defines the shape of an object, specifying the types of its properties and methods. + for the student database—with all sorts of other properties and functions available—but for the purposes of computing the average class score, the above is sufficient.
+ +Sometimes when we do marking we get lists of students indexed by their Student Number (a number
). Sometimes it’s by email address (a string
). You can see the concept of student numbers probably predates the existence of student emails (yes, universities often predate the internet!).
+What if one day our systems will use yet another identifier? We can future proof a program that works with lists of students by deferring the decision of the particular type of the id:
interface Student<T> {
+ id: T;
+ name: string;
+ ... // other properties such as marks etc…
+}
+
Here T
is the type parameterA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+, and the compiler will infer its type when it is used. It could be number
, or it could be a string
(e.g. if it’s an email), or it could be something else.
For example, we could define a student, using a number
as the type parameterA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+ T
const exampleStudent: Student<number> = {
+ id: 12345,
+ name: "John Doe",
+ ... // other properties
+}
+
We could also define a student, using a string
as the type parameterA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+ T
const exampleStudent: Student<string> = {
+ id: "jdoe0001@student.university.edu",
+ name: "John Doe",
+ ... // other properties
+}
+
Type parametersA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code. + can have more descriptive names if you like, but they must start with a capital. The convention though is to use rather terse single letter parameter names in the same vicinity of the alphabet as T. This habit comes from C++, where T used to stand for “Template”, and the terseness stems from the fact that we don’t really care about the details of what it is.
+ +As in function parameter lists, you can also have more than one type parameterA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code. +:
+ +interface Student<T,U> {
+ id: T;
+ name: string;
+ someOtherThingThatWeDontCareMuchAbout: U
+ ...
+}
+
Formally, this is a kind of “parametric polymorphismA type of polymorphism where functions or data types can be written generically so that they can handle values uniformly without depending on their type.
+”. The T
and U
here may be referred to as type parametersA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+ or type variables. We say that the property id
has generic type.
You see generic types definitions used a lot in algorithm and data structure libraries, to give a type—to be specified by the calling code—for the data stored in the data structures. For example, the following interfaceA TypeScript construct that defines the shape of an object, specifying the types of its properties and methods. + might be the basis of a linked list element:
+ +interface IListNode<T> {
+ data: T;
+ next?: IListNode<T>;
+}
+
The specific type of T
will be resolved when data
is assigned a specific type value.
We can add type parametersA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code. + to interfacesA TypeScript construct that defines the shape of an object, specifying the types of its properties and methods. +, ES6 classes, type aliases, and also functions. Consider the function which performs a binary search over an array of numbers (it assumes the array is sorted):
+ +function binarySearch1(arr:number[], key:number): number {
+ function bs(start:number, end:number): number {
+ if(start >= end) return -1;
+ const mid = Math.floor((start + end) / 2);
+ if(key > arr[mid]) return bs(mid + 1, end);
+ if(key < arr[mid]) return bs(start, mid);
+ return mid;
+ }
+ return bs(0,arr.length);
+}
+
+const studentsById = [
+ {id: 123, name: "Harry Smith"},
+ {id: 125, name: "Cindy Wu"},
+ ...
+]
+const numberIds = studentsById.map(s=>s.id);
+console.log(studentsById[binarySearch1(numberIds,125)].name)
+
++ +Cindy Wu
+
If we parameterise the type of elements in the array, we can search on sorted arrays of strings as well as numbers:
+ +function binarySearch2<T>(arr:T[], key:T): number {
+ function bs(start:number, end:number): number {
+ if(start >= end) return -1;
+ const mid = Math.floor((start + end) / 2);
+ if(key > arr[mid]) return bs(mid + 1, end);
+ if(key < arr[mid]) return bs(start, mid);
+ return mid;
+ }
+ return bs(0,arr.length);
+}
+
+const studentsByEmail = [
+ {id: "cindy@monash.edu", name: "Cindy Wu"},
+ {id: "harry@monash.edu", name: "Harry Smith"},
+ ...
+]
+
+const stringIds = studentsByEmail.map(s=>s.id);
+console.log(studentsByEmail[binarySearch2(stringIds,'harry@monash.edu')].name)
+
++ +Harry Smith
+
Why is this better than raw JavaScript with no type checking, or simply using TypeScript’s wildcard any
type? Well it ensures that we use the types consistently.
+For example:
binarySearch(numberIds,"harry@monash.edu")
+
++ +TYPE ERROR!
+
The binarySearch2
function above is usable with more types than binarySearch1
, but it still requires that T does something sensible with <
and >
.
+We can add a function to use for comparison, so now we can use it with students uniquely identified by some other weird thing that we don’t even know about yet:
function binarySearch3<T>(arr:T[], key:T, compare: (a:T,b:T)=>number): number {
+ function bs(start:number, end:number): number {
+ if(start >= end) return -1;
+ const mid = Math.floor((start + end) / 2),
+ comp = compare(key,arr[mid]);
+ if(comp>0) return bs(mid + 1, end);
+ if(comp<0) return bs(start, mid);
+ return mid;
+ }
+ return bs(0,arr.length);
+}
+
Elsewhere in our program where we know how students are sorted, we can specify the appropriate compare function:
+ +binarySearch3(students, key, (a,b)=>/* return 1 if a is greater than b, 0 if they are the same, -1 otherwise */)
+
We can also have multiple type parametersA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+ for a single function.
+The following version of curry
uses type parametersA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+ to catch errors without loss of generality:
function curry<U,V,W>(f:(x:U,y:V)=>W): (x:U)=>(y:V)=>W {
+ return x=>y=>f(y,x) // error!
+}
+
The TypeScript compiler underlines y,x
and says:
++ +Error: Argument of type ‘V’ is not assignable to parameter of type ‘U’. ‘U’ could be instantiated with an arbitrary type which could be unrelated to ‘V’.
+
So it’s complaining that our use of a y:V
into a parameter that should be a U
and vice-versa for x
. We flip them back and we are good again… but TypeScript helps us make sure we use the function consistently too:
function curry<U,V,W>(f:(x:U,y:V)=>W): (x:U)=>(y:V)=>W {
+ return x=>y=>f(x,y) // good now!
+}
+
+function prefix(s: string, n: number) {
+ return s.slice(0,n);
+}
+
+const first = curry(prefix)
+
+first(3)("hello") // Error!
+
++ +Error: Argument of type ‘number’ is not assignable to parameter of type ‘string’.
+
+Error: Argument of type ‘string’ is not assignable to parameter of type ‘number’.
So the error messages are similar to above, but now they list concrete types because the types for U
and V
have already been narrowed by the application of curry
to prefix
.
first("hello")(3) // good now!
+
So type checking helps us to create functions correctly, but also to use them correctly. +You begin to appreciate type checking more and more as your programs grow larger and the error messages appear further from their definitions. However, the most important thing is that these errors are being caught at compile time rather than at run time, when it might be too late!
+ +We have previously seen how useful the function map
is, which applies a function to every item in an array.
const map = (func, l) => {
+ if (l.length === 0) {
+ return []
+ }
+ else {
+ // Apply function to the first item, and
+ // recursively apply to the rest of the list
+ return [func(l[0]), ...map(func, l.slice(1))];
+ }
+}
+
If we wanted to provide types for this, we could consider a specific use case, for example converting a number
to a string
const map_num2str = (func: (x: number) => string, l: number[]): string[] => {
+ // same as above...
+}
+
This would correctly typecheck, and we could use this function to convert between a number and a string, but this means we would have to create one of these for each possible type, and this would be time consuming and be a lot of code you would have to write and provide types for. This is where generic types come in handy! Instead of func
converting between a number
and a string
, we will say func
is some operation which inputs something of a generic type T
, and returns another generic type V
.
const map = <T, V>(func: (x: T) => V, l: T[]): V[] => {
+ // same as above...
+}
+
The important part of this definition:
+ +<T, V>
: Here we define the type parametersA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+ which will be usable inside the type of our function. We can only use the generic types defined in this list. You can consider this analogous to defining variables in normal coding, and you only have access to variables which exist.T
is the type of the elements in the input array l.V
is the type of the elements in the output array, determined by the function func
.func: (x: T) => V
specifies that func
is a function that takes a parameter of type T
and returns a value of type V
.l: T[]
indicates that l
is an array of elements of type T
.V[]
is the return type of the function is an array of elements of type V
.All types defined by the generic parameters (T
and V
) are scoped within the function’s definition. This means that within the function, every instance of T
must be the same type, and every instance of V
must be the same type, ensuring type consistency, e.g., all T
’s can be a string
, but a T
cannot be both a string
at one point, and then number
at any other point in the type definition. T
and V
can be the same type (e.g., both numbers
), but they can also be different (e.g., T
could be string
and V
could be number
). The generic function doesn’t enforce any specific relationship between T
and V
, allowing for flexibility.
We have previously seen how useful the function filter
is, which optionally removes item in a list.
const filter = (func, l) => {
+ if (l.length === 0) {
+ return []
+ }
+ else {
+ if (func(l[0])) {
+ // Keep the current value
+ return [l[0], ...filter(func, l.slice(1))];
+ }
+ else {
+ // Skip the current item.
+ return filter(func, l.slice(1));
+ }
+ }
+}
+
If we have an array of numbers, and we wanted to keep only even numbers, we could define a helper function such as:
+ +const isEven = num => num % 2 === 0
+
If you do not know how to write an isEven function, luckily there are libraries for that. But, we know how to do it know.
+ +We can provide the type of our function as:
+ +const isEven = (num: number): boolean => num % 2 === 0;
+
This specifies that our input, is of type number
and it returns a boolean
. We can write a specific filter operation, which operates directly on numbers, such as:
const filterNumbers = (func: (x: number) => boolean , l: number[]) : number[] => {
+ // same as above
+}
+
This runs in to the same issue as before, where we would need to write this for each possible type, where generic types can help us by allowing us to write a filter function which works over a generic type.
+ +const filterNumbers = <T>(func: (x: T) => boolean , l: T[]) : T[] => {
+// same as above
+}
+
The important part of this definition:
+ +<T>
: Here we define the type parametersA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+ which will be usable inside the type of our function. Compared to map
, we only need a singular type parameterA placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+, as filter
only keeps or remove items, and does not change any data.T
is the type of the elements in the input array l.func: (x: T) => boolean
specifies that func
is a function that takes a parameter of type T
and returns a value of type boolean
. We do not use generic for the output type, as we should be as strict as possible, and we know that the function provided to filter should return a booleanl: T[]
indicates that l
is an array of elements of type T
.T[]
is the return type of the function is an array of elements of type T
.Look again at the next
property of IListNode
:
next?: IListNode<T>;
+
The ?
after the next
property means that this property is optional. Thus, if node
is an instance of IListNode<T>
, we can test if it has any following nodes:
typeof node.next === 'undefined'
+
or simply:
+ +!node.next
+
In either case, if these expressions are true
, it would indicate the end of the list.
A concrete implementation of a class for IListNode<T>
can provide a constructor:
class ListNode<T> implements IListNode<T> {
+ constructor(public data: T, public next?: IListNode<T>) {}
+}
+
Then we can construct a list like so:
+ +const list = new ListNode(1, new ListNode(2, new ListNode(3)));
+
List<T>
whose constructor takes an array parameter and creates a linked list of ListNodeList<T>
class for:forEach(f: (_:T)=> void): List<T>
+filter(f: (_:T)=> boolean): List<T>
+map<V>(f: (_:T)=> V): List<V>
+reduce<V>(f: (accumulator:V, t:T)=> V, initialValue: V): V
+
Note that each of these functions returns a list, so that we can chain the operations, e.g.:
+ +list.filter(x=>x%2===0).reduce((x,y)=>x+y,0)
+
We saw earlier that while an object reference can be declared const:
+ +const studentVersion1 = {
+ name: "Tim",
+ assignmentMark: 20,
+ examMark: 15
+}
+
which prevents reassigning studentVersion1
to any other object, the const
declaration does not prevent properties of the object from being changed:
studentVersion1.name = "Tom"
+
The TypeScript compiler will not complain about the above assignment at all.
+However, TypeScript does give us the possibility to add an as const
after creating an object to make it deeply immutable:
const studentVersion2 = {
+ name: "Tim",
+ assignmentMark: 20,
+ examMark: 15
+} as const
+
studentVersion2.name = "Tom"
+
++ +Cannot assign to ‘name’ because it is a read-only property.ts(2540)
+
The above is a singleton immutable Object. However, more generally, if we need multiple instances of a deeply immutable object, we can
+declare immutable types using the Readonly
construct:
type ImmutableStudent = Readonly<{
+ name: string;
+ assignmentMark: number;
+ examMark: number;
+}>
+
+const studentVersion3: ImmutableStudent = {
+ name: "Tim",
+ assignmentMark: 20,
+ examMark: 15
+}
+
+studentVersion3.name = "Tom"
+
Again, we get the squiggly:
+ +C++ is considered a strongly typed language in the sense that all types of values and variables must match up on assignment or comparison. Further, it is “statically” typed in that the compiler requires complete knowledge (at compile-time) of the type of every variable. This can be overridden (type can be cast away and void pointers passed around) but the programmer has to go out of their way to do it (i.e. opt-out).
+ +JavaScript, by contrast, as we have already mentioned, is dynamically typed in that types are only checked at run time. Run-time type errors can occur and be caught by the interpreter on primitive types, for example the user tried to invoke an ordinary object like a function, or refer to an attribute that doesn’t exist, or to treat an array like a number.
+ +TypeScript represents a relatively new trend in being a gradually typed language. Another way to think about this is that, by default, the type system is opt-in. Unless declared otherwise, all variables have type any. The any type is like a wild card that always matches, whether the any type is the target of the assignment:
+ +let value; // has implicit <any> type
+value = "hello";
+value = 123;
+// no error.
+
Or the source (r-value) of the assignment:
+ +let value: number;
+value = "hello";
+//[ts] Type '"hello"' is not assignable to type 'number'.
+value = <any>"hello"
+// no error.
+
While leaving off type annotationsA syntax in TypeScript where types are explicitly specified for variables, function parameters, and return types to ensure type safety and correctness.
+ and forcing types with any may be convenient, for example, to quickly port legacy JavaScript into a TypeScript program, generally speaking it is good practice to use types wherever possible, and can actually be enforced with the --noImplicitAny
compiler flag. This flag will be on by default in the applied classes. The compiler’s type checker is a sophisticated constraint satisfaction system and the correctness checks it applies are usually worth the extra effort—especially in modern compilers like TypeScript where type inference does most of the work for you.
Generic Types: A way to create reusable and type-safe components, functions, or classes by parameterizing types. These types are specified when the function or class is used.
+ +Immutable Object: An object whose state cannot be modified after it is created. In TypeScript, immutability can be enforced using Readonly or as const.
+ +Interface: A TypeScript construct that defines the shape of an object, specifying the types of its properties and methods.
+ +Transpiling: A process where source code in one programming language is translated into another language with a similar level of abstraction, typically preserving the original code’s structure and functionality.
+ +Type Annotation: A syntax in TypeScript where types are explicitly specified for variables, function parameters, and return types to ensure type safety and correctness.
+ +Type Parameter: A placeholder for a type that is specified when a generic function or class is used, allowing for type-safe but flexible code.
+ +Union Type: A TypeScript construct that allows a variable to hold values of multiple specified types, separated by the |
symbol.