# Thinking in Ramda

node v10.20.1
version: 0.1.0
sharetweet
Based on http://randycoulman.com/blog/categories/thinking-in-ramda/
Pure Functions When writing functional programs, it eventually becomes important to work mostly with so-called “pure” functions. Pure functions are functions that have no side-effects. They don’t assign to any outside variables, they don’t consume input, they don’t produce output, they don’t read from or write to a database, they don’t modify the parameters they’re passed, etc. Immutability Another important concept in functional programming is that of “immutability”. What does that mean? “Immutable” means “unchangeable”. When I’m working in an immutable fashion, once I initialize a value or an object I never change it again. That means no changing elements of an array or properties of an object.
const R = require('ramda') const books = [ { title: 'a', year: 2018 }, { title: 'b', year: 2018 }, { title: 'c', year: 2017 } ]
Ramda filter / reject
const { filter, reject, find } = R const isEven = x => x % 2 === 0 filter(isEven, [1, 2, 3, 4]) // --> [2, 4] reject(isEven, [1, 2, 3, 4]) // --> [1, 3] filter(isEven, [1, 2, 3, 4]) // --> 2
Ramda reduce
const { reduce } = R let add = (accum, value) => accum + value reduce(add, 5, [1, 2, 3, 4])
Combining Functions Once you’ve gotten used to the idea of passing functions to other functions, you might start to find situations where you want to combine several functions together. Ramda complement Ramda provides a higher-order function, complement, that takes another function and returns a new function that returns true when the original function returns a falsy value, and false when the original function returns a truthy value.
const { complement } = R let isOdd = complement(isEven) find(isOdd, [1, 2, 3, 4]) // --> 1
Ramda map & filter
const { map } = R let publishedInYear = (book, year) => book.year === year let titlesForYear = (books, year) => { const selected = filter(book => publishedInYear(book, year), books) return map(book => book.title, selected) } titlesForYear(books, 2018)
Higher order function
publishedInYear = year => book => book.year === year // HOF titlesForYear = (books, year) => { const selected = filter(publishedInYear(year), books) return map(book => book.title, selected) } titlesForYear(books, 2017)
Ramda partial and partialRight - allows calling treating a function like a HOF or like a curried function
const { partialRight } = R publishedInYear = (book, year) => book.year === year titlesForYear = (books, year) => { const selected = R.filter( partialRight(publishedInYear, [year]), books ) return map(book => book.title, selected) } titlesForYear(books, 2017)
Ramda curry Notice that to make curry work for us, we had to reverse the argument order. This is extremely common with functional programming, so almost every Ramda function is written so that the data to be operated on comes last. You can think of the earlier parameters as configuration for the operation. So, for publishedInYear, the year parameter is the configuration (what year are we looking for?) and the book parameter is the data (where are we looking for it?).
const { curry } = R publishedInYear = curry((year, book) => book.year === year) titlesForYear = (books, year) => { const selected = filter(publishedInYear(year), books) return map(book => book.title, selected) } titlesForYear(books, 2017)
curry and flip
const { flip } = R publishedInYear = curry((book, year) => book.year === year) titlesForYear = (books, year) => { const selected = filter(flip(publishedInYear)(year), books) return map(book => book.title, selected) } titlesForYear(books, 2017)
curry and placeholder
const { __ } = R publishedInYear = curry((book, year) => book.year === year) titlesForYear = (books, year) => { const selected = filter(publishedInYear(__, year), books) return map(book => book.title, selected) } titlesForYear(books, 2018)
pipeline
const { pipe } = R publishedInYear = curry((year, book) => book.year === year) titlesForYear = (books, year) => pipe( filter(publishedInYear(year)), map(book => book.title) )(books) titlesForYear(books, 2018)
ramda-fy
publishedInYear = curry((year, book) => book.year === year) titlesForYear = curry((year, books) => pipe( filter(publishedInYear(year)), map(book => book.title) )(books) ) titlesForYear(2018, books)
Ramda arithmetics
const { multiply, inc } = R const square = x => multiply(x, x) const operate = pipe( multiply, inc, square ) console.log(operate(3,4))
Ramda comparison
const { equals, gte, either, both } = R let wasBornInCountry = person => equals(person.birthCountry, 'Romania') let wasNaturalized = person => Boolean(person.naturalizationDate) let isOver18 = person => gte(person.age, 18) let isCitizen = either(wasBornInCountry, wasNaturalized) let isEligibleToVote = both(isOver18, isCitizen) console.log(isEligibleToVote({ birthCountry: 'Romania', age: 19 }))
Ramda defaulting defaultTo checks if the second argument isNil. If it isn’t, it returns that as the value, otherwise it returns the first value.
const { defaultTo } = R const settings = {}; let lineWidth = settings.lineWidth || 80 // What if 0 is a valid setting? Since 0 is falsy, we’ll end up with a line width of 80 lineWidth = defaultTo(80, settings.lineWidth) //
ifElse, always
const { ifElse, lte, always } = R let forever21 = age => age >= 21 ? 21 : age + 1 forever21 = age => ifElse(gte(R.__, 21), () => 21, inc)(age) // curried forever21 = age => ifElse(lte(21), () => 21, inc)(age) forever21 = age => ifElse(gte(R.__, 21), always(21), inc)(age) forever21(50)
identity function, when, unless a => a is equivalent with identity
const { identity, when, unless } = R let alwaysDrivingAge = age => ifElse(lt(__, 16), always(16), a => a)(age) alwaysDrivingAge = age => ifElse(lt(__, 16), always(16), identity)(age) alwaysDrivingAge = age => when(lt(__, 16), always(16))(age) alwaysDrivingAge = age => unless(gte(__, 16), always(16))(age) alwaysDrivingAge(10)
cond, T
const { cond, T } = R let water = temperature => cond([ [equals(0), always('water freezes at 0°C')], [equals(100), always('water boils at 100°C')], [T, temp => `nothing special happens at \${temp}°C`] ])(temperature) water(9)
pointfree style (tacit) Pointfree style takes time to get used to. It can be hard to adapt to the missing data arguments everywhere. It is also important to have some familiarity with Ramda’s functions to know how many arguments they eventually need. But once you get used to it, it becomes very powerful to have a bunch of small pointfree functions combined together in interesting ways. What’s the advantage of pointfree style? One could argue that it’s just an academic exercise designed to win a functional programming merit badge. However, I think there are a few advantages, even in spite of the work it takes to get used to the style: - it makes programs simpler and more concise. This isn’t always a good thing, but it can be. - it makes algorithms clearer. By focusing only on the functions being combined, we get a better sense of what’s going on without the data arguments getting in the way. - it forces us to think more about the transformation being done than about the data being transformed. - it helps us think about our functions as generic building blocks that can work with different kinds of data, rather than thinking about them as operations on a particular kind of data. By giving the data a name, we’re anchoring our thoughts about where we can use our functions. By leaving the data argument out, it allows us to be more creative.
const { lt } = R forever21 = age => ifElse(gte(__, 21), always(21), inc)(age) forever21 = ifElse(gte(__, 21), always(21), inc) alwaysDrivingAge = age => ifElse(lt(__, 16), always(16), identity)(age) alwaysDrivingAge = when(lt(__, 16), always(16)) water = temperature => cond([ [equals(0), always('water freezes at 0°C')], [equals(100), always('water boils at 100°C')], [T, temp => `nothing special happens at \${temp}°C`] ])(temperature) water = cond([ [equals(0), always('water freezes at 0°C')], [equals(100), always('water boils at 100°C')], [T, temp => `nothing special happens at \${temp}°C`] ]) isCitizen = person => either(wasBornInCountry, wasNaturalized)(person) isCitizen = either(wasBornInCountry, wasNaturalized) isEligibleToVote = person => both(isOver18, isCitizen)(person) isEligibleToVote = both(isOver18, isCitizen)
titlesForYear = curry((year, books) => pipe( filter(publishedInYear(year)), map(book => book.title) )(books) ) titlesForYear = year => // not ideal pipe( filter(publishedInYear(year)), map(book => book.title) )
reading object properties In order to make these functions pointfree, we need a way to build up a function that we can then apply to the person at the end. The problem is that we need to access properties on the person and the only way we know how to do that is imperatively.
wasBornInCountry = person => equals(person.birthCountry, 'Romania') wasNaturalized = person => Boolean(person.naturalizationDate) isOver18 = person => gte(person.age, 18)
Fortunately, Ramda can help us out. It provides the prop function for accessing properties of an object.
const { prop } = R wasBornInCountry = person => equals(prop('birthCountry', person), 'Romania') wasNaturalizad = person => Boolean(prop('naturalizationDate', person)) isOver18 = person => gte(prop('age', person), 18) // put data last wasBornInCountry = person => equals('Romania', prop('birthCountry', person)) // apply currying (where needed) wasBornInCountry = person => equals('Romania')(prop('birthCountry', person)) wasNaturalized = person => Boolean(prop('naturalizationDate', person)) isOver18 = person => gte(__, 18)(prop('age', person)) // curry one more time wasBornInCountry = person => equals('Romania')(prop('birthCountry')( person)) wasNaturalized = person => Boolean(prop('naturalizationDate')(person)) isOver18 = person => gte(__, 18)(prop('age')(person))
Worse again. But now we see a familiar pattern. All three of our functions have the same shape as f(g(person)), and we know from Part 2 that this is equivalent to compose(f, g)(person).
wasBornInCountry = person => compose(equals('Romania'), prop('birthCountry'))(person) wasNaturalized = person => compose(Boolean, prop('naturalizationDate'))(person) isOver18 = person => compose(gte(__, 18), prop('age'))(person)
Apply pointfree style
const { compose } = R wasBornInCountry = compose(equals('Romania'), prop('birthCountry')) wasNaturalized = compose(Boolean, prop('naturalizationDate')) isOver18 = compose(gte(__, 18), prop('age')) isOver18({age: 100})
It wasn’t obvious when we started that our methods were doing two different things. They were both accessing a property of an object and performing some operation on the value of that property. This refactoring to pointfree style has made that very explicit.
Ramda pick, has, path
const { pick, has, path } = R person = { name: 'Pepe', age: 22, address: { zipCode: 221100 } } pick(['name', 'age'], person) has('name', person) path(['address', 'zipCode'], person)
propOr / pathOr propOr and pathOr are similar to prop and path combined with defaultTo. They let you provide a default value to use if the property or path cannot be found in the target object.
const { propOr } = R propOr('<Unnamed>', 'name', person)
key / values keys returns an array containing the names of all of the own properties in an object. values returns the values of those properties.
assoc / assocPath
const { assoc, assocPath } = R let updatedPerson = assoc('name', 'Mary', person) updatedPerson = assocPath(['address', 'zipCode'], 97504, updatedPerson) console.log(updatedPerson)
dissoc / dissocPath / omit
const { dissoc, dissocPath, omit } = R updatedPerson = dissoc('age', updatedPerson) updatedPerson = dissocPath(['address', 'zipCode'], updatedPerson) updatedPerson = omit(['address', 'name'], updatedPerson) // complements pick
Transforming Properties
let nextAge = compose(inc, prop('age')) // returns incremented age nextAge(person) let celebrateBirthday = person => assoc('age', nextAge(person), person) celebrateBirthday(person) // or const { evolve } = R celebrateBirthday = person => evolve({ age: inc }, person) celebrateBirthday = evolve({ age: inc }) // WOW celebrateBirthday(person)
Merging Objects merge returns a new object containing all of the properties and values from both objects. If both objects have the same property, the value from the second argument is used. merge performs a shallow merge. If the objects being merged both have a property whose value is a sub-object, those sub-objects will not be merged. Ramda does not currently have a “deep merge” capability, where sub-objects are merged recursively.
const { merge } = R merge(person, { yolo: 123})
Immutability and Arrays
const { nth, slice, contains } = R const numbers = [10, 20, 30, 40, 50, 60] nth(3, numbers) nth(-2, numbers) slice(2, 5, numbers) contains(20, numbers) const { head, tail, last, init, take, takeLast } = R head(numbers) // => 10 tail(numbers) // => [20, 30, 40, 50, 60] last(numbers) // => 60 init(numbers) // => [10, 20, 30, 40, 50] take(3, numbers) // => [10, 20, 30] takeLast(3, numbers) // => [40, 50, 60]
Adding, Updating, and Removing Array Elements
const { insert, append, prepend, update, concat } = R insert(3, 35, numbers) // => [10, 20, 30, 35, 40, 50, 60] append(70, numbers) // => [10, 20, 30, 40, 50, 60, 70] prepend(0, numbers) // => [0, 10, 20, 30, 40, 50, 60] update(1, 15, numbers) // => [10, 15, 30, 40, 50, 60] concat(numbers, [70, 80, 90]) // => [10, 20, 30, 40, 50, 60, 70, 80, 90]
const { remove, without, drop, dropLast } = R // removes elements by index remove(2, 3, numbers) // => [10, 20, 60] // removes by value without([30, 40, 50], numbers) // => [10, 20, 60] drop(3, numbers) // => [40, 50, 60] dropLast(3, numbers) // => [10, 20, 30]
Transforming elements
const { adjust } = R update(2, multiply(10, nth(2, numbers)), numbers) adjust(2, multiply(10), numbers)
Lenses A lens combines a “getter” function and a “setter” function into a single unit. We can think of a lens as something that focuses on a specific part of a larger data structure.
const { lens, lensProp, lensPath, lensIndex } = R person = { name: 'Randy', socialMedia: { github: 'randycoulman', twitter: '@randycoulman' } } let nameLens = lens(prop('name'), assoc('name')) let twitterLens = lens( path(['socialMedia', 'twitter']), assocPath(['socialMedia', 'twitter']) ) // rewrite nameLens = lensProp('name') twitterLens = lensPath(['socialMedia', 'twitter'])
view / set / over
const { view, set, over, toUpper } = R view(nameLens, person) set(twitterLens, '@randy', person) over(nameLens, toUpper, person)
Eye opening!