Introduction
In our last post, we talked about what Tacit Programming is, how it can help reduce argument count of public API functions using known concrete implementations, and how it can help shrink code size & function count for Array comprehensions and Promise chaining.
In this post, we’ll show some helpful ways to use tacit programming in data validation & composing functions together synchronously as well as an example of taking things way too far.
Data Validation
If you’re validating data that is large Objects, you’ll probably want to check a lot of properties. Let’s create some predicates to check those properties of a Person Object that should have a name and address.
const person = {
name: 'Jesse Warden',
address: {
street: '123 Cow Ville',
phone: [
'123-555-1234',
'999-555-8234'
]
}
}
And the predicates:
const { has, every } = require('lodash/fp')
const legitPerson = person => every(
hasResult => hasResult === true,
[
has('name', person),
has('address.street', person),
has('address.phone[0]', person)
]
)
/* use like legitPerson({name: 'sup'}) which is false, no address */
… however, it’s a bit verbose. Let’ use point free style to whittle it down:
const legitPerson = person => every(
predicate => predicate(person),
[
has('name'),
has('address.street'),
has('address.phone[0]')
]
)
Instead of looping through an Array of [true, true, true]
, we instead loop through an Array of functions, can call ’em on our data. Same result, less code.
Smaller Composing
There are 2 places in JavaScript, less so in Lua/Python, where you can use the point free style for less code, and thus less stuff to remember & maintain.
Synchronous Composing
The most common used, and demo’d, is data parsing. Let’s parse this string below from a JSON string, convert to person Objects, filter out the humans, and clean up their names.
const peopleFields = `[
["jesse warden", "123 Cow Ville", ["123-555-1234", "999-555-8234"], "human"],
["brandy fortune", "123 Cow Ville", ["123-867-5309"], "human"],
["albus dumbledog", "92 Dog Down", ["123-555-1234"], "dawg"]
]`
To parse that, we’ll use 4 functions, in order.
/* take in Arrays, convert to a Person Object */
const listToPeople = people =>
map(list =>
({
name: nth(0, list),
address: {
street: nth(1, list),
phone: nth(2, list)
},
type: nth(3, list)
}),
people
)
/* keep only humans in the Array */
const filterHumans = people =>
filter(
person => getOr('unknown', 'type', person) === 'human',
people
)
/* convert jesse warden to Jesse Warden */
const formatNames = people =>
map(
list => set('name', startCase(get('name', list)), list),
people
)
/* put 'em all together */
const parseHumans = jsonString =>
formatNames(
filterHumans(
listToPeople(
JSON.parse(jsonString)
)
)
)
When you pass peopleFields
to parseHumans
you get:
[ { name: 'Jesse Warden',
address: { street: '123 Cow Ville', phone: [Array] },
type: 'human' },
{ name: 'Brandy Fortune',
address: { street: '123 Cow Ville', phone: [Array] },
type: 'human' } ]
It works, but… wow. Even with pure functions and not a function block {}
to be seen, it’s still verbose. Let’s use point-free style to shrink it.
map and nth vs. operators
The listToPeople
uses map
which takes 2 parameters: a function that takes in an item of the Array, and whatever it returns will be put into a new Array at the some position, and the Array you want to convert.
const listToPeople = people =>
map(list =>
({
name: nth(0, list),
address: {
street: nth(1, list),
phone: nth(2, list)
},
type: nth(3, list)
}),
people
)
However, it’s curried by default in lodash/fp. That means there’s no need to copy people twice, we already provide the first parameter, flow will supply the 2nd later. Simply remove people, and no need to create a new Arrow function:
const listToPeople =
map(list =>
({
name: nth(0, list),
address: {
street: nth(1, list),
phone: nth(2, list)
},
type: nth(3, list)
})
)
We’ll cover the destructuring way after, but I want to show the function way first. If you’re willing to get a bit imperative for demo sake, we can shrink it even more. The nth
function in Lodash/fp is curried and takes 2 parameters: The index and the Array. It’s like list[0]
except you write nth(0, list)
. It, too, is curried by default. But… it’s the wrong way. The dynamic part is SUPPOSED to be to the right, but in our case, the dynamic part is on the right. Variety of solutions, let’s just do manual for now to get our ideas down:
const listToPeople =
map(list => {
const lst = index => nth(index, list)
// or, if you want point-free
// const lst = curry(flip(nth))
return {
name: lst(0),
address: {
street: lst(1),
phone: lst(2)
},
type: lst(3)
}
})
Sometimes, point-free isn’t needed because operators that the language has can do the job.
const listToPeople =
map(list =>
({
name: nth(0, list),
address: {
street: nth(1, list),
phone: nth(2, list)
},
type: nth(3, list)
})
)
We can simply replace nth
with the actual value:
const listToPeople =
map( ([name, address, street, phone, type]) =>
({
name,
address: {
street,
phone,
},
type
})
)
Yes, functions are powerful, but remember operators are as well. Why not always operators, then? Operators can throw
Errors, pure functions like nth
do not. Using point-free style assumes they are pure functions and no side-effects. However, I get it, it’s hard not to be seduced by their beauty.
filter and getOr
The filter
function’s 1st parameter is the function that returns true
for keeping the item in the array, false
for not. The 2nd parameter is the Array.
const filterHumans = people =>
filter(
person => getOr('unknown', 'type', person) === 'human',
people
)
Like map
, it’s curried, so we can remove the 2nd parameter and let “someone else” fill that in later. That someone else is when it’s used in flow
as you’ll see at the bottom. We can also remove the Array function, and just set the return value since it’s a partial application (a function with 1 argument already supplied, we’re just waiting for the 2nd argument to actually invoke the function):
const filterHumans =
filter(
person => getOr('unknown', 'type', person) === 'human'
)
The getOr
is like get
, except, if it detects undefined
or null
, it’ll use whatever default value we give it instead. It’s like a Maybe’s getOrElse
. While the getOr
inline is readable and small, this IS an article about tacit programming, so let’s refactor it as well:
const getTypeOrUnknown = getOr('unknown', 'type')
const filterHumans =
filter(
person => getTypeOrUnknown(person) === 'human'
)
Now getTypeOrUnknown
is just expecting the actual Object to check. We’re close… but I still see an Arrow function. Let’s FINISH HIM Mortal Kombat style:
const getTypeOrUnknown = getOr('unknown', 'type')
const equalsHuman = isEqual('human')
const isHuman = flow([getTypeOrUnknown, equalsHuman])
const filterHumans = filter(isHuman)
TACITALITY!!!11oneone
If you missed it:
– getTypeOrUnknown
takes advantage of getOr
requiring 3 parameters: default value, the path, and the value. We’re waiting on the person
value, so we just supply the default value and the path.
– equalsHuman
is a way to write equalsHuman = o => o === 'human'
without creating an Arrow function and staying point-free. More too it than that, but that’s the simple version.
– isHuman
is just isHuman = person => equalsHuman(getTypeOrUnknown(person))
, but point-free; no parameter, no arrow function.
– filterHumans
not just takes the function, and is waiting on the 2nd parameter; the Array of humans and non-humans to filter against.
Taking Point-Free Too Far
We’re not going to get into profuctor optics or lenses, but the core engine of them, get
and set
, we are are going to shrink them in formatNames
. Side note: If you’ve mastered get
and set
and want new lens-like toys, check out Ramda’s lens functions.
We’ve talked about get
in the past on this blog. It’s a safe way to do a dot access without worry that you’ll do dot access on undefined
and cause an error. Additionally, you can dot access a path, even using Arrays like get('some.deep.property[2].man')
without using try/catch, the if(thing && thing.dot && thing.dot.cow)
insanity.
The set
function is about the same, except it can set a value AND will return a brand new Object. It’s like Object.assign
or Object destructor copying like {...cow, name: 'new cow name'}
. It’s signature in lodash/fp is the path, value, and Object.
const formatNames = people =>
map(
list => set('name', startCase(get('name', list)), list),
people
)
Let’s do the easy one first, and remove the 2nd parameter of map
, and then the Arrow function:
const formatNames =
map(
list => set('name', startCase(get('name', list)), list)
)
Cool, now let’s untangle that get/set mess:
const getName = flow([get('name'), startCase])
const setName = set('name')
const formatNames =
map(
list => setName(getName(list), list)
)
Not bad, but I see list
3 times and an arrow function, ack! For this one, we’ll have to borrow 2 functions from Ramda and build up our compose chain. Let’s get cray:
const getName = get('name')
const setName = set('name')
const getFixedName = flow([getName, startCase])
const nameAndPerson = [{f: getFixedName}, {f: identity}]
const callF = invoker(1, 'f')
const flipMap = curry(rearg([1, 0], map))
const mapNameAndPerson = flipMap(nameAndPerson)
const getFixedNameAndPerson = flow([callF, mapNameAndPerson])
const setNameWithPersonNameAndPerson = apply(setName)
const mapFormatNames = flow([getFixedNameAndPerson, setNameWithPersonNameAndPerson])
const formatNames = map(mapFormatNames)
Insanity. BUT, not a parameter, nor arrow function in sight.
While I’m proud I figured this out in 3 days, this is an example of taking point-free way too far. When people say point-free style results in smaller, more readable code, you can respond “not always”, heh. While all the functions are pure and point-free, it’s a ton of functions… a ton of code which originally fit in 1 unformatted line and is now 11. You just got Arrow functions in ES6; are they really that bad? It’s not exactly clear that each one isn’t supposed to be a standalone function, but rather a tool for the greater whole.
Feel free to skip to Data Validation. For you nerds who want the down low on what those functions do, read on.
– getName
: the get
function in lodash takes a path and an Object and is curried by default. If you just supply the path, you pass it an Object, and it’ll return to you that path. The get('name', {name: 'cow'})
will return cow. You can also write getName({name: 'cow'})
and it does the same thing.
– setName
: like get
, except it returns a deep copy of the Object with the property you set changed. Think Object.assign
or object destructuring copying, i.e. const newVersion = {...person, name: 'new name'}
. Calling const result = setName('new name', {name: 'cow'})
will end up having result equal {name: 'new name'}
and be a different Object than you one you passed in to ensure immutability.
– getFixedName
: This uses flow
to take the argument you send to function, send it first to getName
, then take whatever getName
spits out, and capitalize it. Passing in {name: 'jesse warden'}
will spit out the String ‘Jesse Warden’. Think of it as a choo-choo train of functions, or simple pipes connected together. Every function inside takes the input from the left, and whatever it spits out will go to the function to the right, on down the “flow train”.
– nameAndPerson
: The set
function needs 2 parameters. The problem is, set’s 2nd parameter needs to the result of getName
. We can’t use flow
because it’d spit out a String, and we need the original person
Object. The easiest way to handle multiple things is to put ’em into an Array since Array’s are bread and butter for array comprehension libraries like Lodash/Ramda, etc. However, things like invokeMap
don’t allow parameters without creating your own. An easier way is to use invoker from Ramda, and then throw that function in a map
. That way we can “call a bunch of functions in an Array, and get their results”. Once you have those results, you can feed them later to the set
function via Ramda’s apply which works like Function.prototype.apply; call a function with an Array of parameters. The identity
function, gives you back what you gave it. It is there so when we give it a person
, it’ll return the person
so our arguments Array we end up with has the person ready to go for the 2nd parameter to set
.
– callF
: Using Ramda’s invoker
, we can create a function that will “call a function on an Object”. Like a get
, but instead of getting the value, it gets the value that is assumed to be a function, calls it, then gives you the result. For invoker
, you supply the path, and parameter(s), and you get a function back ready to go. Up in the nameAndPerson
Array, we created Objects with an “f” property that has the function we want to call. The callF
will get that Object, find the “f”‘s value, then call that function with whatever parameter we pass. Think of it like const callF = (arg, person) => person.f(arg)
, except curried.
– flipMap
: The lodash map
function takes a function and an Array. However, we know the set function we want to run, but we don’t know the Array, or results of running getName
and identity
, so we have to reverse it. Now, you could use flip(map)
, but that breaks currying. Meaning, if you call flipMap(array, func)
, you’re fine, but if you call flipMap(array)(func)
, say putting it as a partial application into flow
for example, boom. So, we have to do surgery manually using rearg
. Same thing, different function, and we curry
it again. I supposed we could just curry(flip(map))
, but after losing 3 hours to flip
, 3 stars, would not flip in a curry again. Remember kids, blame the library and its author(s), not your own inexperience/inability.
– mapNameAndPerson
: We know the 2 functions we want to run, we’re just waiting on the function that’ll have the person
Object we need for map. So… just create a partial application for now.
– getFixedNameAndPerson
: We compose the callF
and mapNameAndPerson
together. First, take the person
Object you pass, and load it into the callF
. When he’s run through the mapNameAndPerson
function, that callF
will have the person ready to go. He’ll invoke and pass it to getName
first, and identity
will just send back the person
, resulting in ['Jesse Warden', person]
. Dope, the 2 arguments we need for set
!
– setNameWithPersonNameAndPerson
: We take our Array of 2 ready to go values above, and “spread ’em like buttah” using function.apply to setName
. The set
is path, value, and Object. setName
already has the path, “name”. Our Array has value and Object. The apply
function will call that function, and give it those 2 arguments, in order.
– mapFormatNames
: We wire the 2 pipes together. Passing a person
Object to this function will first “fix it” via getFixedNameAndPerson
, and then call the setNameWithPersonNameAndPerson
with those 2 values, and we’ll get new person
Object out with the correctly capitalized name.
– formatNames
: The mapFormatNames
function only works with 1 person. We pre-populate a map
function so it’ll work with an Array of person
Objects.
Composing
The last function that wires the whole shebang together, parseHumans
, is a nest of functions. While nicely formatted, it’s just a prettier nested if statement. The real problem, though, is it doesn’t really read from top to bottom like a Promise
chain does. It’s actually inside out.
const parseHumans = jsonString =>
formatNames(
filterHumans(
listToPeople(
JSON.parse(jsonString)
)
)
)
Using flow, we can say “run these functions in order”. Pass the value to the first, and whatever that function spits out, pass the the right function… and on down the line. Like the old game telephone, but even more inappropriate.
const parseHumans = jsonString => flow([
JSON.parse,
listToPeople,
filterHumans,
formatNames
])(jsonString)
A lot more clear and smaller. Pass this jsonString
to the JSON.parse
, and the Object he parses out, give to the listToPeople
, and on down the chain. One problem, it’s not point-free because of the jsonString
. Let’s fix.
const parseHumans = flow([
JSON.parse,
listToPeople,
filterHumans,
formatNames
])
And we’re done! Not an Arrow function, nor parameter in 47 lines of code.
Imports
BTW, if you’re curious about the functions used in the above for the imports, check it:
const { map, nth, filter, getOr, set, startCase, get, flow, isEqual, rearg, identity, curry } = require('lodash/fp')
const R = require('ramda')
const { invoker, apply } = R
Conclusions
As you can see, for data parsing where you need to operate many times on the same data, point-free style can help reduce how much code you have to write. For data parsing, you can see how even a little bit can go a long way in what ends up often being a lot of some of the most important code you write. Remember, if you even you use a typed language, and have runtime type enforcement, one wrong data parsing, and boom, errors. Creating fixtures (fake, known data) to unit test your parsing code can be painful to create and maintain as the data/API changes. Anything you can do to make it into small, re-usable pieces that are easier to test in isolation can go a long way.
You’ve seen how using various operators provided in the language can reduce code size as well, but unlike pure functions, they can also blow up (throw exceptions).
Finally, the formatNames
has shown you how you can take things too far. Sometimes writing simple 1 line arrow functions vs 11 point-free functions is a lot easier to read, follow, and maintain. Lua, Python, and JavaScript in ES6 can be beautiful in a non-point-free style. Only use where you’ve made things redundant.