Package a constant value, but make it look like a functor. An article for object-oriented programmers.

This article is an instalment in an article series about functors. In previous articles, you've learned about useful functors such as Maybe and Either. You've also seen at least one less-than useful functor: The Identity functor. In this article, you'll learn about another (practically) useless functor called Const. You can skip this article if you want.

Like Identity, the Const functor may not be that useful, but it nonetheless exists. You'll probably not need it for actual programming tasks, but knowing that it exists, like Identity, can be a useful as an analysis tool. It may help you quickly evaluate whether a particular data structure affords various compositions. For example, it may enable you to quickly identify whether, say, a constant type and a list may compose to a functor.

This article starts with C#, then proceeds over F# to finally discuss Haskell's built-in Const functor. You can just skip the languages you don't care about.

C# Const class #

While C# supports records, and you can implement Const as one, I here present it as a full-fledged class. For readers who may not be that familiar with modern C#, a normal class may be more recognizable.

public sealed class Const<T1T2>
{
    public T1 Value { get; }
 
    public Const(T1 value)
    {
        Value = value;
    }
 
    public Const<T1TResultSelect<TResult>(Func<T2TResultselector)
    {
        return new Const<T1TResult>(Value);
    }
 
    public override bool Equals(object obj)
    {
        return obj is Const<T1T2@const &&
               EqualityComparer<T1>.Default.Equals(Value, @const.Value);
    }
 
    public override int GetHashCode()
    {
        return -1584136870 + EqualityComparer<T1>.Default.GetHashCode(Value);
    }
}

The point of the Const functor is to make a constant value look like a functor; that is, a container that you can map from one type to another. The difference from the Identity functor is that Const doesn't allow you to map the constant. Rather, it cheats and pretends having a mappable type that, however, has no value associated with it; a phantom type.

In Const<T1T2>, the T2 type parameter is the 'pretend' type. While the class contains a T1 value, it contains no T2 value. The Select method, on the other hand, maps T2 to TResult. The operation is close to being a no-op, but still not quite. While it doesn't do anything particularly practical, it does change the type of the returned value.

Here's a simple example of using the Select method:

Const<stringdoublec = new Const<stringint>("foo").Select(i => Math.Sqrt(i));

The new c value also contains "foo". Only its type has changed.

If you find this peculiar, think of it as similar to mapping an empty list, or an empty Maybe value. In those cases, too, no values change; only the type changes. The difference between empty Maybe objects or empty lists, and the Const functor is that Const isn't empty. There is a value; it's just not the value being mapped.

Functor laws #

Although the Const functor doesn't really do anything, it still obeys the functor laws. To illustrate it (but not to prove it), here's an FsCheck property that exercises the first functor law:

[Property(QuietOnSuccess = true)]
public void ConstObeysFirstFunctorLaw(int i)
{
    var left = new Const<intstring>(i);
    var right = new Const<intstring>(i).Select(x => x);
 
    Assert.Equal(leftright);
}

If you think it over for a minute, this makes sense. The test creates a Const<intstring> that contains the integer i, and then proceeds to map the string that isn't there to 'itself'. Clearly, this doesn't change the i value contained in the Const<intstring> container.

In the same spirit, a property demonstrates the second functor law:

[Property(QuietOnSuccess = true)]
public void ConstObeysSecondFunctorLaw(
    Func<stringbytef,
    Func<intstringg,
    short s)
{
    Const<shortbyteleft = new Const<shortint>(s).Select(g).Select(f);
    Const<shortbyteright = new Const<shortint>(s).Select(x => f(g(x)));
 
    Assert.Equal(leftright);
}

Again, the same kind of almost-no-op takes place. The g function first changes the int type to string, and then f changes the string type to byte, but no value ever changes; only the second type parameter. Thus, left and right remain equal, since they both contain the same value s.

F# Const #

In F# we may idiomatically express Const as a single-case union:

type Const<'v'a> = Const of 'v

Here I've chosen to name the first type parameter 'v (for value) in order to keep the 'functor type parameter' name 'a. This enables me to meaningfully annotate the functor mapping function with the type 'a -> 'b:

module Const =
    let get (Const x) = x
    let map (f : 'a -> 'b) (Const x : Const<'v'a>) : Const<'v'b> = Const x

Usually, you don't need to annotate F# functions like map, but in this case I added explicit types in order to make it a recognizable functor map.

I could also have defined map like this:

// 'a -> Const<'b,'c> -> Const<'b,'d>
let map f (Const x) = Const x

This still works, but is less recognizable as a functor map, since f may be any 'a. Notice that if type inference is left to its own devices, it names the input type Const<'b,'c> and the return type Const<'b,'d>. This also means that if you want to supply f as a mapping function, this is legal, because we may consider 'a ~ 'c -> 'd. It's still a functor map, but a less familiar representation.

Similar to the above C# code, two FsCheck properties demonstrate that the Const type obeys the functor laws.

[<Property(QuietOnSuccess = true)>]
let ``Const obeys first functor law`` (i : int) =
    let  left = Const i
    let right = Const i |> Const.map id
 
    left =! right
 
[<Property(QuietOnSuccess = true)>]
let ``Const obeys second functor law`` (f : string -> byte) (g : int -> string) (s : int16) =
    let  left = Const s |> Const.map g |> Const.map f
    let right = Const s |> Const.map (g >> f)
 
    left =! right

The assertions use Unquote's =! operator, which I usually read as should equal or must equal.

Haskell Const #

The Haskell base library already comes with a Const newtype.

You can easily create a new Const value:

ghci> Const "foo"
Const "foo"

If you inquire about its type, GHCi will tell you in a rather verbose way that the first type parameter is String, but the second may be any type b:

ghci> :t Const "foo"
Const "foo" :: forall {k} {b :: k}. Const String b

You can also map by 'incrementing' its non-existent second value:

ghci> (+1) <$> Const "foo"
Const "foo"
ghci> :t (+1) <$> Const "foo"
(+1) <$> Const "foo" :: Num b => Const String b

While the value remains Const "foo", the type of b is now constrained to a Num instance, which follows from the use of the + operator.

Functor law proofs #

If you look at the source code for the Functor instance, it looks much like its F# equivalent:

instance Functor (Const m) where
    fmap _ (Const v) = Const v

We can use equational reasoning with the notation that Bartosz Milewski uses to prove that both functor laws hold, starting with the first:

  fmap id (Const x)
= { definition of fmap }
  Const x

Clearly, there's not much to that part. What about the second functor law?

  fmap (g . f) (Const x)
= { definition of fmap }
  Const x
= { definition of fmap }
  fmap g (Const x)
= { definition of fmap }
  fmap g (fmap f (Const x))
= { definition of composition }
  (fmap g . fmap f) (Const x)

While that proof takes a few more steps, most are as trivial as the first proof.

Conclusion #

The Const functor is hardly a programming construct you'll use in your day-to-day work, but the fact that it exists can be used to generalize some results that involve functors. Now, whenever you have a result that involves a functor, you know that it also generalizes to constant values, just like the Identity functor teaches us that 'naked' type parameters can be thought of as functors.

To give a few examples, we may already know that Tree<T> (C# syntax) is a functor, but a 'naked' generic type parameter T also gives rise to a functor (Identity), as does a non-generic type (such as int or MyCustomClass).

Thus, if you have a function that operates on any functor, it may also, conceivably, operate on data structures that have non-generic types. This may for example be interesting when we begin to consider how functors compose.

Next: The State functor.



Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or somewhere else with a permalink. Ping me with the link, and I may respond.

Published

Monday, 07 October 2024 18:37:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Monday, 07 October 2024 18:37:00 UTC