When writing toy frameworks in my free time, they usually revolve around a category. A category is a collection of objects with arrows between them such that a) there's an identity arrow and b) arrows can be composed in an associative manner.
For instance, Swift itself is a category with types being the objects, functions being the arrows and composition being function composition. But there are a lot of other categories of interest.
Another example would be gradient based learners. There, the objects are pairs of the type of interest (e.g. images or Float-arrays) and whatever you need for backpropagation; you then compose learners by composing their forward-channel and backward-composing their backward-channel.
A third example are reducers of equal state type and equal action type. Reducers can be thought of as functions of the form (State, Action) -> State
. If you have reducers of equal type, you can compose them by applying the first one and then the second one. This is an example of a monoid, i.e. a category with only one object Reducer<State, Action>
.
A fourth example are views in SwiftUI. They, too, form a kind of monoid, since you can just compose views into groups. Note, however, that here the arrows are of heterogenous type, because when composing views, you don't erase them to AnyView.
Now I find myself writing a lot of boilerplate that I could erase if only I found a way to write some "category protocol" or at least an "arrow protocol". But whenever I try, I run into trouble.
protocol Arrow {
associatedtype A
associatedtype B
func compose<Other : ???>(with other: Other) -> ???
}
Any ideas?
Context: My specific use-case where I produce endless boilerplate is DI. I like the way SwiftUI handles DI, using property wrappers. However, for observed objects, this may fail if you forget to first inject an observed object. Therefore, I wanted to do it the safe way inspired by Haskell:
protocol Injector {
associatedtype Injected
func inject(_ dependencies: MyEnvironmentType) -> Injected
}
MyEnvironmentType being essentially a wrapper for [String : Any]
with a typesafe subscript. However, if you have some composition defined on the Injected
types of Injector
s (which can be different types!), you really do want to pull them back to your Injector
types so you can just write nicer and cleaner code. It is obvious and easy to do this whenever you have a bunch of injectors injecting the same type (e.g. the reducer example): just write a monoid protocol, conform Injected
to Monoid and define a composition of Injector
s whenever Injected
conforms to monoid. But already for improper Monoids like Views, this approach fails and you end up defining your composition for each type. This becomes a problem when you try to write not only a composition of two values, but of three, four, ..., ten values and you have to do that for anything that is composable.