There's a common notion that one sees in many programming languages of an "infectious function tag" -- some special behavior for a function that must extend to its callers as well.
- Rust functions can be
unsafe
, meaning they perform operations that can potentially violate memory unsafety. unsafe
functions can call normal functions, but any function that calls an unsafe
function must be unsafe
as well.
- Python functions can be
async
, meaning they return a promise rather than an actual value. async
functions can call normal functions, but invocation of an async
function (via await
) can only be done by another async
function.
- Haskell functions can be impure, meaning they return an
IO a
rather than an a
. Impure functions can call pure functions, but impure functions can only be called by other impure functions.
- Mathematical functions can be partial, meaning they don't map every value in their domain to an output. The definitions of partial functions can reference total functions, but if a total function maps some of its domain to a partial function, it becomes partial as well.
While there may be ways to invoke a tagged function from an untagged function, there is no general way, and doing so can often be dangerous and threatens to break the abstraction the language tries to provide.
The benefit, then, of having tags is that you can expose a set of special primitives that are given this tag and have any function that uses these primitives make that clear in its signature.
Say you're a language designer and you recognize this pattern, and you decide that you want to allow user-defined tags. Let's say the user defined a tag Err
, representing computations that may throw an error. A function using Err
might look like this:
function div <Err> (n: Int, d: Int): Int
if d == 0
throwError("division by 0")
else
return (n / d)
If we wanted to simplify things, we might observe that there's nothing erroneous about taking arguments - it's computing the return value where problems might arise. So we can restrict tags to functions that take no arguments, and have div
return a closure rather than the actual value:
function div(n: Int, d: Int): <Err> () -> Int
() =>
if d == 0
throwError("division by 0")
else
return (n / d)
In a lazy language such as Haskell, we don't need the closure, and can just return a lazy value directly:
div :: Int -> Int -> Err Int
div _ 0 = throwError "division by 0"
div n d = return $ n / d
It is now apparent that, in Haskell, tags need no special language support - they are ordinary type constructors. Let's make a typeclass for them!
class Tag m where
We want to be able to call an untagged function from a tagged function, which is equivalent to turning an untagged value (a
) into a tagged value (m a
).
addTag :: a -> m a
We also want to be able to take a tagged value (m a
) and apply a tagged function (a -> m b
) to get a tagged result (m b
):
embed :: m a -> (a -> m b) -> m b
This, of course, is precisely the definition of a monad! addTag
corresponds to return
, and embed
corresponds to (>>=)
.
It is now clear that "tagged functions" are merely a type of monad. As such, whenever you spot a place where a "function tag" could apply, chances are you've got a place suitable for a monad.
P.S. Regarding the tags I've mentioned in this answer: Haskell models impurity with the IO
monad and partiality with the Maybe
monad. Most languages implement async/promises fairly transparently, and there seems to be a Haskell package called promise that mimics this functionality. The Err
monad is equivalent to the Either String
monad. I'm not aware of any language that models memory unsafety monadically, it could be done.