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);
User reports bug
User reports bug
Product team documents the bug
Dev fixes bug
User reports bug
Product team documents the bug
Code review
User reports bug
Product team documents the bug
Dev fixes bug
Product team checks the bug fix
User reports bug
Product team documents the bug
Dev fixes bug
Code review
Product team checks the bug fix
User reports bug
Dev fixes bug
Product team documents the bug
Code review
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 setup(config) {
window.url = config.url + "&t=42";
// ...
}
function build_img() {
const img = new Image();
img.src = widow.url;
return img;
}
setup(config);
// ...
build_img();
function setup(config) {
window.url = config.url + "&t=42";
// ...
}
function build_img() {
const img = new Image();
img.src = widow.url;
return img;
}
setup(config);
// ...
build_img();
IMPURE
???
function enrich_config(config) {
return {
url: config.url + "&t=42",
// ...
};
}
function build_img(enriched_config) {
const img = new Image();
img.src = enriched_config.url;
return img;
}
enriched_config = enrich_config(config);
build_img(enriched_config);
PURE
function enrich_config(config) {
return {
url: config.url + "&t=42",
// ...
};
}
function build_img(enriched_config) {
const img = new Image();
img.src = enriched_config.url;
return img;
}
enriched_config = enrich_config(config);
build_img(enriched_config);
PURE
PUSH SIDE EFFECTS ASIDE!
document.getElementById
document.body.clientWidth
Math.random()
document.getElementById
document.body.clientWidth
Math.random()
launchRocket()
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
CPU 1
CPU 2
CPU 3
"General purpose"
functional programming language
Compilers
multicore compiler
PHP transformer
GraphQL → SQL
Webservers
Servant
Yesod
Others...
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