Functional programming is, funnily enough, all about functions. As such, it's good to refine how we write them. This post is all about the domains of our functions.
A function's domain is the set of all its possible argument values, for example:
exclaim :: String -> String exclaim x = x <> "!"
const exclaim = (x: string): string => x + '!';
The domain of this function is a set of all possible strings. In other words, the input
x could be any possible string; there are no constraints on this argument except the string type.
Can you spot the problem with this function?
head :: [a] -> a head (x:_) = x
const head = <A>(xs: A): A => xs;
It's partial, meaning its implementation is not defined for all possible inputs, specifically in the case of an empty list.
One way in which we can address this is to return
Option, thus making our function total i.e. non-partial:
head :: [a] -> Maybe a head (x:_) = Just x head _ = Nothing
const head = <A>(xs: A): Option<A> => O.fromNullable(xs);
Really, totality means only ensuring that you've handled all possible inputs such that no input could cause the function to throw or otherwise unexpectedly fail.
Note that property-based testing would be a good way to catch edge-case bugs such as that in our first, naive implementation, and could be used to ensure that this implementation isn't somehow flawed for some input we haven't considered.
There is an alternative technique we can employ however, one that's often preferable. That is to limit our function's domain itself through the use of more restrictive types:
head :: NonEmpty a -> a head (x:|_) = x
const head = <A>(xs: NonEmptyArray<A>): A => xs;
By constraining our function's domain we've been able to define a safe head function that's maximally ergonomic. If someone doesn't already have a decidedly non-empty list, they can maybe make one and be no worse off than before. On the other hand, if the consumer's list is definitely non-empty, then they no longer have to deal with a false notion of nullability. Additionally, it's simplified our function implementation.
If you ever catch yourself widening your output type to satisfy a bad input, that's a sign that you might need to constrain your domain. A very common mistake from beginners to the
Maybe type is to discover that they need to manipulate it in their business logic path, and write a function of type
Maybe a -> Maybe b. Never do this! This is what functors are for.
To round up this post, a quick mention that just as
a -> b is the domain,
b is the codomain.
This post can also be found on my personal blog: