I'm confused by his argument that Haskell functors are only endofunctors because everything is Hask. I thought that even if everything is part of one category, if the input is a different category that is a subset of the one category and transformed to another category, even if a subset of the same category, it could still be a regular functor instead of an endofunctor. For example, lets say you had a function f(x) t…
As with many definitional things in pure mathematics, the answer is "there's no absolute iron-clad reason why you can't think of things that way, but there are a variety of reasons both mechanistic and motivated by intuition why you might not want to do that."
To motivate it via intuition, let me start with another example. Take the function that maps every natural number k to the function f(x) = x + k (so it's a function whose codomain is `(N -> N)` or also expressed as `N^N`, allowing again for the fact that N^N is again "too loose" since we only care about functions of the form f(x) = x + k). Now someone could come along and say "well, f(x) = x + k" can be simply identified with just k so really the function we have is the identity function on `N -> N`, but I think most people would object and say that `k` is not the same thing as `f(x) = x + k`. The former can be thought of as an encoding of the latter, but it's not the same thing. So saying that the codomain of our function is N^N is in some sense more accurate than N and instead we can say that there's a way of embedding a subset of N^N into N via encoding f(x) = x + k as just k.
The crucial thing here is that deciding whether we are dealing with an artificial encoding of something, or the real thing itself is what lets us distinguish between saying that the function's codomain is N vs N^N, and in some sense the former feels "wrong" even though it's a "tighter bound."
Let's develop that intuition a little bit further. Imagine you have a function that takes a rational number and maps it to the largest integer smaller than it (that is the floor function). You could think of this function in one of at least four ways:
1. Q -> Q: A function that maps a rational number back to a rational number 2. Q -> Z: A function that maps a rational number to an integer 3. Z -> Z: Because rational numbers are countable, every rational number is "just" the indexed form of an integer, so "really" what we have is a function from Z -> Z 4. N -> N: Well by the same logic, every function on integers is just a function on natural numbers!
I think the last two probably feel the most "unnatural" for folks, and most representative of the problem of "representation vs what an object 'really' is" I talked about earlier. But the first two have what is basically the same choice, just in a context where the answer isn't as clear. Do you think the integer 2 really is just the same number as the rational number "2 / 1?" Or do you think that they are different from each other and an encoding process is necessary to move from one to the other? In some cases it's useful to say "yes every integer is exactly a rational number" and in that case we can say that Q -> Q reflects this intuition best. In other cases it might be more useful to say "no an integer is not a rational number, but there is a canonical way of identifying every integer with a certain rational number" and in that case we can say that Q -> Z reflects that intuition.
So your choice of codomain reflects what you think an object "really is" as opposed to just being an encoding. In this case we say that choosing Hask as the codomain (or "target") of our functor, hence making it an endofunctor, is a choice that reflects an intuition that the result of mapping using a Haskell functor is really just another type, not some other object that can be thought of as a type.
From this intuition there flows a variety of practical, mechanistic ramifications. Almost always when talking about mappings we are concerned about how they affect the structure of the source and target. When we say the codomain is the same as the domain, we also almost always mean that structurally they are the same and this considerably simplifies the analysis we need to do later, because we can reuse a lot of the same machinery in the domain and codomain. If you say that the codomain and domain are not the same, then all of a sudden you have to concern yourself with the structure of the codomain and domain and prove that all the equivalences and isomorphisms you want to hold and preserve the properties you care about do actually exist and do actually preserve everything you want preserved and it's just a lot more annoying.
So at a mechanistic level, what we gain from saying that "Haskell functors are only endofunctors because everything is Hask" is that we know we never map into something that has more structure than Hask; it always has the same structure. So we don't have to concern ourselves with a bunch of exploration into what happens if we map into a category with more structure than Hask.