Victor Nakoryakov
18 Apr 2021
âą
10 min read
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.
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:
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.
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:
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 Eithers. 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
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.
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, rocket boost a few times ;)
Victor Nakoryakov
Founder of @amperka && @xodio. Team lead, and developer.
See other articles by Victor
Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ
108 E 16th Street, New York, NY 10003
Join over 111,000 others and get access to exclusive content, job opportunities and more!