Practical Functional Programming using Ramda.js

Nick Ribal

  github.com/elektronik2k5

    @elektronik2k5

image/svg+xml

working @

il.linkedin.com/in/elektronik

Why is Vlad angry?

"Too many classes, too many bugs!" - Vlad

The problem with OO languages is they’ve got all this implicit environment that they carry around with them.

You wanted a banana but what you got was a gorilla holding the banana and the entire jungle.

Joe Armstrong, "Coders at Work"

OOP anonymous

got hurt by OOP

wrote class hierarchies

wrote mixins

saw that testing is hell

spent waaay too much time debugging instead of reasoning

Hello, my name is Nick and I too...

There has to be a better way...

OOP focuses on the differences in data

  • Data and operations are tightly coupled

  • Central abstraction is the data itself

  • You compose new objects and extend existing ones by adding new methods

FP concentrates on consistent data structures

  • Data is decoupled from functions 

  • Central abstraction is the function and the way they are combined or expressed

Goals for this talk

  • 20% effort for 80% profit

  • stuff you can use right now

Let's put the fun back in function! =)

Introducing Ramda.js

image/svg+xml

Concepts

  • Currying and partial application

  • Predicate: a function returning a Boolean

  • Lists: Array, Strings & Array like Objects

  • Arity: a function’s number of arguments

  • Placeholder for partial application: R.__

This is just the tip of the iceberg

Currying

A curried function returns a new function until it receives all it’s arguments, and only

then runs, returning

the value

Haskell Curry

Delicious currying

function logger(name, message){
  console.info(name + ' says:', message)
}
var namedLogger = R.curry(logger)

var monsterLogger = namedLogger('Cookie monster')
monsterLogger('OM NOM NOM')
monsterLogger('ALL YOUR COOKIES ARE BELONG TO US!')
// >> 'Cookie monster says: OM NOM NOM'
// >> 'Cookie monster says: ALL YOUR COOKIES ARE
// >> BELONG TO US!'

var billLogger = namedLogger('Bill'),
    billMessages = ['Bill can curry', 'Be like Bill']
R.forEach(billLogger, billMessages) // side effect
// >> 'Bill says: Bill can curry'
// >> 'Bill says: Be like Bill'

MOAR currying and DRYing

function converter(toUnit, factor, offset, input){
  offset = offset || 0
  return (offset+input)*factor).toFixed(2) + toUnit
}

var convert = R.curry(converter)

var milesToKm =          convert('km', 1.609, null),
    poundsToKg =         convert('kg', 0.454, null),
    farenheitToCelsius = convert('°C', 0.5556, -32)

milesToKm(10)
// >> "16.09km"
poundsToKg(2.5)
// >> "1.14kg"
farenheitToCelsius(98)
// >> "36.67°C"

Auto currying (AKA magic)

var DEFAULTS = {
  filename: 'webpack-[name].js',
  path: '//static.90min.com/assets/',
}
// DEFAULTS are shared and overridable
var CONFS_BY_ENV = {
  dev:  { path: '//dev.90min.com:8080/assets' },
  qa:   { filename: 'WEBPACK-[name].QA.js' },
  prod: {}, // matches DEFAULTS
}

// auto-currying: R.merge(DEFAULTS) returns a fn,
// which expects a second argument: a specific env
// conf object to override DEFAULTS with. It then
// returns a new object, leaving originals intact.
var CONFS = R.map(R.merge(DEFAULTS), CONFS_BY_ENV)

Output is

{
  dev: {
    filename: 'webpack-[name].js',
    path: '//dev.90min.com:8080/assets',
  },
  qa: {
    filename: 'WEBPACK-[name].QA.js',
    path: '//static.90min.com/assets/',
  },
  prod: {
    filename: 'webpack-[name].js',
    path: '//static.90min.com/assets/',
  },
}

Composition

var listOfWords = ['foo', 'bar', 'baz'],
    first = list => list[0]
first(listOfWords)
// >> 'foo'

var reverse = list => list.reverse()
reverse(listOfWords)
// >> ['baz', 'bar', 'foo']

var last = R.compose(first, reverse)
last(listOfWords)
// >> 'baz'

var lastLeftToRight = R.pipe(reverse, first)
lastLeftToRight(listOfWords)
// >> 'baz'

Strings

var first =   list => list[0],
    reverse = list => list.reverse(),
    last =    R.compose(first, reverse)

var words = 'foo bar baz'

last(words)
// >> Uncaught TypeError: list.reverse is not
// >> a function

// Because 'list' in reverse() is a String,
// which doesn't have a 'reverse' method on
// it's prototype.

'Array like's

var first =   list => list[0],
    reverse = list => list.reverse(),
    last =    R.compose(first, reverse)

var listOfWords = ['foo', 'bar', 'baz']

function lastArg(){ return last(arguments) }
lastArg.apply(null, listOfWords)
// >> Uncaught TypeError: list.reverse is not
// >> a function

// Because 'list' in reverse() is an Arguments
// object, which is an 'Array like' object. It
// lacks Array.prototype's methods (it has
// numeric keys, length and a few other
// properties).

R treats Strings & Arrays as 'lists'

var first =   list => R.head(list),
    reverse = list => R.reverse(list),
    last =            R.compose(first, reverse)
function lastArg(){ return last(arguments) }

var listOfWords = ['foo', 'bar', 'baz']
lastArg.apply(null, listOfWords)
// >> 'baz'

var words = R.join(listOfWords, ' ') // 'foo bar baz'
last(words)
// >> 'z'
// Oops, while it works, it's not quite what we meant...

var isntSpace = char => char !== ' ' // <== predicate

// this is the essence of functional composition!
var lastWord = R.pipe(R.takeLastWhile(isntSpace), R.join(''))
lastWord(words)
// >> 'baz'

Fetch from API & transform

var responseSample = {
  result: "SUCCESS",
  tasks: [
    { id: 104,
      complete: false,          priority: "high",
      dueDate:  "2013-11-29",   username: "Scott",
      title:    "Do something", created:  "9/22/2013"},
    { id: 105,
      complete: false,          priority: "medium",
      dueDate:  "2013-11-22",   username: "Lena",
      title:    "Have fun",     created:  "9/22/2013"},
    { id: 107,
      complete: true,           priority: "high",
      dueDate:  "2013-11-22",   username: "Mike",
      title:    "Fix the bug",  created:  "9/22/2013"},
    // , ...
  ]
}

We need

getIncompleteTaskSummaries("Scott").then(log);

// Will log:
[
  {
    id: 110,
    title: "Rename everything", 
    dueDate: "2013-11-15",
    priority: "medium"
  },
  {
    id: 104,
    title: "Do something", 
    dueDate: "2013-11-29",
    priority: "high"
  }
]

Old skool

function getIncompleteTaskSummaries(membername){
  return fetchData()
    .then(function(data){ return data.tasks })
    .then(function(tasks){
      var results = [];
      for (var i = 0, len = tasks.length; i < len; i++) {
        if (tasks[i].username == membername) {
          results.push(tasks[i]);
        }
      }
      return results;
    })
    .then(function(tasks){
      var results = [];
      for (var i = 0, len = tasks.length; i < len; i++) {
        if (!tasks[i].complete) {
          results.push(tasks[i]);
        }
      }
      return results;
    })
    .then(function(tasks){
      var results = [], task;
      for (var i = 0, len = tasks.length; i < len; i++) {
        task = tasks[i];
        results.push({
          id: task.id,
          dueDate: task.dueDate,
          title: task.title,
          priority: task.priority
        })
      }
      return results;
    })
    .then(function(tasks) {
      tasks.sort(function(first, second) {
        var a = first.dueDate, b = second.dueDate;
        return a < b ? -1 : a > b ? 1 : 0;
      });
      return tasks;
    });
}

R.pipeP(romise) + R.awesome

var getIncompleteTaskSummaries = R.pipeP(
  fetchData(),
  R.prop('tasks'),
  R.filter(R.propEq('username', username)),
  R.reject(R.propEq('complete', true)) 
  R.map(
    R.pick(
      ['id', 'dueDate', 'title', 'priority']
    )
  ),
  R.sortBy(R.get('dueDate')),
)

getIncompleteTaskSummaries("Scott").then(log)

Why Ramda?

Inspiration & sources

If you are curious about FP, check these out!

Thank you and

stay curious :)

Practical Functional Programming Using Ramda.js

By Nick Ribal

Practical Functional Programming Using Ramda.js

Learn how easy it is to write functional programming in JS using Ramda.js. Master simple, yet practical FP concepts and apply to your daily work to produce more robust and DRY code. Let’s put the ‘fun’ back in function!

  • 2,801