Two Years of Functional Programming in JavaScript: Lessons Learned

74,515 reads

The article is not about learning FP principles or JavaScript FP libraries. There are numerous of good articles on this topic out there. The article is about adventures and consequences of switching to functional JS in one project.

When this story started, I was already a professional programmer with 10+ years of experience. C++, then C#, then Python. I was able to program anything. My confidence in patterns and principles I have obtained extended to a point where I saw no rationale to learn something new. “I know 90% of good parts in programming,” I thought.

Luckily, in May 2016 we started development of XOD project. XOD is a visual programming IDE for electronics hobbyists. To keep it casual we had to have a web-version of the IDE. Web? JavaScript! Full-blown IDE in JavaScript? Yep, we’ll end nowhere with quick and dirty jQuery; we need something better.

At the time, a new technology for heavy front-end development was emerging: something called React and its accompanying Flux/Redux patterns. In docs and articles, they were highly interlaced with the concepts of functional programming. I started to explore FP.

Whoa! It’s like I discovered another continent. Australia of development, where programmers walk upside down and data flows on the other side of the road. Of course, I have heard about Haskell, OCaml, LISP, but I used to think that such developers are a sort of marginal intelligentsia who program for the sake of programming, not to release products. My belief in own expertise level quickly eroded.

XOD is a product with functional and reactive programming principles in its genes. It was not apparent before the development have started. Many things I have “invented” or borrowed from other products are indeed FP basics. So, stars matched, we’re going to create an FRP programming environment with some heavy modern FRP JavaScript.

Anticipating the events, it worth the effort. FP gave the project a very solid and flexible framework. I don’t want to look back to the “classical” programming anymore and definitely, will develop all new projects with functional programming principles in the foreseeable future.

Breaking the barrier

You’ll find plenty of JavaScript functional programming libraries on NPM. One of most notable is Ramda. It’s a kind of “lodash” or “Underscore,” but with FP-first in mind. Ramda gives you a few dozens of functions to process your data and compose functions.

Functions alone are good, but you’ll need some FP objects to work with. Another library Ramda Fantasy will give them to you. You might also note other trending FP libs like Sanctuary, Fluture, Daggy. Check them out when you start to get the idea. Begin with Ramda alone, though, to keep your brain in-place.

Here’s the first barrier you stumble upon. If you look at the docs of any FP library, you’ll end up with many WTF questions in the best case. The wild argument order, foreign terminology, unclear practical value of some functions will incline you to stop trying and switch back to the customary programming. So…

Point #1. Start learning FP with articles not tied to a particular language or libraries. You need to overview the basic concepts first, understand the benefits, evaluate how your existing code could be transformed to live in the new world.

Many articles about functional programming are written by nerdy mathematician assholes. Reading them without preliminary training is dangerous: categories and morphisms can blow your mind in exchange for nothing.

Fortunately, there are excellent publications to start with. The most influential readings for me were:

Pointless madness

One of the first unusual concepts you learn when starting to explore FP is tacit programming also known as point-free style or (ironically) pointless coding.

The basic idea is omitting function argument names or, to be more precise, omitting arguments at all:

export const snapNodeSizeToSlots = R.compose(

nodeSizeInSlotsToPixels,

pointToSize,

nodePositionInPixelsToSlots,

offsetPoint({ x: WIDTH * 0.75, y: HEIGHT * 1.1 }),

sizeToPoint

);

That’s a typical function definition which is entirely made with a composition of other functions. It has no input arguments declared although a call will require them. Even without a context, you can understand the function acts as some conveyor belt taking a size and producing some pixel coordinates. To learn concrete details, you dig into functions comprising the composition. They, in turn, might be a composition of other functions, and so on.

That’s a very powerful technique until you lift it to the point of absurd. When we started using FP tricks aggressively, we took the problem of converting everything to point-free as a puzzle we have to solve again and again:

// Instead of

const format = (actual, expected) => {

const variants = expected.join(‘, ‘);

return `Value ${actual} is not expected here. Possible variants are: ${variants}`;

}

// you write

const format = R.converge(

R.unapply(R.join(‘ ‘)),

[

R.always(“Value”),

R.nthArg(0),

R.always(“is not expected here. Possible variants are:”),

R.compose(R.join(‘, ‘), R.nthArg(1))

]

);

Argh, what? You’re a cool guy, you’ve solved it. Share the puzzle with others on the code review.

Next, you learn monads and purity. OK, my functions can’t have any side effects from now on. They can’t refer this (that’s fine), they can’t refer time and random (o-o-ok), they can’t refer anything other than the arguments they are given, even the global string constants, even the math Pi. You carry the necessary args, factories, and generators from the outermost function through the nesting chain down to the internals, you explode the signatures, and then you learn the Reader or State monad. Ouch, you infect all your code with sporadic monadic maps and chains, and the bowl of spaghetti is ready!

So, combinators! What the funny beasts. Oh, Y-combinator is not only a startup accelerator but a recursion replacement. Let’s use it the next time I came with a problem trivially solvable by recursion or a simple `reduce` call.

Point #2. Functional programming is not about lambda calculus, monads, morphisms, and combinators. It’s about having many small well-defined composable functions without mutations of global state, their arguments, and IO.

In other words, if point-free style helps to communicate better in a particular case, use it. Otherwise, don’t. Don’t use monads because you can, use them when they precisely solve a problem. BTW, do you know that an Array and Promise are monads? If not, it does not stop you from applying them correctly. You should train your intuition to an extent when you understand what monad is required or, better, it is not required at all. It comes with practice, don’t overuse new stuff until you reason about it comfortably.

Alone, switching to small composable functions without side-effects where possible will give you most of the benefits. Start with it.

Either throw an exception or, maybe, return null

One aspect of switching to FP style used to annoy me a lot. In classical JS you have at least two options to show an error:

Return null/undefined instead of a result

Throw an exception

When you pick up FP, you still have these options and as a bonus get Either and Maybe monads. How should I handle errors now? What should the public API of my lib look like?

From one point of view, Maybe / Either is a more “proper” way, but they might be unfamiliar for library consumers. Nulls and exceptions are customary, but you always end up with ` undefined is not a function ` in the console. Long story short…

Point #3. Don’t be afraid of error handling through Maybe s and Either s. This couple is your best acquisition in the monadic world.

Take a look at the excellent Railway oriented programming pattern for the aha-moment. Use Maybes in your public API and if you afraid you won’t be understood, provide thin-wrapper satellites with suffixes like `Unsafe`, `Nullable`, `Exc` for consumption by the imperative JS

Clarity is a drug

When you collaborate in a project developed with functional programming principles, you notice the consequence pretty quickly. Doing a review now requires a much lower cognitive load. If you look at the function, the function code is all you should think about. You no longer have to imagine what consequences of mutating this field for that components will be. You don’t think whether a shallow copy, deep copy, or just a reference is more appropriate here. You just don’t have to think broader than the ten lines of code you’re looking at right now.

Then, when you see an old-fashion code, it always looks suspicious. “Hmmm… why it changes a field in my object? Why it stores it in the field, will it mutate the my object without a permission at a random moment?” The classical code starts looking just wrong.

Point #4. You’ll have to choose FP-compatible libraries and FP-compatible colleagues. The later is especially important. If the friction area is large and one part of the team strives for FP, and another part freely ruins the principles, finally FP will be defeated in the project.

Hiring FP JS developers is harder because it sets a high minimum level. But once you find one, chances you got the best professional possible for your product are high. In XOD we’re all FP adepts, and I’m happy we work together.

Benefits come with victims

Functional programming is so much different than the mainstream that the mainstream-targeted tools you’re using will stop to work.

Flow and Typescript fail to work correctly because it’s hard for them to express all that currying and argument polymorphism. Although there’re bindings for Ramda, for example, they often give you false alarms, and when there’s indeed an error, the message is very cryptic and unclear.

You could find some libraries that perform type checks at runtime. We use such one. Alas, they don’t scale well. The performance penalty is often higher than the cost of function execution per se. So you can afford the checking only by an explicit enabling it, e.g., for unit tests.

If you make a mistake in a deep composition, for example, mess input and output types a bit, you will cry when you see the stack trace.

Error: Can’t find prototype Patch of Node with Id “HJbQvOPL-” from Patch “@/main”

at /home/nailxx/devel/xod/packages/xod-func-tools/dist/monads.js:88:9

at /home/nailxx/devel/xod/node_modules/sanctuary-def/index.js:2491:23

at /home/nailxx/devel/xod/node_modules/sanctuary-def/index.js:860:20

at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_pipe.js:3:14

at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_arity.js:7:53

at src/project.js:887:5

at /home/nailxx/devel/xod/node_modules/sanctuary-def/index.js:2491:23

at /home/nailxx/devel/xod/node_modules/sanctuary-def/index.js:860:20

at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_pipe.js:3:14

at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_pipe.js:3:27

at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_arity.js:5:45

at _filter (/home/nailxx/devel/xod/node_modules/ramda/src/internal/_filter.js:7:9)

at /home/nailxx/devel/xod/node_modules/ramda/src/filter.js:47:7

at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_dispatchable.js:39:15

at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_curry2.js:20:46

at f1 (/home/nailxx/devel/xod/node_modules/ramda/src/internal/_curry1.js:17:17)

at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_pipe.js:3:14

at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_arity.js:5:45

at src/typeDeduction.js:171:37

at /home/nailxx/devel/xod/node_modules/sanctuary-def/index.js:2491:23

at /home/nailxx/devel/xod/node_modules/sanctuary-def/index.js:864:20

at src/project.js:618:33

at _Right.chain (/home/nailxx/devel/xod/node_modules/ramda-fantasy/src/Either.js:67:10)

at src/project.js:617:8

at /home/nailxx/devel/xod/node_modules/sanctuary-def/index.js:2491:23

at /home/nailxx/devel/xod/node_modules/sanctuary-def/index.js:860:20

at _map (/home/nailxx/devel/xod/node_modules/ramda/src/internal/_map.js:6:19)

at map (/home/nailxx/devel/xod/node_modules/ramda/src/map.js:57:14)

at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_dispatchable.js:39:15

at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_curry2.js:20:46

at f1 (/home/nailxx/devel/xod/node_modules/ramda/src/internal/_curry1.js:17:17)

at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_pipe.js:3:14

at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_pipe.js:3:27

at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_pipe.js:3:27

at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_arity.js:5:45

at validateProject (src/project.js:1031:3)

at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_pipe.js:3:27

at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_pipe.js:3:27

at /home/nailxx/devel/xod/node_modules/ramda/src/internal/_arity.js:5:45

at src/flatten.js:1021:5

at /home/nailxx/devel/xod/node_modules/sanctuary-def/index.js:2491:23

at /home/nailxx/devel/xod/node_modules/sanctuary-def/index.js:864:20

at Context.<anonymous> (test/flatten.spec.js:1805:27)

The most of the trace is pointless when it comes to finding the problem source. Luckily, once FP code runs successfully for the first time, you can be sure it is rock-solid and will bring you no surprises in future. The obvious consequence is a requirement for a thorough unit test suite if you’re doing FP in JS.

Code coverage and breakpoints also break. FP code is more like CSS than JS. Take a look at XOD sources. Does it make much sense to place a breakpoint to CSS and execute it step-by-step? What’s the coverage of CSS file? Of course, the effect is not 100%. At the places where you switch back from declarative to imperative style, these tools still work; but now your code is fragmented for the devtools and the experience change wildly.

Point #5. Once you touch FP you will be unhappy and angry. I had experienced the same emotion as when I switched from Windows to Linux and understood that both suck and I have no way to undo the knowledge. The same with a switch from full-blown IDE to Vim. Hope, you understand the idea.

Can we take the best of both worlds? Get the functional programming without madness and excellent developer experience at the same time? I think so. There’re other JS-targeted languages that are functional from the very beginning: Elm, PureScript, OCaml (BuckleScript), ReasonML.

I’ve tried ReasonML in practice recently, but that’s another story. If you’d like to hear, clap a few times ;)

Tags