4

The cats documentation on FunctionK contains:

Thus natural transformation can be implemented in terms of FunctionK. This is why a parametric polymorphic function FunctionK[F, G] is sometimes referred as a natural transformation. However, they are two different concepts that are not isomorphic.

What's an example of a FunctionK which isn't a Natural Transformation (or vice versa)?


It's not clear to me whether this statement is implying F and G need to have Functor instances, for a FunctionK to be a Natural Transformation.

The Wikipedia article on Natural Transformations says that the Commutative Diagram can be written with a Contravariant Functor instead of a Covariant Functor, which to me implies that a Functor instance isn't required?

Alternatively, the statement could be refering to impure FunctionKs, although I'd kind of expect analogies to category theory breaking down in the presence of impurity to be a given; and not need explicitly stating?

Joe
  • 1,479
  • 13
  • 22
  • 2
    What the documentation is saying is that if F and G are functors then FunctionK[F, G] is a natural transformation. It automatically satisfies the commuting condition because of parametricity. FunctionK is parametrically polymorphic, that is it uses a single formula for all types. There are more natural transformations between two functors, but they can't be expressed in a parametrically polymorphic language. Essentially, they would have to use different formulas for different type parameters (e.g. distinguish between List[Int] and List[Bool]). – Bartosz Milewski Oct 13 '22 at 15:57

2 Answers2

3

You can write down FunctionK-instances for things that aren't functors at all, neither covariant nor contravariant.

For example, given

type F[X] = X => X

you could implement a FunctionK[F, F] by

new FunctionK[F, F] {
  def apply[X](f: F[X]): F[X] = f andThen f
}

Here, the F cannot be considered to be a functor, because X appears with both variances. Thus, you get something that's certainly a FunctionK, but the question whether it's a natural transformation isn't even valid to begin with.

Note that this example does not depend on whether you take the general CT-definition or the narrow FP-definition of what a "functor" is, the mapping F is simply not functorial.

Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
  • That's specific example is an [Invariant Functor](https://hackage.haskell.org/package/invariant-0.6/docs/Data-Functor-Invariant.html) right? So you can still draw a commutative diagram for it? – Joe Oct 13 '22 at 12:05
  • According to the documentation, yes, it seems so; However, "Invariant Functor" is clearly a red herring, because it's not actually a functor. Furthermore, being able to draw _some_ commutative diagram is somehow a non-statement, you can always draw some commutative diagrams, it's like saying _"you can write down an equation"_, but it doesn't guarantee that they will be particularly interesting. – Andrey Tyukin Oct 13 '22 at 12:49
  • Do you have a source for "Invariant Functor" isn't a functor (in the category theoretic sense)? That's an interesting statement – Joe Oct 13 '22 at 13:05
  • 1
    @Joe See [point 1 in the list here](https://ncatlab.org/nlab/show/counterexamples+in+category+theory) or [this answer here](https://mathoverflow.net/a/21329/79922). Strictly speaking, your question is not well-posed: a functor consists of two parts, the mapping for objects, and the mapping for morphisms. You gave only the object function that maps `X` to `X => X`, but you didn't say what the morphism-function would be. Until you give me a definition of `map`, I can't even tell you why your definition breaks. As you can see from Haskell docs, there doesn't seem to be a `fmap` anywhere. – Andrey Tyukin Oct 13 '22 at 13:20
  • I want to caveat this with "this isn't a strong area for me, so I might be talking nonsense", but do you not get a functor if you restrict your morphisms to isomorphisms (and have `map :: Iso a b -> f a -> f b`)? – Joe Oct 13 '22 at 14:07
  • 1
    @Joe Yes, if your `C` happens to be a groupoid, then `X -> End(X)` is indeed a functor. Or, if you take any `C`, and then restrict your attention to isomorphisms only, and consider that to be the domain of your mapping, then it's indeed a functor. This is indeed said indirectly in the two links above: [this is reachable through one more link on nLab](https://ncatlab.org/nlab/show/core), and [here, Riehl is pointing out that it's not necessarily a plain "no"](https://mathoverflow.net/a/216069/79922). – Andrey Tyukin Oct 13 '22 at 14:58
3

When you have some objects (from CT) in one category and some objects in another category, and you are able to come up with a way show that each object and arrow between objects has a correspondence in later then you can say that there is a functor from one to another. In less strict language: you can say that there is a functor from A to B if you can find a "subgraph" in B that has the same shape as A.

In such case you can "zoom out": draw a point, call it object representing category A, draw another, call it object representing B, and draw an arrow and call it functor.

If there are many ways you can do it, you can have multiple functors. And with more categories you can combine these functors like you compose arrows. Which in this "zoomed out" world look like normal objects and arrows. And you can form categories with them. If you can find a mapping between these categories, a functor on functors, then this is a natural transformation.

When it comes to functional programming you don't work in such generic framework. Usually, you assume that:

  • object is a type
  • arrow is a function
  • to define a category you almost always would have to use a generic type, or else it would be too specific to be useful as a general purpose library (isomorphism between e.g. possible transitions of one enum into transition states of another enum could be a functor, but that wouldn't necessarily fit some generic interface)
  • since programming languages cannot let you define a generic mapping between two arbitrary types, functors that you'll see will be almost exclusively Id ~> F: you can lift a function A => B into List[A] => List[B], Future[A] => Future[B] and so one easily (this proves existence of F[A] -> F[B] arrow for given A -> B arrow, and if A and B are generic you provided a proof for all arrows from Id), but try finding something more complex than "given A, add a wrapper around it to get F[A]" and it's a challenge
  • similarly the only natural transformations you'll see will be from Id ~> F into Id ~> G that is "given A, change the wrapper type from F[A] to G[A]", because you have a guarantee that there is the same A hidden somehow in both F and G and you don't have to deal with modifying it (only with modifying the wrapper)

The latter being exactly a FunctionK or just a polymorphic function in Scala 3: [A] => F[A] => G[A]. A concept from type theory rather than from CT (although in mathematics a lot of concepts map into each other, like here it FunctionK maps to natural transformation where objects represent types and arrows functions between them).

Category theory isn't so restrictive. As a matter of the fact, isn't even rooted in computer science and was not created with functional programmers in mind. Let's create non-FunctionK natural transformation which models some operation on "state machines" implementation:

  • object is a state in state machine of sort (let's say enum value)
  • arrow is a legal transition from one state to another (let say you can model it as a pair of enum values, and it somehow incorporates transitivity)
  • when you have 2 models of state machines with different enums, BUT you can take one model: one enum and allowed pairs and translate it to another model: another enum and its pair, you have a functor. one that doesn't implement cats.Functor but still
  • let's say you would model it with some class Translate[Enum1, Enum2] {...} interface
  • let's say that this translation also extends the model with some functionality, so it's actually Translate[Enum1, Enum2 | ExtensionX]
  • now, build another extension Translate[Enum1, Enum2 | ExtensionY]
  • if you can somehow convert Translate[A, B | ExtensionX] into Translate[A, B | ExtensionY] for a whole category of enums (described as A and B) then this would be a natural transformation
  • notice that it would not fit FunctionK just like Translate doesn't fit Functor

It's a bit stretched example, also hardly anyone implementing an isomorphisms between state machines would reach for functor as a way to describe it, but it should show that natural transformation is not FunctionK. And why it's so rare to see functors and natural transformations different than Id ~> F lifts and (Id ~> F) ~> (Id ~> G) rewrappings.

PS. When it comes to Scala, you can also meet CT used as: object is a type, arrow is a subtyping relationship, thus covariant functors and contravariant functors appear in type parameters. Since "A is a subtype of B" translates to "A can be always upcasted to B", then these 2 interpretations of functors will often be mashed together - something along "don't define map if you cannot upcast and don't define contramap if you cannot downcast parameter" (see also narrow and widen operations in Cats).

PS2. Authors might be defending against hardcore-CT fans: from the point of view of CT Kleisli and ReaderT are 2 different things, yet in Cats they are combined together into a single Kleisli type for convenience. I saw some people complaining about it so maybe authors of the documentation felt that they need this disclaimer.

Mateusz Kubuszok
  • 24,995
  • 4
  • 42
  • 64
  • _"functors that you'll see will be almost exclusively Id ~> F"_ - did you mean "nat trafos"? – Andrey Tyukin Oct 13 '22 at 12:52
  • No, I did not. I meant "functors from category Id to category F" which in many CT sources I saw (and FP codebases) would be just called "F". I used NT notation because IMO just using "F" would not show that functors starting at Id is a subset of all functors. – Mateusz Kubuszok Oct 14 '22 at 07:31