Functors as invariant functors by Mark Seemann
A most likely useless set of invariant functors that nonetheless exist.
This article is part of a series of articles about invariant functors. An invariant functor is a functor that is neither covariant nor contravariant. See the series introduction for more details.
It turns out that all functors are also invariant functors.
Is this useful? Let me be honest and say that if it is, I'm not aware of it. Thus, if you're interested in practical applications, you can stop reading here. This article contains nothing of practical use - as far as I can tell.
Because it's there #
Why describe something of no practical use?
Why do some people climb Mount Everest? Because it's there, or for other irrational reasons. Which is fine. I've no personal goals that involve climbing mountains, but I happily engage in other irrational and subjective activities.
One of them, apparently, is to write articles of software constructs of no practical use, because it's there.
All functors are also invariant functors, even if that's of no practical use. That's just the way it is. This article explains how, and shows a few (useless) examples.
I'll start with a few Haskell examples and then move on to showing the equivalent examples in C#. If you're unfamiliar with Haskell, you can skip that section.
Haskell package #
For Haskell you can find an existing definition and implementations in the invariant package. It already makes most common functors Invariant
instances, including []
(list), Maybe
, and Either
. Here's an example of using invmap
with a small list:
ghci> invmap secondsToNominalDiffTime nominalDiffTimeToSeconds [0.1, 60] [0.1s,60s]
Here I'm using the time package to convert fixed-point decimals into NominalDiffTime
values.
How is this different from normal functor mapping with fmap
? In observable behaviour, it's not:
ghci> fmap secondsToNominalDiffTime [0.1, 60] [0.1s,60s]
When invariantly mapping a functor, only the covariant mapping function a -> b
is used. Here, that's secondsToNominalDiffTime
. The contravariant mapping function b -> a
(nominalDiffTimeToSeconds
) is simply ignored.
While the invariant package already defines certain common functors as Invariant
instances, every Functor
instance can be converted to an Invariant
instance. There are two ways to do that: invmapFunctor
and WrappedFunctor
.
In order to demonstrate, we need a custom Functor
instance. This one should do:
data Pair a = Pair (a, a) deriving (Eq, Show, Functor)
If you just want to perform an ad-hoc invariant mapping, you can use invmapFunctor
:
ghci> invmapFunctor secondsToNominalDiffTime nominalDiffTimeToSeconds $ Pair (0.1, 60) Pair (0.1s,60s)
I can't think of any reason to do this, but it's possible.
WrappedFunctor
is perhaps marginally more relevant. If you run into a function that takes an Invariant
argument, you can convert any Functor
to an Invariant
instance by wrapping it in WrappedFunctor
:
ghci> invmap secondsToNominalDiffTime nominalDiffTimeToSeconds $ WrapFunctor $ Pair (0.1, 60) WrapFunctor {unwrapFunctor = Pair (0.1s,60s)}
A realistic, useful example still escapes me, but there it is.
Pair as an invariant functor in C# #
What would the above Haskell example look like in C#? First, we're going to need a Pair
data structure:
public sealed class Pair<T> { public Pair(T x, T y) { X = x; Y = y; } public T X { get; } public T Y { get; } // More members follow...
Making Pair<T>
a functor is so easy that Haskell can do it automatically with the DeriveFunctor
extension. In C# you must explicitly write the function:
public Pair<T1> Select<T1>(Func<T, T1> selector) { return new Pair<T1>(selector(X), selector(Y)); }
An example equivalent to the above fmap
example might be this, here expressed as a unit test:
[Fact] public void FunctorExample() { Pair<long> sut = new Pair<long>( TimeSpan.TicksPerSecond / 10, TimeSpan.TicksPerSecond * 60); Pair<TimeSpan> actual = sut.Select(ticks => new TimeSpan(ticks)); Assert.Equal( new Pair<TimeSpan>( TimeSpan.FromSeconds(.1), TimeSpan.FromSeconds(60)), actual); }
You can trivially make Pair<T>
an invariant functor by giving it a function equivalent to invmap
. As I outlined in the introduction it's possible to add an InvMap
method to the class, but it might be more idiomatic to instead add a Select
overload:
public Pair<T1> Select<T1>(Func<T, T1> tToT1, Func<T1, T> t1ToT) { return Select(tToT1); }
Notice that this overload simply ignores the t1ToT
argument and delegates to the normal Select
overload. That's consistent with the Haskell package. This unit test shows an examples:
[Fact] public void InvariantFunctorExample() { Pair<long> sut = new Pair<long>( TimeSpan.TicksPerSecond / 10, TimeSpan.TicksPerSecond * 60); Pair<TimeSpan> actual = sut.Select(ticks => new TimeSpan(ticks), ts => ts.Ticks); Assert.Equal( new Pair<TimeSpan>( TimeSpan.FromSeconds(.1), TimeSpan.FromSeconds(60)), actual); }
I can't think of a reason to do this in C#. In Haskell, at least, you have enough power of abstraction to describe something as simply an Invariant
functor, and then let client code decide whether to use Maybe
, []
, Endo
, or a custom type like Pair
. You can't do that in C#, so the abstraction is even less useful here.
Conclusion #
All functors are invariant functors. You simply use the normal functor mapping function (fmap
in Haskell, map
in many other languages, Select
in C#). This enables you to add an invariant mapping (invmap
) that only uses the covariant argument (a -> b
) and ignores the contravariant argument (b -> a
).
Invariant functors are, however, not particularly useful, so neither is this result. Still, it's there, so deserves a mention. The situation is similar for the next article.