Âge du capitaine ?

Gustave Flaubert

1841

Un navire est en mer…

{
  shipAtSea: true,
}

…il est parti de Boston chargé de coton…

{
  shipAtSea: true,
  from: [42.3601, 71.0589],
  payload: "cotton"
}

…il jauge 200 tonneaux…

{
  shipAtSea: true,
  from: [42.3601, 71.0589],
  payload: "cotton",
  tonnage: 200
}

…il fait voile vers Le Havre…

{
  shipAtSea: true,
  from: [42.3601, 71.0589],
  payload: "cotton",
  tonnage: 200,
  to: [49.4944, 0.1079]
}

…le grand mât est cassé…

{
  shipAtSea: true,
  from: [42.3601, 71.0589],
  payload: "cotton",
  tonnage: 200,
  to: [49.4944, 0.1079],
  brokenMast: true
}

…il y a un mousse sur le gaillard d'avant…

{
  shipAtSea: true,
  from: [42.3601, 71.0589],
  payload: "cotton",
  tonnage: 200,
  to: [49.4944, 0.1079],
  brokenMast: true,
  sailor: "forecastle"
}

…les passagers sont au nombre de douze…

{
  shipAtSea: true,
  from: [42.3601, 71.0589],
  payload: "cotton",
  tonnage: 200,
  to: [49.4944, 0.1079],
  brokenMast: true,
  sailor: "forecastle",
  passengers: 12
}

…le vent souffle N.-E.-E.…

{
  shipAtSea: true,
  from: [42.3601, 71.0589],
  payload: "cotton",
  tonnage: 200,
  to: [49.4944, 0.1079],
  brokenMast: true,
  sailor: "forecastle",
  passengers: 12,
  wind: ["N", "E", "E"]
}

…l'horloge marque trois heures un quart d'après midi…

{
  shipAtSea: true,
  from: [42.3601, 71.0589],
  payload: "cotton",
  tonnage: 200,
  to: [49.4944, 0.1079],
  brokenMast: true,
  sailor: "forecastle",
  passengers: 12,
  wind: ["N", "E", "E"],
  time: "15:15:00"
}

…on est au mois de mai.

{
  shipAtSea: true,
  from: [42.3601, 71.0589],
  payload: "cotton",
  tonnage: 200,
  to: [49.4944, 0.1079],
  brokenMast: true,
  sailor: "forecastle",
  passengers: 12,
  wind: ["N", "E", "E"],
  time: "15:15:00",
  month: 5
}

Caroline Flaubert

10 candidats

Nemo

Haddock

Iglo

Crochet

Planet

Flam

Caverne

America

Sam

Kirk

Data

var captains = [
  { name: "Nemo", birth: 1869 },
  { name: "Haddock", birth: 1941 },
  { name: "Iglo", birth: 1967 },
  { name: "Crochet", birth: 1904 },
  { name: "Planet", birth: 1990 },
  { name: "Flam", birth: 1978 },
  { name: "Caverne", birth: 1977 },
  { name: "America", birth: 1920 },
  { name: "Sam", birth: "ZOOS" },
  { name: "Kirk", birth: 2233 }
]

Moyenne arithmétique (ou empirique)

la somme des valeurs d'une série statistique divisée par le nombre de valeurs

division

var meanAges = sumAges / captains.length // cuidado au zéro

iteration

var sumAges = 0 // init

for (var i = 0; i < captains.length; i++) {
  // … do the magic
}

var meanAges = sumAges / captains.length

current item

var sumAges = 0

for (var i = 0; i < captains.length; i++) {
  var captain = captains[i] // lui !
}

var meanAges = sumAges / captains.length

calcul âge

var sumAges = 0

for (var i = 0; i < captains.length; i++) {
  var captain = captains[i]
  var age = currentYear - captain.birth // currentYear?
}

var meanAges = sumAges / captains.length

moment!

npm i -S moment babel babel-truc babel-yolo react-router webpack
import moment from 'moment' // ES42
webpack --t browserify --rollupify --isomorphic > success.js

currentYear

var sumAges = 0
//                (new Date).getFullYear()
var currentYear = moment().year()

for (var i = 0; i < captains.length; i++) {
  var captain = captains[i]
  var age = currentYear - captain.birth
}

var meanAges = sumAges / captains.length

Incrementation

var sumAges = 0
var currentYear = moment().year()

for (var i = 0; i < captains.length; i++) {
  var captain = captains[i]
  var age = currentYear - captain.birth
  sumAges += age
}

var meanAges = sumAges / captains.length

NaN

  // …
  { name: "Sam", birth: "ZOOS" }
  // …

isNaN

var sumAges = 0
var currentYear = moment().year()

for (var i = 0; i < captains.length; i++) {
  var captain = captains[i]
  var age = currentYear - captain.birth
  if (!isNaN(age)) { // only keep NotNotANumber
    sumAges += age
  }
}

var meanAges = sumAges / captains.length

length

var sumAges = 0
var realCaptainsCount = 0
var currentYear = moment().year()

for (var i = 0; i < captains.length; i++) {
  var captain = captains[i]
  var age = currentYear - captain.birth
  if (!isNaN(age)) { // only keep NotNotANumber
    sumAges += age
    realCaptainsCount++
  }
}

var meanAges = sumAges / realCaptainsCount

40.55555555…

  // …
  { name: "Kirk", birth: 2233 }
  // …

retour vers le futur

var sumAges = 0
var realCaptainsCount = 0
var currentYear = moment().year()

for (var i = 0; i < captains.length; i++) {
  var captain = captains[i]
  var age = currentYear - captain.birth
  if (!isNaN(age) && age >= 0) { // only positive
    sumAges += age
    realCaptainsCount++
  }
}

var meanAges = sumAges / realCaptainsCount

72.75

Indices

// 1) init
var sumAges = 0
var realCaptainsCount = 0
var currentYear = moment().year()

for (var i = 0; i < captains.length; i++) { // 2) iteration
  var captain = captains[i]
  var age = currentYear - captain.birth // 3) pluck, 4) substraction
  if (!isNaN(age) && age >= 0) { // 5) conditions
    sumAges += age // 6) accumulation
    realCaptainsCount++
  }
}

var meanAges = sumAges / realCaptainsCount

phases

  • [ captains ] → [ births ]
  • [ births ] → [ ages? ]
  • [ ages? ] → [ ages ]
  • [ ages ] → [ positive ages ]
  • [ positive ages ] → sum ages

forEach

Iteration simplifiée !

for (var i = 0; i < arr.length; i++) {
  action
}
arr.forEach(x => action)

Problème: side effects!

map

Transformer chaque élément

var results = []
data.forEach(x => {
  results.push(projection(x))
})
results = data.map(projection)

filter

Autant ou moins d'éléments en sortie

var results = []
data.forEach(x => {
  if (cond)
    results.push(x)
})
results = data.filter(predicate)

reduce

var results = ""
data.forEach(x => {
  if (cond)
    results += projection(x)
})
results = data.reduce((acc, x) => {
  if (cond) acc += projection(x)
  return acc
}, "")

[ captains ] → [ births ]

var births = []
for (var i = 0; i < captains.length; i++) {
  births.push(captains[i].birth)
}

map

var births = captains.map(x => x.birth)

[ births ] → [ ages? ]

var currentYear = moment().year()
var ages = []
for (var i = 0; i < births.length; i++) {
  ages.push(currentYear - births[i])
}

map

var currentYear = moment().year()
var ages = births.map(x => currentYear - x)

[ ages? ] → [ ages ]

var realAges = []
for (var i = 0; i < ages.length; i++) {
  if (!isNaN(ages[i])) realAges.push(ages[i])
}

filter

var realAges = ages.filter(x => !isNaN(x))

[ ages ] → [ positive ages ]

var positiveAges = []
for (var i = 0; i < realAges.length; i++) {
  if (realAges[i] >= 0) positiveAges.push(realAges[i])
}

filter

var positiveAges = realAges.filter(x => x >= 0)

[ positive ages ] → sum ages

var sumAges = 0
for (var i = 0; i < positiveAges.length; i++) {
  sumAges += positiveAges[i]
}

reduce

var sumAges = positiveAges.reduce((acc, x) => acc + x)

tout ensemble

var currentYear = moment().year()

var births = captains.map(x => x.birth) // 1
var ages = births.map(x => currentYear - x) // 2
var realAges = ages.filter(x => !isNaN(x)) // 3
var positiveAges = realAges.filter(x => x >= 0) // 4
var sumAges = positiveAges.reduce((acc, x) => acc + x) // 5

var meanAges = sumAges / positiveAges.length

Chainer ?

var currentYear = moment().year()

var positiveAges = captains
  .map(x => x.birth)
  .map(x => currentYear - x)
  .filter(x => !isNaN(x))
  .filter(x => x >= 0)

var sumAges = positiveAges.reduce((acc, x) => acc + x)

var meanAges = sumAges / positiveAges.length

Fusion ?

var currentYear = moment().year()

var positiveAges = captains
  .map(x => currentYear - x.birth)
  .filter(x => !isNaN(x) && x >= 0)

var sumAges = positiveAges.reduce((acc, x) => acc + x)

var meanAges = sumAges / positiveAges.length

Nommer les transformations

var currentYear = moment().year()

var positiveAges = captains
  .map(x => x.birth) // pluck
  .map(x => currentYear - x) // sub
  .filter(x => !isNaN(x)) // negation, casting
  .filter(x => x >= 0) // positive

var sumAges = positiveAges.reduce((acc, x) => acc + x) // add

var meanAges = sumAges / positiveAges.length

math utils → add, subFrom, positive

// math
var add = (a, b) => a + b
var subFrom = a => b => a - b
var positive = n => n >= 0

var currentYear = moment().year()

var positiveAges = captains
  .map(x => x.birth) // pluck
  .map(subFrom(currentYear))
  .filter(x => !isNaN(x)) // negation, casting
  .filter(positive)

var sumAges = positiveAges.reduce(add)
var meanAges = sumAges / positiveAges.length

object utils → prop

var add = (a, b) => a + b
var subFrom = a => b => a - b
var positive = n => n >= 0
// object
var prop = k => o => o[k]

var currentYear = moment().year()

var positiveAges = captains
  .map(prop('birth'))
  .map(subFrom(currentYear))
  .filter(x => !isNaN(x)) // negation, casting
  .filter(positive)

var sumAges = positiveAges.reduce(add)
var meanAges = sumAges / positiveAges.length

logic utils → not

var add = (a, b) => a + b
var subFrom = a => b => a - b
var positive = n => n >= 0
var prop = k => o => o[k]
// logic
var not = f => (...args) => !f(...args)

var currentYear = moment().year()

var positiveAges = captains
  .map(prop('birth'))
  .map(subFrom(currentYear))
  .filter(not(isNaN))
  .filter(positive)

var sumAges = positiveAges.reduce(add)
var meanAges = sumAges / positiveAges.length

Fusion filters ? → and

var add = (a, b) => a + b
var subFrom = a => b => a - b
var positive = n => n >= 0
var prop = k => o => o[k]
var not = f => (...args) => !f(...args)
var and = (f, g) => x => f(x) && g(x)

var currentYear = moment().year()

var positiveAges = captains
  .map(prop('birth'))
  .map(subFrom(currentYear))
  .filter(and(not(isNaN), positive))

var sumAges = positiveAges.reduce(add)
var meanAges = sumAges / positiveAges.length

Fusion maps ? → compose

var add = (a, b) => a + b
var subFrom = a => b => a - b
var positive = n => n >= 0
var prop = k => o => o[k]
var not = f => (...args) => !f(...args)
var and = (f, g) => x => f(x) && g(x)
var compose = (f, g) => x => f(g(x))

var currentYear = moment().year()

var positiveAges = captains
  .map(compose(subFrom(currentYear), prop('birth'))) // r to l
  .filter(and(not(isNaN), positive))

var sumAges = positiveAges.reduce(add)
var meanAges = sumAges / positiveAges.length

Rich Hickey

David Nolen

Collections intermédiaires

Process

Nommer les fonctions

array.map(fn)

array.filter(fn)

array.reduce(fn, seed)

Callbacks !

array.map(callback)

array.filter(callback)

array.reduce(callback, seed)

Yo dawg!

Nommer les fonctions

array.map(projection)

array.filter(predicate)

array.reduce(reducer, seed) // Dan Abramov style!

map → reduce

const births = captains.map(x => x.birth)

specific reducer

const birthReducer = (acc, x) => {
  acc.push(x.birth)
  return acc
}

const births = captains.reduce(birthReducer, [])

map → reduce

const prop = k => o => o[k] // projection
const births = captains.map(prop('birth'))

specific reducer

const birthReducer = (acc, x) => {
  acc.push(prop('birth')(x))
  return acc
}

const births = captains.reduce(birthReducer, [])

map → reduce

const prop = k => o => o[k]

// projection → reducer
const mapReducer = projection => (acc, x) => {
  acc.push(projection(x))
  return acc
}

const births = captains.reduce(mapReducer(prop('birth')), [])

filter → reduce

const positiveAges = realAges.filter(x => x >= 0)

specific reducer

const positiveAgesReducer = (acc, x) => {
  if (x >= 0)
    acc.push(x)
  return acc
}
const positiveAges = realAges.reduce(positiveAgesReducer, [])

filter → reduce

const positive = n => n >= 0 // predicate
const positiveAges = realAges.filter(positive)

specific reducer

const positiveAgesReducer = (acc, x) => {
  if (positive(x))
    acc.push(x)
  return acc
}
const positiveAges = realAges.reduce(positiveAgesReducer, [])

filter → reduce

const positive = n => n >= 0

// predicate → reducer
const filterReducer = predicate => (acc, x) => {
  if (predicate(x))
    acc.push(x)
  return acc
}
const positiveAges = realAges.reduce(filterReducer(positive), [])
const positiveAges = captains
  .map(prop('birth'))
  .map(subFrom(currentYear))
  .filter(not(isNaN))
  .filter(positive)
const positiveAges = captains
  .reduce(mapReducer(prop('birth')), [])
  .reduce(mapReducer(subFrom(currentYear)), [])
  .reduce(filterReducer(not(isNaN)), [])
  .reduce(filterReducer(positive), [])

mapReducer vs filterReducer

// projection → reducer
const mapReducer = projection => (acc, x) => {
  acc.push(projection(x))
  return acc
}
// predicate → reducer
const filterReducer = predicate => (acc, x) => {
  if (predicate(x))
    acc.push(x)
  return acc
}

push

Concat?

const append = (acc, x) => acc.concat(x)

spreeeeaaddddd…

r = [].concat('yolo') // ['yolo']
r = [].concat(['yolo']) // ['yolo'] WUT? expected: [['yolo']]

Symbol.isConcatSpreadable

var yolos = ['yolo']
yolos[Symbol.isConcatSpreadable] = false
r = [].concat(yolos) // [['yolo']]

Overkill?

Back to push

// le joli reducer!
const append = (acc, x) => {
  acc.push(x)
  return acc
}

mapping

// projection → reducer
const mapReducer = proj => (acc, x) => {
  acc.push(proj(x))
  return acc
}

push → reducer

// projection → reducer  → reducer
const mapping = proj => reducer => (acc, x) => reducer(acc, proj(x))

filtering

// predicate → reducer
const filterReducer = pred => (acc, x) => {
  if (predicate(x))
    acc.push(x)
  return acc
}

push → reducer

  // predicate → reducer → reducer
const filtering =
  pred => reducer => (acc, x) => pred(x) ? reducer(acc, x) : acc

reducing

const positiveAges = captains
  .reduce(mapReducer(prop('birth')), [])
  .reduce(mapReducer(subFrom(currentYear)), [])
  .reduce(filterReducer(not(isNaN)), [])
  .reduce(filterReducer(positive), [])
const positiveAges = captains
  .reduce(mapping(prop('birth'))(append), [])
  .reduce(mapping(subFrom(currentYear))(append), [])
  .reduce(filtering(not(isNaN))(append), [])
  .reduce(filtering(positive)(append), [])

chain reducers

const positiveAges = captains
  .reduce(mapping(prop('birth'))(append), [])
  .reduce(mapping(subFrom(currentYear))(append), [])
  .reduce(filtering(not(isNaN))(append), [])
  .reduce(filtering(positive)(append), [])
const positiveAgesReducer =
  mapping(prop('birth'))
    (mapping(subFrom(currentYear))
      (filtering(not(isNaN))
        (filtering(positive)
          (append))))

const positiveAges = captains.reduce(positiveAgesReducer, [])

binary composition

const compose = (f, g) => x => f(g(x))

3 fonctions ?

const compose = (f, g, h) => x => f(g(h(x))

n fonctions ?

Ramda.compose // ou _.compose

compose reducers

const positiveAgesReducer = R.compose(
  mapping(prop('birth')),
  mapping(subFrom(currentYear)),
  filtering(not(isNaN)),
  filtering(positive)
)

const positiveAges = captains.reduce(positiveAgesReducer(append), [])

transduce

const transduce = (transform, reducing, acc, input)
  => input.reduce(transform(reducing), acc)

transduce

const positiveAgesTransform = R.compose(
  mapping(prop('birth')),
  mapping(subFrom(currentYear)),
  filtering(not(isNaN)),
  filtering(positive)
)

const positiveAges = transduce(positiveAgesTransform, append, [], captains)

complete

const R = require('ramda')
const captains = require('./captains')

const add = (a, b) => a + b
const subFrom = y => b => y - b
const not = fn => (...args) => !fn(...args)
const positive = n => n >= 0
const prop = (k) => o => o[k]

const currentYear = (new Date).getFullYear()

const mapping = proj => reducer => (acc, x) => reducer(acc, proj(x))
const filtering = pred => reducer => (acc, x) => pred(x) ? reducer(acc, x) : acc
const append = (acc, x) => { acc.push(x); return acc }

const positiveAgesTransform = R.compose(
  mapping(prop('birth')),
  mapping(subFrom(currentYear)),
  filtering(not(isNaN)),
  filtering(positive)
)

const transduce = (transform, reducing, acc, input) => input.reduce(transform(reducing), acc)

const positiveAges = transduce(positiveAgesTransform, append, [], captains)
const meanAges = positiveAges.reduce(add) / positiveAges.length

ramda all the things!

const R = require('ramda')
const captains = require('./captains')

const currentYear = (new Date).getFullYear()

const transducer = R.compose(
  R.map(R.prop('birth')),
  R.map(R.subtract(currentYear)),
  R.filter(R.complement(isNaN)),
  R.filter(R.lt(0))
)

const positiveAges = R.transduce(transducer, R.flip(R.append), [], captains)

const meanAges = positiveAges.reduce(R.add) / positiveAges.length
console.log(meanAges)

Conclusion

  • plus de collections intermédiaires
  • transformation indépendante et donc réutilisable
  • arrays, generators, observables, streams…

Smith

Blogs

Videos

https://www.youtube.com/watch?v=6mTbuzafcII https://www.youtube.com/watch?v=4KqUvG8HPYo

Slide

http://slides.com/plaid/deck http://www.slideshare.net/fxposter/transducers-in-javascript

Projects

https://github.com/jlongster/transducers.js https://github.com/cognitect-labs/transducers-js https://github.com/ramda/ramda

…et donc ?

const captainAge = R.transduce({
  shipAtSea: true,
  from: [42.3601, 71.0589],
  payload: "cotton",
  tonnage: 200,
  to: [49.4944, 0.1079],
  brokenMast: true,
  sailor: "forecastle",
  passengers: 12,
  wind: ["N", "E", "E"],
  time: "15:15:00",
  month: 5
})

42

C'était :

Les transducers depuis son transat

by @Delapouite