Ok so now that we covered for most parts the base theory of variance; let's look at a practical code example of covariance vs. contravariance.
Covariance is easy
Covariance is both easy to understand and easy to employ in your code; because it's the "normal" for function application, for example:
PHP:
func increment(_ x: Int) -> Int {
return x + 1
}
func square(_ x: Int) -> Int {
return x * x
}
// usage
let input = 1
let result = square(increment(input))
print(result) // 4
In the above code we have 2 functions:
- increment: adds 1 to its input parameter value
- square: squares its input parameter value
In the usage example, we start with a variable input with a value of 1; and with
result we have pass the
input to
increment (standard function application), and then the output of that is finally applied to the
square function.
This is an example of a covariance, because we are working with outputs i.e. the output of our variable is fed into the
increment function, and the output of that is fed into our
square function. This mapping over outputs is covariance, and as we saw in the sine graph, the result is mutually proportional to the change we effect to the outputs.
Let's look at another example using an array of integers:
PHP:
let input = [0, 1, 2, 3, 4, 5, 6]
let result = input.map { pow(2, $0) } // $0 is syntactic sugar for the first element value being iterated in.
print(result) // [1, 2, 4, 8, 16, 32, 64]
Our
map method is what we refer to as a covariant functor; i.e. it's a higher order function that takes as function / computation as its input and then iterates over the elements, applying the computation to each element within that container type: the Array.
[table="width: 80%, class: outer_border, align: center"]
[tr]
[td]In a later post we'll look in detail at the internal workings of map, in comparison with a standard for-loop.[/td]
[/tr]
[/table]
Contravariance can be a bit confusing
Contravariance can be a bit confusing because it's not the "natural" way functional application works. As we saw in the sine graphs it's about affecting changes to the inputs of a computation, and is additionally confusing because it's not proportional or mutual to the change we make. In the sine graph we saw that affecting changes to the input had an opposing effect that was also not proportional in value to the change we applied.
Now if that wasn't confusing enough, we also need to appreciate that because contravariance only works with inputs; we won't be able use it in a majority of situations where covariance is natural to use.
[table="width: 80%, class: outer_border, align: center"]
[tr]
[td]Contravariance essentially only makes sense for data types that encapsulate computations; because we are effecting changes to a computation before it's applied to an input value.[/td]
[/tr]
[/table]
Let's define a type that encapsulates a computation
A good example of a this is a
Predicate; i.e. a Boolean computation that evaluates to either
True to
False
PHP:
struct Predicate<A> {
let contains: (A) -> Bool
}
In Swift a struct is like class except that it employs value semantics over reference semantics; you can however implement this as a class and it will produce the same result. So basically we've created a type called Predicate that is generic over a single type; our placeholder variable being
A can represent any type (including those we define) e.g. Int, String, Double, User, Employee, etc.
Internally we have a single property called
contains that is a variable that holds onto a boolean computation. Note: In Swift the initialisers / constructors for structs are automatically generated by the compiler. i.e. this struct has a
init method that we can use setup a Predicate value, for example:
PHP:
let lessThan10 = Predicate<Int>(contains: { x in x < 10 })
// Usage examples
print(lessThan10.contains(23)) // false
print(lessThan10.contains(8)) // true
Using this new predicate variable; works just like accessing a getter on our property called
contains, except that we have to pass in the expected value for the computation that is held by our Predicate variable i.e. we need to pass in an Integer value.
This works similar to logic conditions that we typically evaluate in a
if...else type clause, and similarly we can modify our predicate type to allow conjunctions of more than 1
Predicate.
We'll implement this by making a change to our predicate type to essentially allow concatenation of
Predicate types, for example:
PHP:
struct Predicate<A> {
let contains: (A) -> Bool
func concatenate(_ with: Predicate<A>) -> Predicate<A> {
return Predicate<A>(contains: {x in self.contains(x) && with.contains(x) })
}
}
In the above new version of Predicate we have added a method called
concatenate that has a single argument; a second
Predicate which we will logically concatenate with our existing
Predicate. In the internals we basically created a new Predicate that is the logical (and) combined result of both Predicates by applying the same input value to both Predicates.
Here's an example of how we use this.
PHP:
let lessThan10 = Predicate<Int>(contains: { x in x < 10 })
let greaterThan5 = Predicate<Int>(contains: { x in x > 5 })
// let's combine these two Predicates
let combined = lessThan10.concatenate(greaterThan5)
// and finally let's test this with a few integer values
print(combined.contains(5)) // false
print(combined.contains(6)) // true
print(combined.contains(10)) // false
print(combined.contains(9)) // true
We can also define an operator for this; i.e. to simplify both the concatenation of predicates and composition, basically by not having to wrap everything in brackets. Let's add concatenation operator to our
Predicate type.
PHP:
struct Predicate<A> {
let contains: (A) -> Bool
func concatenate(_ with: Predicate<A>) -> Predicate<A> {
return Predicate<A>(contains: {x in self.contains(x) && with.contains(x) })
}
static func <>(lhs: Predicate<A>, rhs: Predicate<A>) -> Predicate<A> {
return lhs.concatenate(rhs)
}
}
...and let's see how that changes our usage:
PHP:
// instead of .dot method chaining
let combined = lessThan10.concatenate(greaterThan5)
// we can use our new <> concatenation operator
let combined = lessThan10 <> greaterThan5
[table="width: 80%, class: outer_border, align: center"]
[tr]
[td]
Note: The
<> operator is derived from the algebraic term
Semigroup; which is an algebraic structure with an associative binary operation e.g. addition, multiplication, logical conjunction, etc.
[/td]
[/tr]
[/table]
Let's add Contravariance
Ok without getting too complicated, let's using a very simple example, examine how we can apply an effect to our Predicate's input; in much the same way that an effect was applied to the 2nd sine graph.
First off let's define a predicate for all the odd Integers
PHP:
let isOdd = Predicate<Int>(contains: { $0 % 2 == 1 })
Now to create a Predicate for all odd Integers we can do either of the following:
Option 1:
We create a new predicate where the closure evaluates odd numbers using modulus of 2
PHP:
let isOdd = Predicate<Int>(contains: { $0 % 2 == 0 })
Option 2:
We create a new predicate by adjusting the input of the isEven predicate; basically we add 1 to its input to derive an isOdd
PHP:
let isOdd = Predicate<Int>(contains: { x in isEven.contains(x + 1) })
This 2nd option is an example of contravariance; because we are adjusting the input before it enters the
isEven.contain computation.
There is however a more elegant way to achieve this -- we can create a higher order method like
map to contravariantly adjust the input to a computation; aptly the most appropriate name for this method is
contramap or alternatively
comap.
Let's modify our
Predicate to include a
contramap method:
PHP:
struct Predicate<A> {
let contains: (A) -> Bool
func contramap<B>(_ f: escaping (B) -> A) -> Predicate<B> {
return Predicate<B>(contains: f >>> self.contains)
}
func concatenate(_ with: Predicate<A>) -> Predicate<A> {
return Predicate<A>(contains: {x in self.contains(x) && with.contains(x) })
}
static func <>(lhs: Predicate<A>, rhs: Predicate<A>) -> Predicate<A> {
return lhs.concatenate(rhs)
}
}
Ok we've added a new method called
contramap, that essentially feeds (or injects) a modification into our Predicate's type
(A) ahead of its own computation; we achieve this by forward composing our new function from
(B) to a new
A with the computation that our Predicate is currently holding onto in its
contains method. Essentially this allows us to adjust the input before the computation.
Let's finally for this post examine how we would create an
isOdd predicate by modifying the input of
isEven using our new
contramap method.
PHP:
let isOdd = isEven.contramap { $0 + 1 }
...and there we have it a higher order function that allows us to easily contravariantly inject changes into the input of a computation i.e. the one we're holding onto in our Predicate type.
In the next post we'll examine a more elaborate usage case for our new
contramap method.