WELCOME TO
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);
ON ACTUAL CODE BASE
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
ON ACTUAL CODE BASE
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
ON ACTUAL CODE BASE
RESULT
setColor("red");
// LEFT EAR
circle(50);
// RIGHT EAR
setColor("blue");
moveTo(150, 50);
circle(50);
// HEAD
moveTo(100, 100);
circle(100);
SO THIS HAPPENS
ON ACTUAL CODE BASE
BUG IS EXPENSIVE
User reports bug
BUG IS EXPENSIVE
User reports bug
Product team documents the bug
BUG IS EXPENSIVE
Dev fixes bug
User reports bug
Product team documents the bug
BUG IS EXPENSIVE
Code review
User reports bug
Product team documents the bug
Dev fixes bug
BUG IS EXPENSIVE
Product team checks the bug fix
User reports bug
Product team documents the bug
Dev fixes bug
Code review
BUG IS EXPENSIVE
Product team checks the bug fix
User reports bug
Dev fixes bug
Product team documents the bug
Code review
A lot of
people,
time,
communication
involved
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
(PURE) DATA
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!
PURE FUNCTIONS
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:
- hidden input
- hidden output
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));
PURE FUNCTION
This function doesn't have:
- hidden input
- hidden output
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
Pure Functions are
- Referentially Transparent
- Easy to Test
- Easy to Reason about
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!
Notorious impure functions
- display on screen
- user input
- HTTP (or database) request
-
document.getElementById
-
document.body.clientWidth
-
Math.random()
Notorious impure functions
- display on screen
- user input
- HTTP (or database) request
-
document.getElementById
-
document.body.clientWidth
-
Math.random()
-
launchRocket()
MOVE MICKEY BY CLICKING ON IT
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...
TRIGGER EVENTS
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...
THE ELM ARCHITECTURE
THE ELM ARCHITECTURE
Implemented in (or variations):
- elm (captain obvious!)
- Vuex (Vue.js)
- Redux
- NgRx/NGXS (Angular)
THE ELM ARCHITECTURE
INITIAL STATE
TIME
THE ELM ARCHITECTURE
Event 1
INITIAL STATE
TIME
THE ELM ARCHITECTURE
Event 1
INITIAL STATE
update(event, state)
TIME
THE ELM ARCHITECTURE
Event 1
INITIAL STATE
STATE 2
update(event, state)
TIME
THE ELM ARCHITECTURE
Event 1
Event 2
INITIAL STATE
STATE 2
STATE 3
update(event, state)
update(event, state)
TIME
THE ELM ARCHITECTURE
Event 1
Event 2
Event 3
INITIAL STATE
STATE 2
STATE 3
STATE 4
update(event, state)
update(event, state)
update(event, state)
TIME
THE ELM ARCHITECTURE
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
TIME TRAVEL DEBUGGER
- out of the box in elm
- need some install in other frameworks
Hey, Mario!
Demo time!
DATABASE 101
ID | NAME | IS ADMIN |
---|---|---|
1 | Alice | Yes |
2 | Bob | No |
3 | Charlie | No |
4 | Daine | Yes |
5 | Evan | No |
6 | Flora | No |
DATABASE 101
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
EVENT SOURCING
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
EVENT SOURCING
Constraining side effects!
07:32
07:33
07:34
07:48
STORE
PERFORMANCE
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
Our Toy Implementation
Diff on pixels
Our Toy Implementation
Diff on DOM
Elm, React, Vue
Virtual DOM
Map !
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
Let's explore other languages!
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
HASKELL
"General purpose"
functional programming language
Compilers
multicore compiler
PHP transformer
GraphQL → SQL
Webservers
Servant
Yesod
Others...
- Finance
- Security
- Blockchain
- ...
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
Pure Functions are
- Referentially Transparent
- Easy to Test
- Easy to Reason about
Pure Functions are
- Referentially Transparent
- Easy to Test
- Easy to Reason about
- Easy to Parallelize
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);
FUNCTIONAL PROGRAMMING
without monads!
SIDE EFFECTS
PURE FUNCTIONS
Side Effect Free!
- Referentially Transparent
- Easy to Test
- Easy to Reason about
- Easy to Parallelize
THE ELM ARCHITECTURE
EVENT
SOURCING
OPTIMIZATIONS
HOPE YOU ENJOYED YOUR JOURNEY AT
@_sebbes_
Sébastien Besnier
Fullstack Functional Programming without Monads
By sebbes
Fullstack Functional Programming without Monads
- 394