Foreword

This page is dedicated to introducing and understanding some FP(Functional Programming) concepts practiced through the use of an open source javascript library, Ramda.js. For those already familiar with lodash and its many functions, you have a head start as ramda contains many overlapping, although differently named, functions.

As a disclaimer the scope of this content is to provide a summary through explanation and examples and is not meant to be comprehensive. I will note that there are other excellent existing and more exhaustive web resources out there. For a great entry point to functional programming, I'd recommend getify's guide, Functional Light JS.

Lastly, I do not claim to be an expert on functional programming. Content here is what I learn over time and is subject to change as my own understanding grows.

That is a lot...help?

Functions! Everywhere! Okay not exactly everywhere, but a lot of them! Let's set a starting point for our understanding.

          
function callIt(a) {
  return a();
}
          
        

Do you see what happened? Our function took its argument, and called it. It means we've passed a function as data to another function and by data here I mean an object, specifically. Wow. Turns out this is a really powerful mechanism and will help drive our momentum in becoming more "functional". Functions that take other functions that themselves may take other functions... it's functions all the way down.

Also note that function declaration syntax was used. There is a valid argument for using declaration|expression or lamda syntax, however I advocate whichever is most clear of indicating its purpose should be used. All styles will be used interchangeably.

So the last example was simple to illustrate our starting point. The next will be a little more complex to further illustrate the idea.

            
const asTimed = function(timerStart, timerStop) {
  return innerTimed(functionToMeasure) {
    timerStart();
    functionToMeasure();
    timerStop();
  }
}
            
          

Here our asTimed function first has two parameters, both functions, which then returns another function, innerTimed, that accepts our target function to measure. A real benefit to this strategy is our ability to provide our timer functions initially, then on demand use it wherever needed. This fairly magical capability is powered by javascripts' use of closures. While I will not provide an explanation for it here, I do encourage the reader to use MDN(Mozilla Developer Network) for that and all other questions they may have.

So far what we have is useful, but what if our function actually took more parameters and some of which were not known at once.

          
const calculatePoints = function(a, b, x, y) {
  return function(formatter) {
    formatter((a + b) / (x + y));
  }
}
          
        

For our imaginary use case, we only know (a,b) at first and (x,y) later. We could defer calling our function until we have all points, or we could even provide yet another inner function that captures (a, b). While these might work, imagine that the time at which each of the variables are available changes. Again, we could simply defer using our function while we, likely, temporarily store our inputs somewhere, or we could follow up on our second route, altering our function to the following.

          
const calculatePoints = a => b
  => x
    => y
      => formatter 
        => formatter((a + b) / (x + y));
          
        

This now works exactly as we need; we continue deferring our work until we have everything as demanded. But, we have a new problem. It has become harder to read and quite verbose. It would be great if there were a function available from a library allowing us to...

Currying and Ramda

Well, you see, our "workaround" actually led us to our next topic, currying. This is the official term for the transformation we applied to our original calculatePoints function. It is powerful, and so vital to our functional endeavors that almost every function exported from Ramda is already curried for you. Using it, we could rewrite the function to the following.

            
const calculatePoints = R.curry(function(a, b, x, y, formatter) {
    formatter((a + b) / (x + y));
});
            
          

Functions reference beginning with 'R' are ramda functions. If you're curious to know how curry or other ramda features do what they do, you can always inspect the source within the ramda repository.

The next point of discussion actually has no points; call it 'point free'. Let's examine a common function pattern used inline for operations that accept them.

            
[1, 2, 3, 4].map(element => element + 1)
            
          

Our element list contains a function property, map, which can be invoked with a function receiving each element. The function to handle the operation was provided inline, in lamda syntax. This works fine, but what if we wanted to use this function later, or if we wanted to reduce the amount of things happening within one line to improve readability? To address the first concern, let's move the inline function to its own function expression.

            
const addTo = element => element + 1;
            
          

Now comes the last concern and the focus of our attention.

            
[1, 2, 3, 4].map(addTo)
            
          

Notice we have plugged our variable addTo directly into the map function and removed the explicit point, or value, mapping. The style is called point free and it can improve the readability of functions as they become more complex. To better understand how this will help, we will combine it with our previous concept, currying, in a code fragment. Do not worry about understanding it fully; for now read it to see how much you can follow using the function names and concepts covered.

            
R.pipe(R.concat(' foo'), R.trim)(' bar ') // "foo bar"
            
          

We begin with a function, pipe, and provide to it two functions, concat and trim. Notice how concat was called with its own argument, ' foo', currying it for use with its next function. Next, trim is provided without arguments; point free at work. Lastly, we call pipe with ' bar '. We could also assign the result of pipe to its own expression as we did with addTo to call it with any string value. Congratulations, you have read your first Ramda expression.

I will admit that the syntax takes some getting used to. Functional style coding requires that the reader know the purpose and output of each part to understand the intent. Considering these functions can then be used to build over functions which may also contain one or more functions, the mental overhead required to track logic increases.

Some of the functionality happening in the example code has been explained, but pipe has been left out. It is a powerful function used often in tying logical pieces together and will be our next topic of discussion.

Team Pipe and Team Compose

Some readers may be familiar with these patterns working with popular tools, and can skip this section. I will briefly touch the two most common sources, lodash and redux, and explain how they relate to ramda functions. Readers may continue with the fuller explanation following that.

Lodash users have access to _.flow, which iterates the provided functions in a left to right manner. Inversely, _.flowRight flows from right to left. In ramda, pipe is the left to right iterator, and compose acts as the right to left. For redux users, compose corresponds exactly to ramda compose, however no inverse function equivalent is exported, though one could created easily.

Take a mental step back for a moment and reflect on what we have so far. Javascript, being a multi-paradigm language, supports functional development by treating functions as data and deferring execution with captured arguments from closures, which we build on with curried functions to be used wherever and however needed. This is a lot to take in and I recommend you pause to experiment with this style and reinforce these concepts with other great resources out there. While all of this is great, we are still missing the crucial piece, or more specifically, connective glue that will become the real workhorse of our programs.

Imagine a one-way railroad that transports trains through specially purposed stations. During the train's visit at each station, the train or its cargo can be modified(or copied) in some way. Once a station's process is complete, the train exits and is transported to the next station wherein the process repeats for subsequent stations. After all stations have been visited, the train continues onward, possibly onto another station lined track, or maybe into a null oblivion, there are countless possibilities. Translating our analogy, the train is our data, the stations functions, and the track is our next focus. We will revisit a previous code example.

          
R.pipe(R.concat(' foo'), R.trim)
          
        

Notice that pipe is called with two functions, one of which is curried with a string. pipe, like the tracks for our train, initially takes multiple functions, our stations, and returns a function to handle our data. Once called, pipe invokes the provided function furthest to the left, concat, with data. After concat operates on the data and returns a value, pipe then invokes the next provided function, trim, with concat's value. trim is our last registered function, so its return output will be our final value. Remember that our functional logic is not limited to inline use and that the result of our pipe call, also a function, can be assigned to a variable for later uses.

Following up on the alluded title, compose is the other ramda offering capable of, well, composing our functions. compose is nearly identical to pipe but differences in that it first invokes the function furthest to the right. The two are each other's reverse form. With this knowledge, you could build one using the other.

          
function reportTrue(input = {}) {
  if (!Object.prototype.hasOwnProperty.call(input, 'processed')) {
    return {
      processed: true,
      value: 'True'
    };
  }
  return input;
}
function reportFalse(input = {}) {
  if(!Object.prototype.hasOwnProperty.call(input, 'processed')) {
    return {
      processed: true,
      value: 'False'
    };
  }
  return input;
}


function pipe(...functions) {
  if (!functions.length) {
    throw Error('Function pipe requires at least one argument!');
  }
  return R.compose(...functions
    .map((func, index, source) =>
      source[(source.length - 1) - index]))
}

R.compose(reportTrue, reportFalse)(); // value: False 
pipe(reportTrue, reportFalse)(); // value: True
          
        

Here the execution order of our reporter functions matter, demonstrated by how compose and pipe operate. The two are effectively the same differing only by execution order, but many developers choose based on prior familiarity or preference for reading style. For instance, it is common for react developers to recognize compose as a redux utility. Either way, functional composition is a vital element for creating reusable segments of logic. I like to think of them as pipelines where their purpose and operation is made as clear as possible from their naming. To that end, it is recommended that you maximize their maintainability by restraining their scope and not providing names that are terse, const toObjCntAsc = compose(...) or vapid, const fieldsToFieldsMapper = pipe(...). Remember that composed functions themselves can be combined or used within with other composed functions, so well defined scopes and names greatly help when debugging to isolate an issue.

But they're more like guidelines

Previous sections have dropped general recommendations and ramda advice, and in this section we will focus on those. Points covered draw from personal experience and guidance obtained from other developers through their experience or their tooling.

The hardest problem

Naming is highly important. Carefully track the input and output of the functional process and capture that as best you can. Explaining it verbally helps. Put that rubber duck (or chicken) to use. If this is really hard or taking longer than you thought, it might be better to break it into smaller pieces. By example, pretend you come across the following in an old file while working on a high priority task and find it is used in multiple places.

          
export R.compose(R.apply((a, b) => ({...a, ...b})),
  R.values,
  R.converge(R.mergeRight,
    [R.identity, R.identity]), R.juxt([R.countBy(R.identity),
  word => ({ value: word })]), R.split(''));
          
        

Perhaps the segment is as painful to read as it was to write. Admittedly the logic is contrived, but the point is poor design. There might be a unit test or two that shows you the output, but what if you discover a bug or need to modify it.

I implore you to resist any urge to write your function as cleverly as you can stretch your brain to construct. Here, trying to overfill a single function with logic produces enormous mental overhead, and can result in the most vague ramda "error" possible, another function. Good luck finding where you went wrong! Think carefully through your logic, cover it with tests, and then use it in conjunction with other things. If you remember only one thing from this guide, let it be the importance of carefully named, modular functions. A reorganized version with some optimization follows.

          
const toValue = word => ({ value: word });
const wordCountPairs = R.countBy(R.identity);
const lettersAndWordList = R.juxt([wordCountPairs, toValue])
const combineFromList = R.apply(R.mergeRight)

export const withValueAndLetterCount =
  R.compose(combineFromList, lettersAndWordList, R.split(''));
          
        

Side effects

This recommendation has a caveat and should be defined first to give context. The type of side effect under discussion here is the use of data or objects within a function's implementation that originate outside of its own scope. Ideally, a function should only operate on the arguments provided to it, making its output determined only by its input. Functions respecting this style are known as referentially transparent. Essentially, if you swapped the call site of your function with its output, the end result should remain the same.

          
// Only dependent on a and b.
function add(a, b) {
  return a + b;
}

function main() {
  ...
  const result = otherFunction() + add(1,2);
  // or
  const results = otherFunction() + 3;
}
          
        

Most importantly, there are no side effects of running add. It could be called hundreds of times and the result would be predictable without any surprises; known as idempotent behavior. Why does this matter? Look through the following example scenario.

          
const sharedState = [];      
const SPECIFIC_KEY = 42.24;

// somewhere in the file or another file importing it
function handler(value) {
  if(!sharedState[value]) {
    internalFunction(value);
  } else {
    sharedState.push({
      [SPECIFICY_KEY]: obj 
    });
  }
}
          
        

Multiple functions in this file are responsible for managing some internal state which may or may not be used by other files which also may or may not be mutating it. The problem here is that our functions pull in objects through a closure and changes it. If either of these functions are called, we have no way of knowing the result since there is no return value and no visibility on whether sharedState was updated correctly. sharedState and SPECIFIC_KEY are like global variables used by our functions to execute side effects. Reading the function signature, the only an input, value, is visible. There is no indication of actions beyond that, so modifications to the program's internal state after calling the function is a surprise; a surprise effect.

Ideally, a function warrants its usability and purpose through appropriate naming and signature. Adhering to this principle better facilitates two major concerns. In order to provide reasonable proof that code meets standards, tests are utilize to cover numerous scenarios. When code under test relies on shared mutable state or variables that cannot easily be replaced(hard-coded constants) or used(expensive database calls), we are forced to regard much of its behavior as a blackbox beyond control or testing, so the value of our proof drops. Functionality around side effects also goes against functional programming principles which emphasize a declarative "what you want" style. In tightly coupling our behavior with state, possibly across multiple modules, we are intricately describing how to do something. Ramda offers functions, map, any, pipe, and many more that handle implementation details for you and depend only their arguments. Combining them with side effect laden code hurts our ability to reason about and reuse these functions.

With all of that in mind, I will step off my soap box to address some problems with it. I described it earlier as having a caveat and being ideal. This is because things are not always so straightforward. In order to fully integrate referential transparency throughout our code, it would ultimately involve some potentially undesirable verbosity. Code would require any external functionality, like services and utilities, to be passed in as arguments. While that does help with testing, it also places a higher burden on the consumer to provide all these dependencies. We could provide some curried wrappers in various forms that aid there, but that may also be too arduous a task depending on the complexity of the code base. I do not believe there is a one size fits all answer for this, but my recommendation is to write code as idempotent as possible.

The best for last

The final suggestion comes from ramda's own philosophy, so I place extra emphasis on it. When constructing your functions, defer your data and prefer your options and configuration. Wait what? I will explain with the following function.

          
function slice(index, data) {
  return data.slice(index)
}
          
        

We explicitly order our data, the most dynamic piece we will operate on, last. All parameters that are known ahead of time are placed before our data. This is crucial to support a previously discussed concept, currying. With our data last, we are able to construct a reusable function containing arguments usable on multiple inputs.

          
const sliceLast = R.curry(function slice(index, data) {
    return data.slice(index)
  })(-1)
sliceLast('ramda') // 'a'
sliceLast('lodash') // 'h'
          
        

It is also key for functional composition when using ramda's common functions, pipe and compose.

          
compose(
  emitChange(bind(changeModule, dispatcher)),
  concat([existingState]),
  createHandler(info, ['prop1'])
)
          
        

Notice all of the known data points are provided upfront to each of the functions, deferring our data for last. When the function signature or executing context make this difficult, there are other functions that can help us. flip reverses two arguments provided to a function before passing them through, and there is also reverse which does the same for a list.

          
const takeFive = R.flip(R.curryN(2, _.take))(5);
// later
R.pipe(processTopElements, takeFive)(dataArray)
          
        

Reflection

At this stage, take a momentary pause to gather your thoughts on the content so far, and, hopefully, your own practical experimentation in applying them. As was mentioned before, the intent of this guide is to provide you with a careful balance of explanation and demonstration. Each of the topics discussed can and should be explored more fully to further strengthen your understanding. Honestly, it took me some time to wrap my head around most of this, and I know there is still much more to learn. The functional, declarative way of doing things in javascript differs significantly from the object-oriented, imperative style that many developers have likely started their own journey in. It requires experience to recognize abstract-able patterns and the commitment to do so while following sound design principles, SOLID, DRY, etc. That might be a harsh selling point for some, but do note improved maintainability and error reduction are the major gains of this effort.

In the remaining sections of this guide, I attempt to provide an analysis of one more interesting functional concept and its practical benefits. The final section provides simple practice problems meant to be solved utilizing ramda functionality; the extent of which is up to the implementer. The guides and the practice problems may be found at sensible-ramda. The project depends on chai and mocha for testing, esm in case there are environmental export and module clashes, and ramda using node|npm.

Transwhat?

Transduce. While it may sound like a fancy unapproachable term, it is not so bad once you get to know it. Here is my best one sentence summary to jump start understanding. A uniquely powerful mechanism serving as an optimization technique for processing large or continuous sets of data. That is most practical answer I can offer, but transducers, like so many other functional programming concepts, has a lot of, we will call it lore, behind it for those that prefer the exhaustive learning approach. So, before we delve into this concept, keep in mind one key element we will revisit later: abstraction. We will start with a small data set; an array containing a few objects with common properties.

          
const users = [
  {
    "id":1,
    "last_name":"Learie",
    "email":"plearie0@comsenz.com",
    "gender":"Female",
    "ip_address":"57.195.62.150"
  },
  // ...
  ]
          
        

From this set, we would like to apply a series of operations, combining some filters and a map. The first filter includes data with last_name beginning with a given pattern. The second filter includes data with ip_address containing three decimal numbers representing the first eight bits. Lastly, our map projects a new property, derived_name which is a combination of last_name and email.

          
function lastNameStartsWith(pattern) {
  return function(data) {
    return data.last_name.startsWith(pattern);
  }
}

function ipIsMaxCount(data) {
  return data.ip_address.match(/^([0-9]+)./)[1].length === 3;
}

function addDerivedName(data) {
  return {
    ...data,
    derived_name: `${data.last_name}, ${data.email}`,
  };
} 
          
        

With our desired operations defined, we will apply them.

          
const conventionalResult = users
  .filter(lastNameStartsWith('M'))
  .filter(ipIsMaxCount)
  .map(addDerivedName);
          
        

Fairly standard, no tricks. The filters are applied first, then the map last. So am I saying there is a problem here? No, this is a nice, expressive format for making sense of our data and is a pattern I use frequently. Countless applications in production environments are likely to use code just like this and probably do just fine. But what if we altered one thing from this example: the users size. Instead of an array of three, or maybe even a thousand users, our data contained hundreds of thousands of records, or at least enough to indicate our app has a significant performance problem. To understand how our example code could be causing this issue, we have to break down our instructions.

We rely on two native javascript array operations, filter and map. The first filter applies our operation to the data, the result of that goes to the second filter, then the remaining results is feed into to the map. The key issue here is how our mechanisms are applied to our data. After the first filter operation iterates through the array, the result is handed off to the second filter via another array, which is iterated by the second filter. Again, the results of the second filter are sent to map in another array wherein map iterates over that array one last time. The common pattern between them is looping through the entire array, then storing the resultant into a new array, wherein the process repeats. For an enormous sized array, the machine may run out of usable memory, experience heavily degraded performance from thread bottlenecks, or even crash.

Some solutions to this problem would seek to limit the amount of data processed by reducing or redistributing it before sending it to the application. We could take a similar approach from within the application by slicing the data into manageable chunks. Any of these might alleviate the issue, but what if could fix the problem by keeping our reduction functions the same and focus our attention on the inefficiencies just described? Enter transducers.

With the problem scenario explained and my short summary provided earlier in mind, I will cut straight to the selling point. Our transduce function will utilize the same filter and map functions while applying them to the data set while iterating only once. Absolutely crazy right? To understand more on how this is possible, we will create our own simple transduce method from scratch using ramda only to help us tie it all together. It might sound daunting, but the simplicity and work required may surprise once you have wrapped your head around it.

We will begin with our original solution. Our issue was the inline chaining of filters and maps. So if can not use that pattern, what else is left? A hint is in the name of what we are learning. A reducer. Great, so that means our reduction functions have to be rewritten right? Nope. It all begins with the signature of reduce, specifically the accumulator and current value. In effect, we want to create a process that will fit our existing functions into a reducer signature. This is important because each function can pass along the total output of the data source being processed as well as each value without introducing another set of values, like map, or removing values entirely, like filter. Think back really hard to the element I said keep with you: Abstraction. In order to fit our functions together, like pipes in a pipeline, we need to abstract two vital pieces of our functions.

If you re-examine how our functions are written, each expects data which is an individual piece and not the entire set, then applies some logic to it. How they apply our logic differs by our intent; either we test it to see if it matches a pattern, filtering, or we alter it in some way then pass it through, mapping. Getting any ideas yet? In one case, we conditionally apply a function based on a predicate while the other applies to every value.

          
const filterReducer = predicate => (accumulator, currentValue) => {
  if(predicate(currentValue)) {
    // True, so we do want this value.
    return accumulater.concat(currentValue);
  }
  // False, so skip it.
  return accumulator;
}

const mapReducer = mapTransformer = (accumulator, currentValue) => {
  return accumulator.concat(mapTransformer(currentValue));
}
          
        

Cool. Hopefully this makes sense so far. To get here though, somewhat of an exchange was made. We abstracted our operations, predicate and transformer, to a function passed in, but we also added in more imperative logic handling the result of those calls; injecting implementation details we may not want. In both cases we simply take the accumulator array and concat it with the resultant from our deferred functions. Our implementation is getting closer to what we want. The hint of what happens next is in detailing of the abstraction trade-off. Instead of our filter and map patterns manually handling the resultant, why not use our super power, abstraction, once more?

          
function filterReducer(predicate) {
  return function(combiner) {
    return function(accumulator, currentValue) {
      if(predicate(currentValue)) {
        return combiner(accumulator, currentValue);
      }
      return accumulator;
    }
  }
}

function mapReducer(mapTransformer) {
  return function(combiner) {
    return function(accumulator, currentValue) {
      return combiner(accumulator, mapTransformer(currentValue));
    }
  }
}
          
        

Take a minute to review our new implementation. All that happened is the removal of our concat action in exchange for a function that will handle making sense of the accumulator and current value. I also switched to function declaration form for improved readability. With ramda, we could also have written one function taking all four parameters wrapped with R.curry to provide the same experience with more clarity. The next noteworthy change is the order of the parameters. Recalling previous guidance about deferring dynamic data and preferring static data, we know our filter and map functions well ahead of time. Technically the application of the combiner function and the reduction functions happens around the same time, however the reason for their ordering serves as the next focus point. For clarity and sanity, let us examine how to use these solutions.

          
const lastNameReducer = filterReducer(lastNameStartsWith('M'));
const ipLengthReducer = filterReducer(ipIsMaxCount);
const derivedNameReducer = mapReducer(addDerivedName);
          
        

There should be no surprises here. The abstracted filter and map enabling functions have been renamed as reducers and in creating them we provide our reduction functions first. Based on our reducer functions, the next input should be the combiners. So does this mean we do something similar by passing something to each reducer function and use those? Not quite. These functions are our individual "pipes" to make our "pipeline".

          
const usersMeetingRequirementsReducers =
  R.compose(lastNameReducer, ipLengthReducer, derivedNameReducer)
          
        

This new function is what we would provide our combiner to. So what exactly does our combiner look like? Earlier we used concat but for efficiency reasons we could mutate our accumulator some.

          
function userCombiner(users, user) {
  return users.concat(user);
}
// or
function userCombiner(users, user) {
  users.push(user);
  return users;
};
          
        

With the combiner ready, we are very close to a completed transducer. Pay close attention to the next step because it is simultaneously the hardest to understand and the source of the magic.

          
const usersMeetingRequirementsTransducer =
  usersMeetingRequirementsReducers(userCombiner);
          
        

You can gasp now, functions are intense. The magic lives within the chained calls happening. Through ramda's compose, our userCombiner is sent to the right most function, derivedNameReducer, which itself returns a reducer function passed to ipLengthReducer as its combiner, which again, returns the reducer used as the combiner to lastNameReducer. The final output is a merger of our reducers which contain our filter and map functions. If you resoundingly understood that on the first pass through, excellent work. For the rest of us this, take a breather and mentally chew on it for a while. Here is our final line to consume it.

          
big_data_set.reduce(usersMeetingRequirementsTransducer, [])
          
        

Going back to the original selling point, our reduce iterates over the large dataset once with each reducer, pending on its purpose, operating on and passing along the accumulator array and the current piece of data to the next reducer. Now you're thinking with transducers. What is nice about our functions here is the reusability. usersMeetingRequirementsReducers could be used with a different combiner, or we could easily swap out any of the reducers used.

In Conclusion

The transduce solution we explored serve us well for learning purposes, but should not be used in production code. As we saw, there is a lot happening under the hood with plenty of room for optimizations. I recommend using well tested existing solutions, like ramda's own R.transduce. For those curious to see a comparison of solutions, within the sensible-ramda repo, under concepts, there is a transducer file. Three strategies are provided with timers that report time taken. Two datasets are used for each, one small and one large. The first set uses the standard filter and map strategy. The second set uses the solution covered here. The third solution utilizes R.transduce, which does differ some from our handmade solution with the removal of our filter and map reducer creators alongside the addition of other ramda functions. Uncomment any solutions desired, and run them with yarn test:transducer. Through numerous trial runs, I found that the first solution took the longest, the second solution usually outperforms the first, and the third performs the best.

Problems

In life, I have plenty. Now I give some to you. You can use these sample practice problems, under sensible-ramda/exercises, to help solidify your understanding of ramda's functionality. I recommend taking time to come up with a solution, then reflect on what part(s) could be abstracted further, or better handled with ramda with the goal of shifting your implementation from "how" to solve it to "what" you want solved. My own solutions are listed below along with notes to be used as references.