Sébastien Besnier
Fullstack Functional Programming without Monads
@_sebbes_
setColor("red");
// LEFT EAR
circle(50);
// RIGHT EAR
moveTo(150, 50);
circle(50);
// HEAD
moveTo(100, 100);
circle(100);GOAL
RESULT
setColor("red");
// LEFT EAR
circle(50);
// RIGHT EAR
moveTo(150, 50);
circle(50);
// HEAD
moveTo(100, 100);
circle(100);GOAL
RESULT
setColor("red");
// LEFT EAR
circle(50);
// RIGHT EAR
setColor("blue");
moveTo(150, 50);
circle(50);
// HEAD
moveTo(100, 100);
circle(100);50-100 lines
3000 lines
3000 lines
setColor("red");
// LEFT EAR
circle(50);
// RIGHT EAR
setColor("blue");
moveTo(150, 50);
circle(50);
// HEAD
moveTo(100, 100);
circle(100);setColor("red");
// LEFT EAR
circle(50);
// RIGHT EAR
setColor("blue");
moveTo(150, 50);
circle(50);
// HEAD
moveTo(50, 50);
circle(100);We focus on
setColor("red");
// LEFT EAR
circle(50);
// RIGHT EAR
setColor("blue");
moveTo(150, 50);
circle(50);
// HEAD
moveTo(50, 50);
circle(100);We usually don't look at...
We focus on
RESULT
setColor("red");
// LEFT EAR
circle(50);
// RIGHT EAR
setColor("blue");
moveTo(150, 50);
circle(50);
// HEAD
moveTo(100, 100);
circle(100);setColor("red");
// LEFT EAR
circle(50);
// RIGHT EAR
moveTo(100, 50);
circle(50);
// HEAD
moveTo(50, 50);
circle(100);How many input args for circle?
setColor("red");
// LEFT EAR
circle(50);
// RIGHT EAR
moveTo(100, 50);
circle(50);
// HEAD
moveTo(50, 50);
circle(100);3 input parameters:
* 1 explicit: size (100)
* 2 hidden: position and color
How many input args for circle?
setColor("red");
// LEFT EAR
circle(50);
// RIGHT EAR
moveTo(100, 50);
circle(50);
// HEAD
moveTo(50, 50);
circle(100);output parameters:
3 input parameters:
* 1 explicit: size (100)
* 2 hidden: position and color
How many input args for circle?
setColor("red");
// LEFT EAR
circle(50);
// RIGHT EAR
moveTo(100, 50);
circle(50);
// HEAD
moveTo(50, 50);
circle(100);output parameters:
* no explicit ones
* 1 hidden: global var mutation
3 input parameters:
* 1 explicit: size (100)
* 2 hidden: position and color
How many input args for circle?
setColor("red");
// LEFT EAR
circle(50);
// RIGHT EAR
moveTo(100, 50);
circle(50);
// HEAD
moveTo(50, 50);
circle(100);output parameters:
* no explicit ones
* 1 hidden: global var mutation
3 input parameters:
* 1 explicit: size (100)
* 2 hidden: position and color
How many input args for circle?
SIDE EFFECT
==
HIDDEN PARAMETER
LOCAL CODE CHANGES
⇒ GLOBAL BEHAVIOR CHANGES
LOCAL CODE CHANGES
⇒ GLOBAL BEHAVIOR CHANGES
MAINTENANCE
NIGHTMARE
Describe "WHAT" instead of "HOW"
[
// LEFT EAR
{ pos: [50, 50], color: "red",
size: 50, shape: "circle"
},
// RIGHT EAR
{ pos: [150, 50], color: "red",
size: 50, shape: "circle"
},
// HEAD
{ pos: [100, 100], color: "red",
size: 100, shape: "circle"
}
][
// LEFT EAR
{ pos: [50, 50], color: "red",
size: 50, shape: "circle"
},
// RIGHT EAR
{ pos: [150, 50], color: "red",
size: 50, shape: "circle"
},
// HEAD
{ pos: [100, 100], color: "red",
size: 100, shape: "circle"
}
][
// LEFT EAR
{ pos: [50, 50], color: "red",
size: 50, shape: "circle"
},
// RIGHT EAR
{ pos: [150, 50], color: "blue",
size: 50, shape: "circle"
},
// HEAD
{ pos: [100, 100], color: "red",
size: 100, shape: "circle"
}
][
// LEFT EAR
{ pos: [50, 50], color: "red",
size: 50, shape: "circle"
},
// RIGHT EAR
{ pos: [150, 50], color: "blue",
size: 50, shape: "circle"
},
// HEAD
{ pos: [100, 100], color: "red",
size: 100, shape: "circle"
}
]This only creates a new value.
NOTHING IS DISPLAYED!
draw([
// LEFT EAR
{ pos: [50, 50], color: "red",
size: 50, shape: "circle"
},
// RIGHT EAR
{ pos: [150, 50], color: "blue",
size: 50, shape: "circle"
},
// HEAD
{ pos: [100, 100], color: "red",
size: 100, shape: "circle"
}
])Only ONE side effect!
draw([
// LEFT EAR
{ pos: [50, 50], color: "red",
size: 50, shape: "circle"
},
// RIGHT EAR
{ pos: [150, 50], color: "blue",
size: 50, shape: "circle"
},
// HEAD
{ pos: [100, 100], color: "red",
size: 100, shape: "circle"
}
])
Only ONE side effect!
PUSH SIDE EFFECTS ASIDE!
Side Effect Free!
function mickey() {
return [
{ pos: [50, 50], color: "red",
size: 50, shape: "circle"
},
{ pos: [150, 50], color: "blue",
size: 50, shape: "circle"
},
{ pos: [100, 100], color: "red",
size: 100, shape: "circle"
}
];
}
draw(mickey());function mickey(x, y) {
return [
{ pos: [x, y], color: "red",
size: 50, shape: "circle"
},
{ pos: [x+100, y], color: "blue",
size: 50, shape: "circle"
},
{ pos: [x+50, y+50], color: "red",
size: 100, shape: "circle"
}
];
}
draw(mickey(50, 50));function mickey(x, y) {
return [
{ pos: [x, y], color: "red",
size: 50, shape: "circle"
},
{ pos: [x+100, y], color: "blue",
size: 50, shape: "circle"
},
{ pos: [x+50, y+50], color: "red",
size: 100, shape: "circle"
}
];
}
draw(mickey(50, 50));This function doesn't have:
function mickey(x, y) {
return [
{ pos: [x, y], color: "red",
size: 50, shape: "circle"
},
{ pos: [x+100, y], color: "blue",
size: 50, shape: "circle"
},
{ pos: [x+50, y+50], color: "red",
size: 100, shape: "circle"
}
];
}
draw(mickey(50, 50));This function doesn't have:
function isLightOn() {
var h = (new Date()).getHours();
return (h < 7) || (h > 19);
}
function isLightOn() {
var h = (new Date()).getHours();
return (h < 7) || (h > 19);
}
IMPURE
Very hard to test!
Results depend on current time
function isLightOn() {
var h = (new Date()).getHours();
return (h < 7) || (h > 19);
}
function isLightOn(hours) {
return (hours < 7) || (hours > 19);
}
IMPURE
Very hard to test!
Results depend on current time
function isLightOn() {
var h = (new Date()).getHours();
return (h < 7) || (h > 19);
}
function isLightOn(hours) {
return (hours < 7) || (hours > 19);
}
IMPURE
PURE
isLightOn(5) == true
isLightOn(10) == false
isLightOn(21) == true
Very hard to test!
Results depend on current time
Easy to test!
Results only depend on input args
function mickey(x, y) {
return [
{ pos: [x, y], color: "red",
size: 50, shape: "circle"
},
{ pos: [x+100, y], color: "blue",
size: 50, shape: "circle"
},
{ pos: [x+50, y+50], color: "red",
size: 100, shape: "circle"
}
];
}
draw(mickey(50, 50));function mickey(x, y) {
return [
{ pos: [x, y], color: "red",
size: 50, shape: "circle"
},
{ pos: [x+100, y], color: "blue",
size: 50, shape: "circle"
},
{ pos: [x+50, y+50], color: "red",
size: 100, shape: "circle"
}
];
}
draw(mickey(50, 50));STATE
var state = { x : 50, y : 50};
var state = { x : 50, y : 50};
function view(state) {
return mickey(state.x, state.y);
}
var state = { x : 50, y : 50};
function view(state) {
return mickey(state.x, state.y);
}
PURE FUNCTION
var state = { x : 50, y : 50};
function view(state) {
return mickey(state.x, state.y);
}
draw(view(state)); // intial rendering
PURE FUNCTION
var state = { x : 50, y : 50};
function view(state) {
return mickey(state.x, state.y);
}
draw(view(state)); // intial rendering
function handle_event(event) {
/* ...computing new_state...
*/
state = new_state;
draw(view(state));
}PURE FUNCTION
function handle_event(event) {
switch(event) {
case 'leftEarClicked':
var new_state =
{ ...state,
x: state.x - 1
};
break;
//...
}
state = new_state;
draw(view(state));
}function handle_event(event) {
switch(event) {
case 'leftEarClicked':
var new_state =
{ ...state,
x: state.x - 1
};
break;
//...
}
state = new_state;
draw(view(state));
}IMPURE
function handle_event(event) {
const new_state = update(state, event);
state = new_state;
draw(view(state));
}IMPURE
function handle_event(event) {
const new_state = update(state, event);
state = new_state;
draw(view(state));
}
IMPURE
function update(state, event){
switch(event) {
case 'leftEarClicked':
return {
...state,
x: state.x - 1
};
//...
}
}
function handle_event(event) {
const new_state = update(state, event);
state = new_state;
draw(view(state));
}IMPURE
PURE FUNCTION
function update(state, event){
switch(event) {
case 'leftEarClicked':
return {
...state,
x: state.x - 1
};
//...
}
}
function handle_event(event) {
const new_state = update(state, event);
state = new_state;
draw(view(state));
}function handle_event(event) {
const new_state = update(state, event);
state = new_state;
draw(view(state));
}FRAMEWORK PROVIDES...
function handle_event(event) {
const new_state = update(state, event);
state = new_state;
draw(view(state));
}PROGRAMMER DEFINES...
FRAMEWORK PROVIDES...
function handle_event(event) {
const new_state = update(state, event);
state = new_state;
draw(view(state));
}PROGRAMMER DEFINES
PURE FUNCTIONS!
FRAMEWORK PROVIDES...
function mickey(x, y) {
return [
{ pos: [x, y], color: "red",
size: 50, shape: "circle"
},
{ pos: [x+100, y], color: "blue",
size: 50, shape: "circle"
},
{ pos: [x+50, y+50], color: "red",
size: 100, shape: "circle"
}
];
}
function mickey(x, y) {
return [
{ pos: [x, y], color: "red",
size: 50, shape: "circle",
onClick: "leftEarClicked"
},
{ pos: [x+100, y], color: "blue",
size: 50, shape: "circle",
onClick: "rightEarClicked"
},
{ pos: [x+50, y+50], color: "red",
size: 100, shape: "circle"
}
];
}
function mickey(x, y) {
return [
{ pos: [x, y], color: "red",
size: 50, shape: "circle",
onClick: "leftEarClicked"
},
{ pos: [x+100, y], color: "blue",
size: 50, shape: "circle",
onClick: "rightEarClicked"
},
{ pos: [x+50, y+50], color: "red",
size: 100, shape: "circle"
}
];
}
PURE DATA
PURE DATA
function draw(shapes){
for(const s of shapes){
// ... draw s...
}
}
}
s = {
x: 50,
...,
onClick: "leftEarClicked",
}
function draw(shapes){
for(const s of shapes){
// ... draw s ...
if(s.onClick) {
// ...
// add appropriate event listener
// ...
}
}
}
s = {
x: 50,
...,
onClick: "leftEarClicked",
}
function draw(shapes){
for(const s of shapes){
// ... draw s ...
if(s.onClick) {
addClickListener(evt => {
if(/* s is clicked */) {
handle_event(s.onClick);
}
});
}
}
}
s = {
x: 50,
...,
onClick: "leftEarClicked",
}
function handle_event(event) {
const new_state = update(state, event);
state = new_state;
draw(view(state));
}PROGRAMMER DEFINES
PURE FUNCTIONS!
FRAMEWORK PROVIDES...
Implemented in (or variations):
INITIAL STATE
TIME
Event 1
INITIAL STATE
TIME
Event 1
INITIAL STATE
update(event, state)
TIME
Event 1
INITIAL STATE
STATE 2
update(event, state)
TIME
Event 1
Event 2
INITIAL STATE
STATE 2
STATE 3
update(event, state)
update(event, state)
TIME
Event 1
Event 2
Event 3
INITIAL STATE
STATE 2
STATE 3
STATE 4
update(event, state)
update(event, state)
update(event, state)
TIME
Event 1
Event 2
Event 3
Event 4
INITIAL STATE
STATE 2
STATE 3
STATE 4
STATE 5
update(event, state)
update(event, state)
update(event, state)
update(event, state)
TIME
| ID | NAME | IS ADMIN |
|---|---|---|
| 1 | Alice | Yes |
| 2 | Bob | No |
| 3 | Charlie | No |
| 4 | Daine | Yes |
| 5 | Evan | No |
| 6 | Flora | No |
| ID | NAME | IS ADMIN |
|---|---|---|
| 1 | Alice | Yes |
| 2 | Bob | No |
| 3 | Charlie | No |
| 4 | Daine | Yes |
| 5 | Evan | No |
| 6 | Flora | No |
Reflects the STATE of the application
Event
TIME
Event
Event
Event
Event
Event
Event
Event
05:11
05:31
05:45
06:42
06:44
07:02
07:13
07:22
Event
05:05
STORE
Event
TIME
Event
User 4 renamed to "Diane"
Event
Event
Event
Event
Event
05:11
05:31
05:45
06:42
06:44
07:02
07:13
07:22
Event
05:05
STORE
Event
TIME
Event
User 4 renamed to "Diane"
Event
Event
Event
Event
Event
05:11
05:31
05:45
06:42
06:44
07:02
07:13
07:22
05:22
Event
05:05
STATE
at 05:22
My trip at Farcheville
By Daine
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed aliquam, nisl in scelerisque interdum, ipsum ipsum scelerisque purus, at dapibus sapien eros a odio. Vestibulum feugiat vehicula ipsum, eget facilisis felis sodales quis. Maecenas tincid
STORE
Event
TIME
Event
User 4 renamed to "Diane"
Event
Event
Event
Event
Event
05:11
05:31
05:45
06:42
06:44
07:02
07:13
07:22
06:43
Event
05:05
STATE
at 06:43
My trip at Farcheville
By Diane
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed aliquam, nisl in scelerisque interdum, ipsum ipsum scelerisque purus, at dapibus sapien eros a odio. Vestibulum feugiat vehicula ipsum, eget facilisis felis sodales quis. Maecenas tincid
STORE
Event
TIME
Event
User 4 renamed to "Diane"
Event
User 3 promoted Admin
Post "I like nazis" published
Post 666 removed
Post 418 removed
05:11
05:31
05:45
06:42
06:44
07:02
07:13
07:22
Event
05:05
PWNED
STORE
Event
TIME
Event
User 4 renamed to "Diane"
Event
User 3 promoted Admin
Post "I like nazis" published
Post 666 removed
Post 418 removed
05:11
05:31
05:45
06:42
06:44
07:02
07:13
07:22
Event
05:05
STORE
Event
TIME
Event
User 4 renamed to "Diane"
Event
Box moved 2px to the right (event id: 12345)
Event
Event
Event
05:11
05:31
05:45
06:42
Event
05:05
07:32
07:33
07:34
07:48
STORE
Event
TIME
Event
User 4 renamed to "Diane"
Event
Box moved 2px to the right (event id: 12345)
Event 12345 ignored (event id: 12346)
Event
Event
05:11
05:31
05:45
06:42
07:32
07:33
07:34
07:48
Event
05:05
CTRL+Z
STORE
Event
TIME
Event
User 4 renamed to "Diane"
Event
Box moved 2px to the right (event id: 12345)
Event 12346 ignored
Event
05:11
05:31
05:45
06:42
07:32
07:33
07:34
07:48
Event
05:05
CTRL+Z
CTRL+SHIFT+Z
STORE
Event 12345 ignored (event id: 12346)
Event
TIME
Event
User 4 renamed to "Diane"
Event
Event
Event
Event
Event
STORE
05:11
05:31
05:45
06:42
Event
05:05
07:32
07:33
07:34
07:48
Event
TIME
Event
User 4 renamed to "Diane"
Event
Event
Event
Event
Event
05:11
05:31
05:45
06:42
Event
05:05
Constraining side effects!
07:32
07:33
07:34
07:48
STORE
function handle_event(event) {
const new_state = update(state, event);
state = new_state;
draw(view(state));
}function handle_event(event) {
const new_state = update(state, event);
state = new_state;
draw(view(state));
}Redraw the entire screen
var previous_view = view(init_state);
draw(previous_view);
function handle_event(event) {
const new_state = update(state, event);
const new_view = view(new_state);
apply_diff(previous_view, new_view);
state = new_state;
previous_view = new_view;
}var previous_view = view(init_state);
draw(previous_view);
function handle_event(event) {
const new_state = update(state, event);
const new_view = view(new_state);
apply_diff(previous_view, new_view);
state = new_state;
previous_view = new_view;
}CHEAP
CHEAP
MODERATE
COSTLY
Diff on pixels
Diff on pixels
Diff on DOM
Virtual DOM
fib = [1,1,2,3,5,8,13,21,34,55];
fib.map(n => n + 1);| 1 |
| 1 |
| 2 |
| 3 |
| 5 |
| 8 |
| 13 |
| 21 |
| 34 |
| 55 |
| 2 |
| 2 |
| 3 |
| 4 |
| 6 |
| 9 |
| 14 |
| 22 |
| 35 |
| 56 |
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
fib = [1,1,2,3,5,8,13,21,34,55];
fib.map(n => n + 1);for(var i = 0; i < fib.length; i++) {
fib[i] = fib[i] + 1;
}fib = [1,1,2,3,5,8,13,21,34,55];
fib.map(n => n + 1);for(var i = 0; i < fib.length; i++) {
fib[i] = fib[i] + 1;
}WHAT
HOW
fib = [1,1,2,3,5,8,13,21,34,55];
fib.map(n => n + 1);fib.map(n => print(n));fib.map(n => print(n));IMPURE FUNCTION
→ORDER MATTERS
fib.map(n => n + 1);| 1 |
| 1 |
| 2 |
| 3 |
| 5 |
| 8 |
| 13 |
| 21 |
| 34 |
| 55 |
| 2 |
| 2 |
| 3 |
| 4 |
| 6 |
| 9 |
| 14 |
| 22 |
| 35 |
| 56 |
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
PURE FUNCTION
fib.map(n => n + 1);PURE FUNCTION
| 1 |
| 1 |
| 2 |
| 3 |
| 5 |
| 8 |
| 13 |
| 21 |
| 34 |
| 55 |
| 2 |
| 2 |
| 3 |
| 4 |
| 6 |
| 9 |
| 14 |
| 22 |
| 35 |
| 56 |
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
Alice
Bob
Charlie
map (\n -> n + 1) fib| 1 |
| 1 |
| 2 |
| 3 |
| 5 |
| 8 |
| 13 |
| 21 |
| 34 |
| 55 |
| 2 |
| 2 |
| 3 |
| 4 |
| 6 |
| 9 |
| 14 |
| 22 |
| 35 |
| 56 |
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
CORE 1
.
.
.
CORE 1000
SPLIT WORK
ON GPU!
map (\n -> n + 1) fib| 1 |
| 1 |
| 2 |
| 3 |
| 5 |
| 8 |
| 13 |
| 21 |
| 34 |
| 55 |
| 2 |
| 2 |
| 3 |
| 4 |
| 6 |
| 9 |
| 14 |
| 22 |
| 35 |
| 56 |
+1
+1
+1
+1
+1
+1
+1
+1
+1
+1
SEVER 1
.
.
.
SERVER 100
fib.map(n => n + 1);for(var i = 0; i < fib.length; i++) {
fib[i] = fib[i] + 1;
}Array Reallocated
Array Mutated in place
fib.map(n => n + 1);for(var i = 0; i < fib.length; i++) {
fib[i] = fib[i] + 1;
}Array Reallocated
Array Mutated in place
EFFICIENCY ++
fib.map(n => n + 1);for(var i = 0; i < fib.length; i++) {
fib[i] = fib[i] + 1;
}Array Reallocated
Array Mutated in place
EFFICIENCY ++
BUGS ++
Array Reallocated
Array Mutated in place
EFFICIENCY ++
BUGS --
for(var i = 0; i < fib.length; i++) {
fib[i] = fib[i] + 1;
}Uniqueness analysis: if fib isn't used then we can reuse the memory
fib.map(n => n + 1);Side Effect Free!
EVENT
SOURCING
@_sebbes_
Sébastien Besnier