Posted by
Niklas Leopold
on August 14, 2020 ·
29 mins read
About the post
In a previous post we created our own ValidatorApplicative, but is it for real? I.e. does it follow the rules of an Applicative. Let’s find out.
We will also attempt to create an Applicative that fails to conform to the rules and find out the implications of that.
What is an Applicative?
If you are uncertain, please read this post first.
Using the laws formulated by Scalaz to verify our Applicative
In a previous post we created our own Validator type, here you can also find examples of how the data type is used.
And we also defined it as an Applicative:
What I failed to mention on that occasion is that it not enough to implement an instance of Applicative for you data type, it must also follow the laws of an Applicative.
For our convenience the people behind Scalaz has already written the laws in code for us and we will later use this in our tests:
Let’s walk through the laws one by one.
Identity
We start from the top with the identity law:
This law simply states that if we lift the identity function ((a: A) => a) and then applies it to the Applicative then the Applicative should be the same as before.
Can we test it? Of course we can!
In the code above we run the law specified by Scalaz on instances of our data type that was generated by ScalaCheck.
We can be reasonable sure that we follow the rule after the test above has passed, but let’s prove that we follow the rule as well:
There are two cases here, one is that the generated Validated[A] is valid:
And then we have the case when Validated[A] is invalid:
Homomorphism
Let’s move on to the second law.
This law states that is should not matter if you:
lift a value and a function into the context of an Applicative and then applies the function to the value
or if you first a apply a function on a value and then lift’s the result of the invocation into the Applicative
I.e. it should not matter if you apply the function to the value inside or outside the context of an Applicative, the end result should be the same.
We can use ScalaCheck to test just as before:
Can we prove it? Let’s try!
Interchange
And now the third law.
This law establishes that point is a left and right identity ref1.
And here’s a test for that:
And here’s the proof that we are following the Law. First we substitute with Valid
And now we substitute with Invalid:
MapLikeDerived
And now the fourth law:
Here we are proving that the Applicative supports the map operator.
The test:
And the proof:
Composition
In Scalaz the composition Law is defined for Apply which Applicative extends. However when reading about the Applicative in Haskell literature it is defined on Applicative. I guess Scalaz and Haskell has diverged a bit here.
This looks a bit intimidating but at least it’s easy to test:
And here is a proof for our own Applicative if we substitute with Valid.
Well actually, I think I leave it as an excercise for the reader since, well, I’m starting to get bored.
But why should we follow the rules?
Ok, so now we have proved that our Applicative follows the rules, but why should it? What are we loosing if we relax the rules?
So we are going to break the Laws and investigate the implications of that.
The data type we are using I have shamelessly stolen (or should I say borrowed?) from Learn You a Haskell for Great Good!, hopefully Miran doesn’t mind?
The data type looks like this:
The C here stands for counter. It’s a data type that looks much like Maybe a, only the Just part holds two fields instead of one. The first field in the CJust value constructor will always have a type of Int, and it will be some sort of counter and the second field is of type a, which comes from the type parameter and its type will, of course, depend on the concrete type that we choose for CMaybe a.
Our Applicative instance for the data type look like this:
We are running the same tests as before but for the new “Applicative”, now some of them fails!
It’s easy to see that the offender in this case is the counter, and the reason that we fail the Laws is that we do things in the context that affects our data type. That’s exactly the thing that the Laws are supposed help us to prevent!
The fact that the end result differs depending on if a computation is done inside or outside the context will most likely lead to strange bugs!
When writing this section I tried to come up with a better example then the one above since it feels a little bit “artificial” but I discovered that it’s kind of hard (at least for me) to come up with a good example. The Scala type system does great work to prevent wrongdoings in this case. The fact that Scalaz makes you implement the Applicative as a type class makes it really obvious that you are doing something you shouldn’t if you start to manipulate the data type that it “contains”. I found it much easier to follow the rules then to break them, but that could be due to lack of imagination on my part.
Conclusion
I this post we have investigated the Laws for an Applicative. We found out that Scalaz provides us with test cases that we can use when testing our Applicative.
Provided that we are using immutable data types without side effects is’s also possible to prove that we are following the Laws.
We also tried to break the Laws but found it hard to do since that Scala type system and the Scalaz type class makes it obvious how things should be done in a safe way.
We stole (sorry, I mean borrowed) an example of a data type that doesn’t follow the rules and concluded that the result of some computations differed if they are executed inside or outside the context, it is not difficult to imagine that could lead to confusion and bugs.