How To Lose Functional Programming At Work
Info
Summary | Tips on how to unsuccessfully introduce and keep functional programming styles and principles in your workplace. |
---|---|
Shared | 2023-01-30 |
Revised | 2023-01-30T23:00:00Z |
Hi! If youâre looking to lose functional programming at work, here are a bunch of mistakes Iâve made on JS-heavy web teams over the years that can help you do the same.
Enjoy!
Note: if sarcasm and self-deprecation arenât your thing, you can skip to the real-talk takeaways.
PDF version
Here is the much prettier PDF version that is also useful for sending to your teammates or using in your own lunch-n-learn tech talk.
Thanks to KronisLV on the orange site for helping me fix an issue where the PDF was accidentally auto-downloading in Firefox.
How to Lose Functional Programming at Work - PDF
Donât have static type checking
- No TypeScript
- No Flow
- No ReasonML
- No Elm
- No (insert language with static type checking that compiles to JS)
const processData = composeP(syncWithBackend, cleansePII, validateData)
// * What arguments and their types are expected here?
//
// * If each function is written like this, how can
// one suss out what data are flowing where?
//
// * How hard is this going to be to debug?
// Use this everywhere: `(x) => (console.log(x), x)`
Oh, so point-free style programming is the problem? Not so fast:
async function processData(data) {
await validateData(data)
const cleansedData = cleansePII(data)
await syncWithBackend(cleansedData)
return data
}
// or for the Promise-chainersâŠ
const processData = data =>
validateData(data)
.then(cleansePII)
.then(syncWithBackend)
.then(() => data)
Do keep telling yourself that any of these 3, on their own, are easy for your teammates to work with after 3 months.
Donât use well-known code documentation tools
- No jsdoc
- âŠare there any other contenders?
Deprive your team of this clarity and helpful auto-completion:
// NOTE: this is an untested, small example
/**
* @typedef {Object} ReportingInfo
* @property {("light"|"dark")} userTheme - Current user's preferred theme
* @property {string} userName - Current user's name
* @property {UUID} postId - The current post's ID
*/
/**
* Validates that the reporting data (current user site prefences and post info)
* is OK, removes personally identifiable information, syncs this info with the
* backend, and gives us back the original data.
*
* @param {ReportingInfo} data - The current user's site preferences and post info
* @returns {Promise<ReportingInfo>} - The original reporting data
*/
const processData = data => // âŠ
Donât properly train new and existing colleagues
Truly believe, in your heart, that you can write a pile of blog posts, collect a bunch of other great learning resources, hand them all to a new FP learner, recommend they read as much as they can then come back with questions, and expect them to come out the other side at all.
Conversely, spend all your time and energy on a couple of individuals, neglect the others, fail to write any useful learnings down, and forget to encourage these initiates to turn around and help teach their other colleagues, in turn.
Donât bother getting the other engineering teams on board and rowing in the same direction
- âIf I build it, they will notice⊠right?â
- Idea: Lunch ân learn about FP? Me: Theyâll find out I donât know things.
- Idea: Meet with other team leaders and see if theyâre interested in adoption, what could be better, or share why theyâre not interested? Me: This includes managers who report on things, and if they think Iâm dumb or rocking the boat too much, I might look worse than I potentially already do.
Instead, if you keep it to yourself, other teams wonât get to contribute and probably improve the state of things.
Do live by the creed, âPoint-free or dieâ
Watch the video, âPoint-Free or Die: Tacit Programming in Haskell and Beyondâ, by Amar Shah
Contrived example:
import { __, any, lt } from 'ramda'
const anyLt0 = any(lt(0, __)) // hint: this has a bug in it
anyLt0([1, 2, 3]) // true â ughâŠ
// vs. the probably pretty simpleâŠ
const anyLt0 = numbers => numbers.some(n => n < 0)
anyLt0([0, 1, 2, 3]) // false
anyLt0([0, 1, 2, -1, 3]) // true â looks good
// đ should we resist eta-converting this?!
// âŠ
// NOT ON MY WATCH
const any = fn => array => array.some(fn)
const isLtN = x => n => x < n
const isLt0 = isLtN(0)
const anyLt0 = any(isLt0)
anyLt0([1, 2, 3]) // true â ugh; the bug is back
Real, but altered, example:
const finishItems = compose(
flip(merge)({ isDone: true, amtComplete: 100 }),
over(
lensProp('indexedObjects'),
mapVals(
compose(
over(lensProp('indexedObjects'), mapVals(assoc('isDone', true))),
assoc('isDone', true)
)
)
)
)
Do prefer the wrong abstraction over the right duplication
I was at Sandi Metzâ RailsConf 2014 Chicago talk, All the Little Things, where she blew my mind with the simplicity of âpreferring duplication over the wrong abstractionâ. Two years later, she followed it up with some great blog commentary, The Wrong Abstraction.
But in this case, dilute your core business logic to broad generalizations that can be extracted and abstracted over and over, fail to understand category theory enough for this to be useful, and be the only one who knows how these abstractions work.
Youâll know youâve lost people when normally thorough PR reviews now look like, âđâ.
Donât refactor old patterns that clearly donât work for the team
Make sure that people coming into the project have your old code patterns to emulate that you cringe looking at years later but never made the time to update.
While you could allocate investment time to this or reading up on how to improve your technical leadership skills, spend that time making new features, instead.
Do force functional patterns into a language that wasnât built for them (bonus: cryptic stack traces)
- Lean into recursive functions, realize ES2015 tail call optimization is only a thing in Safari nowadays (if that?), and sprinkle a
trampoline
function over the issue to make it go away and not blow out your call stack - Get cryptic error messages and stack traces because JS isnât going to be able to follow your custom
curry
andcompose
functions by default, meaning youâll have to go the extra mile like Brian does in Debugging functional to prevent the issues described by Thai in Partially-applied (or curried) functions could obfuscate the JavaScript stack trace (Thaiâs ultimate recommendations are âuse a typed language that guarantees that your functions will never receive an invalid dataâ or âjust donât go overboard with pointfree style JavaScriptâ). - The Glasgow Haskell Compiler can be optimized to fuse
map g . map f
into a singlemap
thanks to composition, knocking out the work in one go at runtime. While.map(âŠ).map(âŠ).map(âŠ)
seems to be optimized pretty ok in JS runtimes, youâre still asking it do N times the work, and you may not realize it. Oops. - BYO algebraic data type libraries (many of these are awesome, though)
Do opaquely compose and sequence the entirety of your API endpoints and make them hard to debug
On the surface, this isnât so difficult to readâŠ
// handler for POST /posts
import { createPost } from 'app/db/posts'
import { authenticateUser, authorizeUser } from 'app/lib/auth'
import { trackEvent } from 'app/lib/tracking'
const validateRequestSchema = payload => { /* ⊠*/ }
export const handleCreatePost = curry(metadata =>
pipeP(
authenticateUser(metadata),
authorizeUser(metadata),
validateRequestSchema,
createPost(metadata),
tapP(trackEvent('post:create', metadata)),
pick([ 'id', 'authorId', 'title' ])
)
)
Did you catch that this expects 2 arguments? Did you also know that authenticateUser
ignores the 2nd argument sent to it? How would you? And what about trackEvent
? Does it receive the payload
, or does createPost()
return post-related data?
Letâs write this another way:
export async function handleCreatePost(metadata, payload) {
await authenticateUser(metadata)
await authorizeUser(metadata, payload)
await validateRequestSchema(payload)
const post = await createPost(metadata, payload)
await trackEvent('post:create', metadata, payload)
return {
id: post.id,
authorId: post.authorId,
title: post.title,
}
}
Iâm not saying that option #2 is an awesome handler, but if you want to make it trickier for people, go with option #1.
Do recreate imperative, procedural programming while calling it âdeclarativeâ
const setBookReadPercentByType = (contentType, statusObject) =>
assoc(
'readPercent',
pipe(
prop('subItems'),
values,
filter(propEq(contentType, 'chapter')),
length,
flip(divide)(compose(length, keys, prop('subItems'))(statusObject)),
multiply(100),
Math.round
)(statusObject),
statusObject
)
Do have 8+-ish different patterns for function composition
// đ These 4, plus Promisified versions of them,
// plus combinations of them all used at once;
// doesn't include ramda's pipeWith and composeWith
// compose
const getHighScorers =
compose(
mapProp('name'),
takeN(3),
descBy('score')
)
// pipe
const getHighScorers =
pipe(
descBy('score'),
takeN(3),
mapProp('name')
)
// composeWithValue
const getHighScorers = players =>
composeWithValue(
mapProp('name'),
takeN(3),
descBy('score'),
players
)
// pipeWithValue
const getHighScorers = players =>
pipeWithValue(
players,
descBy('score'),
takeN(3),
mapProp('name')
)
// âŠbut then now mix and match them with actual,
// real-life business logic.
Do make yourself one of the few who can debug algebraic data types during midnight incidents
Ensure your team is surprised by all of the following words when debugging or altering your code in the pursuit of their own work tasks:
Task
,Maybe
,Either
,Result
,Pair
,State
bimap
chain
bichain
option
coalesce
fork
sequence
ap
map
â and I donât meanArray.prototype.map
, nor anew Map()
, nor a key/value object
Donât have SQL (a declarative language) do data transformations for you â DIWHY??? it yourself
Instead, and in the name of immutability, use data pipelines in your app to apply changes to your data, one transformation at a time, and accidentally do as many key/value iterations and memory allocations as possible. đŹ
Do suggest, on PRs, that colleagues completely refactor what theyâve done to fit your functional style
What you have here works great, but what could this look like if we flipped all the function arguments around, removed all these intermediate variables, and mapped these operations over an
Either
?
or
I noticed youâre explicitly constructing these objects in their functions. If you were to use <UTILITY-FUNCTION>, you could declare the shape of your outputted object and use functions as the values to look up or compute each value given some data.
And for some quick final onesâŠ
- Do sow imposter syndrome in others and exclude them by sharing non-beginner FP articles
- Do keep writing code using FP tools even when nobody else on the team is
- Do achieve peak perceived passive-aggression by getting tired and commenting PRs with emojis
- Do have âthe FP talkâ at work, and then publicly own your mistakes
Real-talk takeaways
Much of the backwards recommendations here can be, on the surface, written off as symptoms of inexperience, a lack of technical leadership from me, and obviously not the right paths.
But I think itâs something deeper than those easy explanations.
Most things in life need to be tended to in order for them to go the ways that weâd like them to; our relationships, our physical & mental health, our gardens. With most of these things in life, we strive to purposefully sculpt our futures.
However, there are many things that we accidentally sculpt. For example, if the fastest way from your back door to your garden is through your grassy yard, the simplest thing is to walk over the grass to get there. It makes sense for a while, but over time, your stepping on the grass carves a path that you never intended to create â it was an unintended consequence of your gardening.
This same thing happens with our minds and in our work. If weâre not paying attention to the big picture, the path of least resistance can carve canyons.
In my case, here, not taking responsibility of a path I helped create, coupled with persistent imposter syndrome and a feeling I needed to ship features and just look out for myself, instead of making time for re-evaluation, helped lead to the difficulties above for others and a loss of âhigherâ functional programming in a pretty good workplace that gives teams the freedom to choose their own tools.
But all is not lost! The core tenets of FP seem to remain:
- Immutability: Preferring to efficiently recreate objects over mutating the originals
- Purity: call the same function with the same arguments, and you get the same results
- Moving effects to the conceptual edge of an application
- Very few classes (if any;
React
doesnât count), no inheritance,map
/filter
/reduce
, etc.
It seems a happy balance has been collectively decided on, and Iâm excited to see where it goes. Perhaps, this time around, Iâll be better.
Thanks for reading,
Robert