WELCOME TO
Sébastien Besnier
Functional Programming in JS: don't change anything!
@_sebbes_
WELCOME TO
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);
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
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
ON ACTUAL CODE BASE
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
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);
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
(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!
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!
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));
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...
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(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...
THE ELM ARCHITECTURE
THE ELM ARCHITECTURE
Implemented in (or variations):
- elm (captain obvious!)
- Vuex (Vue.js)
- Redux
- NgRx/NGXS (Angular)
IMMEDIATE AND INDIRECT BENEFITS
IMMEDIATE BENEFITS
PURE CODE IS:
- EASY TO REASON
- FOR HUMAN ⇒ LESS BUGS!
- FOR COMPILERS ⇒ OPTIMIZATIONS!
- TRIVIAL TO TEST
- same arguments ⇒ same result
- don't rely on external/implicit configuration
⇒ MORE MAINTAINABLE CODE BASE
INDIRECT BENEFITS
TIME TRAVEL DEBUGGER
- out of the box in elm
- need some install in other frameworks
Hey, Mario!
Demo time!
INDIRECT BENEFITS
ANALYTICS
All relevant user action go through the "update" function
PURITY
PURE FUNCTION
- minds their own business
- same input leads to same output
- no visible effect from the outside except the returned value
IMMEDIATE BENEFITS
PURE CODE IS:
- EASY TO REASON
- FOR HUMAN ⇒ LESS BUGS!
- FOR COMPILERS ⇒ OPTIMIZATIONS!
- TRIVIAL TO TEST
- same arguments ⇒ same result
- don't rely on external/implicit configuration
⇒ MORE MAINTAINABLE CODE BASE
Security
Security
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
Leftpad
Leftpad
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"
Leftpad
Leftpad
On March 22, 2016 the author unpublish the package from NPM...
And BOOM !
Leftpad
NPM quickly un-unpublish left-pad
Now in the standard:
"toto".padStart(7, "A")
... which appeared in most platforms mid-2016
MALICIOUS NPM PACKAGES
(For Example Stealing Data)
- Exacerbated by the proliferation of dependencies
- Possible because can run arbitrary
side effects (on a simple import!)- Like in almost all packages manager
MALICIOUS NPM PACKAGES
(For Example Stealing Data)
- Exacerbated by the proliferation of dependencies
- Possible because can run arbitrary
side effects (on a simple import!)- Like in almost all packages manager
ELM PACKAGE MANAGER
- Only exposes pure functions
-
Implicit and unwanted side effects
are impossible : you have to explicitly pass a Cmd to the runtime
WRITE FUNCTIONAL CODE
NOW!
WHATEVER THE LANGUAGE!
"Your talk pretends to speak about Functional Programming and you don't speak about Monads and Functors?
Non mais allô quoi!"
LINKS
Improving Declarative APIs for Graphics with Types
Philipp Krüger
What is Functional Programming
Kris Jenkins
http://blog.jenkster.com/2015/12/what-is-functional-programming.html
HOPE YOU ENJOYED YOUR JOURNEY AT
Sébastien Besnier
Functional Programming in JS: don't change anything!
@_sebbes_
BONUSES
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
Programmation Fonctionnelle en JS: ne changez rien!
By sebbes
Programmation Fonctionnelle en JS: ne changez rien!
- 576