Swift 5 happened and I want to discuss HKT again.
I road previous topic and want to tell about our experience. And I will try not to use FP terms at all.
We have some containers in our project.
It is:
- Optional
- Either
- Promise
- etc
We have map function and some operators to use these types as Functor (Mappable).
Optional already have map
function, that works as fmap
for Maybe in Haskell.
We want to append `<^> operator for correct function call order.
func <^><T, U>(_ transform: (T) -> U, _ arg: T?) -> U? {
return arg.map(transform)
}
This code is correct, but we must declare this function for all of the containers: Optional, Array, Dictionary, Promise, Either and etc.
Now we want to append some sugar for Functor. We want to provide an operator to pass value instead of block. We must append this operator to all of the functors with the current type system.
func <^<T, U>(_ transform: T, _ arg: U?) -> T? {
return arg.map { _ in transform }
}
We have 5 methods for this sugar instead of one.
At the finish, we have some map methods (for our own containers) and 2 operators for every container. We can't expand all of the Functors in one place. We can't guaranty that all of the functors implement all of the methods and operators.
We use Applicative pattern in our project and it awesome. We can do not use optional binding in most situation.
Applicative provide us use a function in container + value in the same container.
A simple implementation of the operator:
func <*><T, U>(_ transform: Optional<(T) -> U>,
arg: Optional<T>) -> Optional<U> {
switch (transform, arg) {
case (.some(let transform), .some(let arg)):
return transform(arg)
default:
return nil
}
}
It great pattern. Now we want to append function that takes transform function and 2 containers. Return result of function:
func liftA2<T, U, V>(_ transform: (T, U) -> V,
_ arg1: Optional<T>,
_ arg2: Optional<U>) -> Optional<V> {
switch (arg1, arg2) {
case (.some(let arg1), .some(let arg2)):
return transform(arg1, arg2)
default:
return nil
}
}
We can represent this function through Functor and Applicative pattern. It will have the same implementation for all of Applicative. And we write 5 versions of the function for all of the Applicative containers: Array, Optional, Promise and etc. And we have not to guarantee that developer provide this function for a container.
All of these problems can be resolved through HKT.
protocol Functor {
associatedtype Element
func map<T>(_ transform: (Element) -> T) -> Self<T>
}
Now we can expand all of the Functor with operators, additional method and other.
HKT is a good concept for reuse some code for containers.