DEV Community

Sam A. Horvath-Hunt
Sam A. Horvath-Hunt

Posted on • Updated on

Function domain

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.

What's the domain?

A function's domain is the set of all its possible argument values, for example:

exclaim :: String -> String
exclaim x = x <> "!"
Enter fullscreen mode Exit fullscreen mode
const exclaim = (x: string): string => x + '!';
Enter fullscreen mode Exit fullscreen mode

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.

Totality

Can you spot the problem with this function?

head :: [a] -> a
head (x:_) = x
Enter fullscreen mode Exit fullscreen mode
const head = <A>(xs: A[]): A => xs[0];
Enter fullscreen mode Exit fullscreen mode

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 Maybe/Option, thus making our function total i.e. non-partial:

head :: [a] -> Maybe a
head (x:_) = Just x
head _     = Nothing
Enter fullscreen mode Exit fullscreen mode
const head = <A>(xs: A[]): Option<A> => O.fromNullable(xs[0]);
Enter fullscreen mode Exit fullscreen mode

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.

Constrained inputs

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
Enter fullscreen mode Exit fullscreen mode
const head = <A>(xs: NonEmptyArray<A>): A => xs[0];
Enter fullscreen mode Exit fullscreen mode

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.

Codomain

To round up this post, a quick mention that just as a in a -> b is the domain, b is the codomain.


This post can also be found on my personal blog:
https://www.samhh.com/blog/function-domain

Top comments (1)

Collapse
 
iquardt profile image
Iven Marquardt • Edited

Only a small supplement: If we generalize such an ordinary constraint, i.e. make the value constructor (the NonEmpty portion of NonEmpty<A>) itself polymorphic, we obtain a type class constraint. Since TS doesn't support type constructors like T<A> we can use dictionary passing style (pass the type class constraint as a normal argument to the polymorphic function) or resort to HKT hacks as pursued by FP-TS.