Sébastien Besnier
Functional Programming in JS: don't change anything!
@_sebbes_
Sébastien Besnier
Functional Programming in JS: don't change anything!
@_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);
GOAL
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
setColor("blue");
moveTo(150, 50);
circle(50);
// HEAD
setColor("red");
moveTo(100, 100);
circle(100);
GOAL
RESULT
setColor("red");
// LEFT EAR
circle(50);
// RIGHT EAR
setColor("blue");
moveTo(150, 50);
circle(50);
// HEAD
setColor("red");
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
setColor("red");
moveTo(100, 100);
circle(100);
setColor("red");
// LEFT EAR
circle(50);
// RIGHT EAR
setColor("blue");
moveTo(150, 50);
circle(50);
// HEAD
setColor("red");
moveTo(50, 50);
circle(100);
We focus on
setColor("red");
// LEFT EAR
circle(50);
// RIGHT EAR
setColor("blue");
moveTo(150, 50);
circle(50);
// HEAD
setColor("red");
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);
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
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
output parameters:
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
output parameters:
* no explicit ones
* 1 hidden: global var mutation
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
output parameters:
* no explicit ones
* 1 hidden: global var mutation
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!
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!
PURE
isLightOn(5) == true
isLightOn(10) == false
isLightOn(21) == true
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!
PURE
isLightOn(5) == true
isLightOn(10) == false
isLightOn(21) == true
PUSH SIDE EFFECTS ASIDE!
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));
ESSENTIAL 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(elements){
for(const e of elements){
// ... draw e ...
}
}
}
e = { x: 50, ..., onClick: "leftEarClicked", }
function draw(elements){
for(const e of elements){
// ... draw e ...
if(e.onClick) {
// ...
// add appropriate event listener
// ...
}
}
}
e = { x: 50, ..., onClick: "leftEarClicked", }
function draw(elements){
for(const e of elements){
// ... draw e ...
if(e.onClick) {
addClickListener(evt => {
if(/* e is clicked */) {
handle_event(e.onClick);
}
});
}
}
}
e = { 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):
PURE CODE IS:
All relevant user action go through the "update" function
PURE CODE IS:
Being a pirate with NPM
Developer downloads one of your malicious package which runs on the developer's computer
A Node.js server uses a malicious package
A HTML/JS page embeds a malicious package
module.exports = leftpad;
function leftpad (str, len, ch) {
str = String(str);
var i = -1;
if (!ch && ch !== 0) ch = ' ';
len = len - str.length;
while (++i < len) { str = ch + str; }
return str;
}
leftpad("toto", 7, "A") == "AAAtoto"
On March 22, 2016 the author unpublish the package from NPM...
And BOOM !
NPM quickly un-unpublish left-pad
Now in the standard:
"toto".padStart(7, "A")
... which appeared in most platforms mid-2016
(For Example Stealing Data)
(For Example Stealing Data)
"Your talk pretends to speak about Functional Programming and you don't speak about Monads and Functors?
Non mais allô quoi!"
Philipp Krüger
Kris Jenkins
http://blog.jenkster.com/2015/12/what-is-functional-programming.html
Sébastien Besnier
Functional Programming in JS: don't change anything!
@_sebbes_
function handle_event(event) {
const new_state = update(state, event);
state = new_state;
draw(view(state));
}
EFFICIENCY?
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;
}
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;
}
IMPURE
PURE