Contravariant functors as invariant functors by Mark Seemann
Another 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 contravariant functors are also invariant functors.
Is this useful? Let me, like in the previous article, 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 contravariant 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' contravariant functors Invariant
instances, including Predicate
, Comparison
, and Equivalence
. Here's an example of using invmap
with a predicate.
First, we need a predicate. Consider a function that evaluates whether a number is divisible by three:
isDivisbleBy3 :: Integral a => a -> Bool isDivisbleBy3 = (0 ==) . (`mod` 3)
While this is already conceptually a contravariant functor, in order to make it an Invariant
instance, we have to enclose it in the Predicate
wrapper:
ghci> :t Predicate isDivisbleBy3 Predicate isDivisbleBy3 :: Integral a => Predicate a
This is a predicate of some kind of integer. What if we wanted to know if a given duration represented a number of picoseconds divisible by three? Silly example, I know, but in order to demonstrate invariant mapping, we need types that are isomorphic, and NominalDiffTime is isomorphic to a number of picoseconds via its Enum
instance.
p :: Enum a => Predicate a p = invmap toEnum fromEnum $ Predicate isDivisbleBy3
In other words, it's possible to map the Integral
predicate to an Enum
predicate, and since NominalDiffTime
is an Enum
instance, you can now evaluate various durations:
ghci> (getPredicate p) $ secondsToNominalDiffTime 60 True ghci> (getPredicate p) $ secondsToNominalDiffTime 61 False
This is, as I've already announced, hardly useful, but it's still possible. Unless you have an API that requires an Invariant
instance, it's also redundant, because you could just have used contramap
with the predicate:
ghci> (getPredicate $ contramap fromEnum $ Predicate isDivisbleBy3) $ secondsToNominalDiffTime 60 True ghci> (getPredicate $ contramap fromEnum $ Predicate isDivisbleBy3) $ secondsToNominalDiffTime 61 False
When mapping a contravariant functor, only the contravariant mapping argument is required. The Invariant
instances for Contravariant
simply ignores the covariant mapping argument.
Specification as an invariant functor in C# #
My earlier article The Specification contravariant functor takes a more object-oriented view on predicates by examining the Specification pattern.
As outlined in the introduction, while it's possible to add a method called InvMap
, it'd be more idiomatic to add a non-standard Select
method:
public static ISpecification<T1> Select<T, T1>( this ISpecification<T> source, Func<T, T1> tToT1, Func<T1, T> t1ToT) { return source.ContraMap(t1ToT); }
This implementation ignores tToT1
and delegates to the existing ContraMap
method.
Here's a unit test that demonstrates an example equivalent to the above Haskell example:
[Theory] [InlineData(60, true)] [InlineData(61, false)] public void InvariantMappingExample(long seconds, bool expected) { ISpecification<long> spec = new IsDivisibleBy3Specification(); ISpecification<TimeSpan> mappedSpec = spec.Select(ticks => new TimeSpan(ticks), ts => ts.Ticks); Assert.Equal( expected, mappedSpec.IsSatisfiedBy(TimeSpan.FromSeconds(seconds))); }
Again, while this is hardly useful, it's possible.
Conclusion #
All contravariant functors are invariant functors. You simply use the 'normal' contravariant mapping function (contramap
in Haskell). This enables you to add an invariant mapping (invmap
) that only uses the contravariant argument (b -> a
) and ignores the covariant argument (a -> b
).
Invariant functors are, however, not particularly useful, so neither is this result. Still, it's there, so deserves a mention. Enough of that, though.
Next: Monads.