intermediate
Type classes, also known as extension interfaces, are usually represented in Swift as protocols with associated types and/or Self
requirements. They are groups of functions that operate on generic type parameters and are governed by algebraic laws.
Type classes can be used to enable ad hoc polymorphism; they act as constraints added to type parameters in parametrically polymorphic types or functions.
Consider, for instance, the Equatable
protocol. It is a type class that adds some functionality to existing types; more precisely, it enables to check for equality of values of a type. It is governed by the following laws:
a == a
).a == b
, then b == a
.a == b
and b == c
, then a == c
.The Equatable
type class can be used to constrain implementations of parametrically polymorphic functions; e.g:
func allEqual<A: Equatable>(_ array: [A]) -> Bool {
guard let first = array.first else { return false }
return array.reduce(true) { partial, next in partial && next == first }
}
The function above is parametrically polymorphic; it operates on values of type A
. Valid types to fill this type parameter need to conform to Equatable
and enable the possibility to use the ==
operator.
Equatable
, like many other protocols provided by Foundation or any other Swift framework, operates at the type level. However, there are many other interesting abstractions as type classes that need to operate at higher kind level. This is not possible by default in Swift, but Bow simulates HKTs and enables type classes to work at this level.
Let us consider the following example. We can try to define a type class that enables transforming values in containers; i.e., given an F<A>
, we would like to obtain an F<B>
, provided that we know how to transform from A
to B
:
F<A>
is not possible to be written natively in Swift. Bow supports this feature by writing it as Kind<F, A>
.A
to B
corresponds to a function (A) -> B
.* -> *
, it should be implemented by F
, so in the context of the definition of the type class, we can refer to F
as a Self
requirement.Therefore, the definition of the type class may be:
protocol Transformer {
static func transform<A, B>(
_ fa: Kind<Self, A>,
_ f: (A) -> B) -> Kind<Self, B>
}
As we mentioned above, type classes are governed by algebraic laws. This means that all implementations of the type class must behave on a certain manner and satisfy some properties. Each type class has its own laws, as the ones we presented for Equatable
above.
For the Transformer
type class that we are defining, we can consider some laws as well:
id
), we must obtain the original value; i.e., F.transform(fa, id) == fa
.F.transform(F.transform(fa, f), g) == F.transform(fa, g <<< f)
.Swift does not provide a way of encoding these laws and enforcing them in every implementation. To do this, Bow type classes encode their laws as property-based tests using SwiftCheck. Then, each implementation can be tested against these laws to guarantee that the implementation satisfies them. Type class laws help us have a better reasoning about our code and some times help us rewrite pieces for optimization (like in the case of Transformer
where we can compose functions and apply a single transformation instead of two).
Notice that sometimes you can provide an implementation for a given type class that does not satisfy the laws. Those are called lawless instances, in contrast to the ones that satisfy them, which are called lawful instances.
A type class instance is a concrete implementation of a type class for a given type. Instances are usually created through the extension mechanisms that Swift provides. For instance, let us provide an instance of Transformer
for the Option
data type in Bow:
extension OptionPartial: Transformer {
static func transform<A, B>(_ fa: OptionOf<A>, _ f: (A) -> B) -> OptionOf<B> {
fa^.fold(Option<B>.none, // It is empty, no transformation
{ a in Option<B>.some(f(a)) }) // Transformed with f and wrapped in an Option<B>
}
}
Notice that, since the type class works at the kind level, it is not an extension of Option
, but an extension of OptionPartial
. Revisiting the definition of Transformer
, the Self
requirement is used in Kind<Self, A>
. Therefore, it must be implemented by the Partial
version of the type, omitting the last parameter. In the case of Option, as it only has one type argument, it should be OptionPartial
, with no type arguments.
In the case of types with a higher kind, like Either
, we can resort to the same strategy: Either<L, R>
is equivalent to Kind<EitherPartial
Type | What to extend for a type class like Transformer? |
---|---|
ArrayK | extension ArrayKPartial: Transformer { … } |
Const | extension ConstPartial: Transformer { … } |
Either | extension EitherPartial: Transformer { … } |
Id | extension IdPartial: Transformer { … } |
Ior | extension IorPartial: Transformer { … } |
NonEmptyArray | extension NonEmptyArrayPartial : Transformer { … } |
Option | extension OptionPartial: Transformer { … } |
Try | extension TryPartial: Transformer { … } |
Validated | extension ValidatedPartial: Transformer { … } |
Kind
Type classes that operate on kinds can project some methods on this type so that they are easier to use. This is achieved through extension too; we can extend Kind
with a constraint on its F
type, and enable the syntax for transformer as an instance method:
extension Kind where F: Transformer {
func transform<B>(_ f: (A) -> B) -> Kind<F, B> {
F.transform(self, f)
}
}
This way, every type that its witness has an instance of Transformer
, will be able to use transform
as an instance method. We have two ways of using this type class; consider a function to multiply by two the values contained in some structure:
func multiplyByTwo_v1<F: Transformer>(_ value: Kind<F, Int>) -> Kind<F, Int> {
F.transform(value, { x in 2 * x })
}
func multiplyByTwo_v2<F: Transformer>(_ value: Kind<F, Int>) -> Kind<F, Int> {
value.transform { x in 2 * x }
}
Both versions are equivalent. Type classes defined in Bow project their methods as instance or static methods in Kind to make them easier to find and use. Notice that the compiler is able to resolve which instance it needs to provide based on the type that we use to call the function:
let some = multiplyByTwo_v1(Option.some(1))
let none = multiplyByTwo_v2(Option.none())
The reader may have guessed that the Transformer
type class that we have created above is, in fact, the Functor
type class, and transform
corresponds to map
. Bow modules include multiple type classes, like Functor
, with their corresponding instances. The following table summarizes some of them; for further information, refer to the API reference for each one of them.
Type class | Purpose |
---|---|
Semigroup | Combine two objects of the same type |
Monoid | Combinable objects have an empty value |
SemigroupK | Combine two kinds of the same type |
MonoidK | Combinable kinds have an empty value |
Functor | Transform the contents of an effect, preserving its structure |
Applicative | Perform independent computations |
Monad | Perform sequential computations |
ApplicativeError | Recover from errors in independent computations |
MonadError | Recover from errors in sequential computations |
Comonad | Extract values from a structure |
Bimonad | Monad and Comonad behavior |
Foldable | Summarize values of a structure into a single value |
Traverse | Apply effects on the values of a structure |
FunctorFilter | Transform values based on a predicate |
MonadFilter | Execute values that pass a predicate, sequentially |
TraverseFilter | Traverse values that pass a predicate |
MonadReader | A Monad with capabilities to read from a shared environment |
MonadWriter | A Monad with the ability to produce a stream of data in addition to the computed values |
MonadState | A Monad with the ability to maintain (read and write) a state |