Introduction
Tacit Programming, also called point-free style, is a way to write functions without specifying the arguments. While functional programming languages have more abilities to leverage this style, there are still two key things you can use point-free style to help with in JavaScript, Python, and Lua that I wanted to cover today. Specifically reducing the amount of arguments for functions, and aiding in composition by writing less code.
The later isn't as powerful in JavaScript as in one where you can define your own operators, such as PureScript or Haskell, but that's ok, it's still helpful. Less code === less to remember and keep track of.
Part 2 has more examples.
Example of Point Free Styles
Passing Values
As Dr. Boolean explains in Chapter 2 of his book, functions in JavaScript, and other languages, are also treated as values. You can set them to variables, and pass them around like variables. In currying, you return functions from functions all day, everyday. As regular values.
However, it can be easy to forget this value concept when passing functions as values vs. defining them, especially when dealing with Promises in JavaScript.
getPersonFromServer()
.then(person => getStreet(person))
Instead, just give it the function:
getPersonFromServer()
.then(getStreet)
They both do the same thing, the first one just uses an extra, un-needed function, often for readability's sake. Point free refers to the defining of functions, not using them like the above, but I found that the Promise example resonates a lot with JavaScript developers.
With Currying
The following function attempts to read the street address from an Object using Lodash/fp's get function.
const getStreet = person => get('address.street', person)
So does this function:
const getStreet = get('address.street')
The functions "do the same thing" and "produce the same result," but the first defines its argument, and the second does not. The first requires an extra wrapper function, and the second does not. The second is written in the point-free style.
Note if you don't understand curried functions, partial applications, or pure functions, the above will be harder to grok. Read and understand those first. Or keep going, I love your attitude either way.
All in on Curry
You decide to go all in on curried functions using a library like Lodash, Ramda, or Sanctuary, or making your own. You like how your unit tests only use stubs and have no mocks. You are digging the purity. You like how they are more flexible to compose with others.
... but you start noticing a problem. Or perhaps it's just a new requirement of you creating pure functions that must declare their dependencies in their arguments?
Your functions... they have A LOT of arguments.
Sure, your low-level private functions like the predicates, validators, or simple parsing ones are fine:
/* predicate with 1 parameter */
const firstNameIsLegitString = firstName => firstName.length > 0 && isString(firstName)
But the ones higher up the module chain, the ones that form the basis of the library or API you're building. They either need a lot of parameters to get going, or they require copious partial applications of their own, or both.
/* login database API with 4 parameters */
const login = curry((bycryptModule, dbClient, username, password) => ...
I have one at work that uses 15.
This is the cost of pure functions. The more different types of functions you compose together, the more arguments they require until it eventually becomes quite exponential. Stupid math being all exponential.
WAT DO!?
Point Free to the Rescue
Let's take the login function above and fix him using point free style. First, let's show you a bit about how his module is defined.
import curry from 'lodash/fp/curry'
export const login = curry((bycryptModule, dbClient, username, password) => ...)
It exports just the login function for unit testing and "public access" purposes. Notice it doesn't even import the bycrypt module, nor the Postgres for Node. It assumes, generally correctly, that whoever up the call stack is responsible for supplying the concrete implementations.
However, let's be pragmatic here about two things:
You're making it harder on those "up the call" stack. Let's not. We wrote the code, we know those two dependencies are NEVER GOING TO CHANGE EXCEPT IN UNIT TESTS.
Only we're testing our code by exposing those methods publicly. Let's expose a concrete (a function that actually supplies the REAL Bycrypt and the REAL Postgres for Node) as well.
import curry from 'lodash/fp/curry'
const bcrypt = require('bcrypt')
const { Pool } = require('pg')
export const login = curry((bycryptModule, dbClient, username, password) => ...)
export const loginConcrete = login(bcrypt, new Pool())
Voilà ! We now expose the loginConcrete for consumers to utilize that only requires two parameters, and the only two parameters they care about anyway. Since you manually unit tested the private function login, code coverage via istanbul considers the public function loginConcrete tested as well.
import { loginConcrete } from './login'
loginConcrete('user', 'pass').then(...)
Conclusions
Tacit programming, also called point-free style, is when you write functions without parameters. Those parameters can be referred to the "point" or "points" your function is operating on. This can sometimes reduce the size of your code, decrease the amount of extra, and unneeded functions, and help alleviate API level public functions from having too many parameters by supplying the commonly known concrete implementations.
It can also make the code unreadable, unintentionally hide intent, or result in MORE functions than needed as seen in Part 2. Use with abandon in list comprehensions like map, filter, andreduce as well as functions used in your Promise chains, but be careful everywhere else, especially if you're working with a team that has no familiarity, nor care about function currying.
0 Comments