In general, when it's said "factoring through" it means that some morphish took an implicit roundabout which depends on exact object and unique morphism from/into it.
As an example, let's take a look at ho operator where both categories and functor are Arrows:
We can use these morphisms from ASCII package for an improvised example:
Challenge: what constructors in ASCII package do x/xx/xxx morphisms correspond to?
In this case you can use this ho operator as left-to-right Arrow composition:
If you want to use right-to-left composition with same morphisms you can use ha operator:
So far so good. Now let's slightly change xx morphism so that it returns Alone Glyph:
There is still a way to compose morphisms - we can use the fact that Alone is a covariant functor:
However it comes at cost of putting ASCII into Alone:
Structurally Alone Glyph and Glyph are the same but types are different, Alone here is just a wrapper:
We can wrap and unwrap things using structural subtyping relations:
How to use this subtyping relation? Just compose ho and st operators:
So that xx' output and xxx input match now:
Congratulations, we just mapped second parameter of xx' covariantly (Alone Glyph) factoring through its Supertype!
You also can factor through Supertype contravariantly by the way:
I recommend you to take a second and try to understand what's going on here, just substitute type parameters:
If you think about it, factoring through subtyping relation is a case where notion of variance in Haskell and Scala are intersecting.
Alright, another case. Latin type doesn't contain information about case, and our morphism x just assigns all characters to uppercase:
On, and we can factor through subtyping relation here as well to avoid explicitly applying to Unit since it's trivial:
Let's say that at some point we would not be interested in x output so that we want to replace it with some other value:
The most obvious solution is to use lambda syntax to ignore an input:
But it works only for Arrows, requires parentheses and you have to consume all input... Happy we, there is a better way:
What we are doing here is called factoring through Unit. But why exactly Unit? We'll get to the answer soon.
Now look at those type declarations above. How can we transform first line into second one?
Because it's a terminal object in Arrow category we always have morphisms to Unit per each object.
Just in case if it's not obvious - we can replace output with any object, this type of functions also called constant ones:
I actually lied in a previous paragraph - there is one object we can not replace output with, it's called Void and it doesn't have any value. Therefore it's tricky to work with it but not impossible:
I bet you have questions... Okay, Void is an initial object but what Unit is doing here? We do exactly the same thing:
Even more questions! It doesn't seem we actually map everything, initial is equivalent to identity here... The reason for that is weirdness of Void - there is no other candidate object for mapping. Even if it looks like we don't map everything - we still do and it's useful nevertheless, especially for handling representing objects (about it a bit later).
Another point you might wondering about - why do we have exactly these subtyping relations?
Challenge: Unit is 1, Void is 0, Arrow is exponential. What x¹ and x⁰ are equal to?
There is another class of objects we can factor through and they consist of other objects. Let's start with products:
Products have their counterparts - Sums, also known as coproducts - that tells that that morphisms should be flipped:
If you look at types it becomes obvious - factoring through products means that we construct objects from one source object:
In contrast, factoring through Sums means that we deconstruct objects into one target object:
Alright, it seems like we covered all objects we can factor through: Supertype, Unit, Void, Product, Sum. Time for examples!
We can initialise Maybe value with Unit and Void objects:
In case of Unit it's easy - many constructors are morphisms with Unit domain (except Unit itself, it's the only one nullary constructor):
But what if we have some object but we need to provide a morphism? Just ignore the input i.e. use constant function:
To make it more compact, we can factor through not only Unit but also make another turn on Supertype - since we apply to Unit here:
Due to laziness we can ignore Void as well:
The problem with the latter is that we are providing value from Void - it successfully compiles but doesn't make sense since this value is not going to be presented in functor we are co-representing, this type of functions are also known as absurd:
But wait, we already have a way to eliminate Void:
We just need to substitute type parameters for our case...
Both these options have the same result - it's impossible to evaluate therefore impossible to get any Boolean out of it:
Okay, last example. We covered Maybe and Booleans a bit here - both of them are structurally Sums i.e:
We have a nice way to work with Sums if they are in contravariant position i.e. do a case splitting. So that it's possible to compose factoring through Supertype and Sums:
Syntactically it's not pattern matching since we rely on parameter positioning, but it does the same job:
We even can convert these types (Maybe and Booleans) interchangeably:
Challenge: can you recognise colimit here?