The Polyglot Approach to Getting Better at Modeling the State and Writing Property Tests in Elm
10 October 2016When learning a new programming language, there’s usually this problem of “What should I do next?”. Nowadays, many websites offer a clear path for beginners to follow (shout out to exercism.io!). However, with relatively new languages like Elm it may be hard to find materials on a given topic.
But what if we could… use resources written for other languages?
I stumbled upon Mark Seemann’s talk Types + Properties = Software. It’s about modeling the state with types in such a way that illegal states become unrepresentable. This makes writing property-based tests and programs much easier.
I liked the talk so much that I decided to dive deeper into this topic. Turns out Mark wrote a whole article series about that and on F# for fun an profit there’s another wonderful series of posts on designing with types.
But yeah, those two series are written with F# in mind. However, I’m going to show you that Elm has all the required tools and a type system powerful enough to implement all the concepts laid out in those articles!
What does it mean to make illegal states unrepresentable?
Modeling the state is hard. That’s especially true if you’re coming from a dynamically typed language. You may not know how to harness the type system to help you with this task.
If you’re following the Elm community, you may have heard just recently about making illegal states unrepresentable – Richard Feldman’s Making Impossible States Impossible is an excellent talk with many practical examples.
But what does it really mean to make impossible states impossible?
In short, the type system enables you to describe the program in such a way that it’s impossible to get it into a nonsensical state. You get compiler errors if you ever try to. The compiler tells you that what you wrote doesn’t make much sense even before you run the program.
For example, let’s say in our app we need a way to check if the pie is being prepared in the kitchen or if it’s ready for everybody to eat. A naive approach would be to try to represent this state as a string:
type alias Model = {
pieState: String
}
-- Later in the code:
model = {
pieState = "inKitchen"
}
-- …
{ model | pieState = "ready" }
This could sort of work. However, there’s nothing stopping us or other contributors from making a mistake and changing pieState
to an empty string, "blahblah"
, or just making a typo.
A better approach is to use a union type:
type PieState = InKitchen | Ready
type alias Model = {
pieState: PieState
}
-- Later in the code:
model = {
pieState = InKitchen
}
-- …
{ model | pieState = Ready }
If somebody ever tries to set pieState
to anything else than InKitchen
or Ready
, they’re going to get a compiler error.
What is property-based testing?
A bit contrived, but nonetheless good example to explain property-based tests is a function which reverses a list. Traditional tests check whether the function under the tests returns the correct output given a specific input. We might say that given ["foo", "bar", "baz"]
, our function returns ["baz", "bar", "foo"]
. But that’s just one case and we’d like to be sure the function works with anything people throw at it!
We could write a bunch of other tests, but this would not exhaust all possible inputs. Also, we definitely don’t want to write dozens of tests by hand. Instead, we can generate them.
So what are property-based tests? They are tests which try to ensure that for all possible inputs, the given property holds true. The testing library takes those tests, generates many random inputs and checks if the property is true for all of them.
It’s worth noting that different communities give different names for these tests. From elm-test docs:
These are called “fuzz tests” because of the randomness. You may find them elsewhere called property-based tests, generative tests, or QuickCheck-style tests.
While it may sound scary at first, looking at concrete examples should clear things up.
The biggest challenge with property-based testing is… Well, coming up with the right set of properties. What are the properties of the mentioned reversing function? One of them would be that reversing the list twice gives us the list we began with. How would we express that using just words? Let’s take the bolded sentence from two paragraphs above and rephrase it:
for all possible inputs… : for any list…
…the given property holds true : …reversing the list twice returns the original list
In terms of code, this property looks like this:
reverse (reverse list) == list
The only problem left is generating random inputs. This is the issue the libraries for fuzz testing solve. If we look at the docs for the fuzz
function of the brilliant elm-test library, we can see that the fuzz
function takes three arguments:
Fuzzer a
– The so-called fuzzer which generates random values of typea
.String
– A string describing the test.(a -> Expectation)
– A function describing the property. It takes a value of typea
and returns an expectation – which is just a fancy way of saying that the given value should be equal to the expected value, for example:
Expect.equal [4, 3, 2, 1] (reverse [1, 2, 3, 4])
From this function signature we can figure out that our role in writing property-based tests is to come up with a property expressed as a standard Elm function and a fuzzer—which is also a function—that’s going to generate random values for the property function. elm-test is then going to take the property function, generate hundreds or thousands of random values (the number is up to us) and check if all generated values satisfy the property.
Just as traditional tests, this method also doesn’t exhaust all possible inputs, but it’s able to come up with test cases which you’d never think of when testing in the traditional way. For example, fuzz tests helped with finding bugs in Clojure.1
That is not to say traditional tests are in opposition to property-based tests – they are quite complimentary and you’ll often find yourself using both.
There’s a little more to property-based tests than that. You can find out about it either by watching the mentioned Mark Seemanns’s talk or the intro to property testing done by John Hughes – one of the people behind QuickCheck, the original Haskell library for generative tests released in 1999.
How do unrepresentable states and property tests mesh together?
If you design the program in such a way that invalid states are unrepresentable, it’s much easier to define fuzzers for those states. As Mark Seemann put it:
With the algebraic data types available in F# or Haskell, you can design your types so that illegal states are unrepresentable. (…) This makes it much easier to test the behaviour of your system with Property-Based Testing, because you can declaratively state that you want your Property-Based Testing framework to provide random values of a given type. The framework will give you random values, but it can only give you valid values.
What are the Elm libraries for property tests?
From the available options, I can recommend the mentioned elm-test library. It has a built in support for running fuzz tests. It comes with a bunch of predefined fuzzers, but it also allows you to write custom fuzzers by combining functions delivered by two other libraries.
And that’s it! There’s another tool required to actually run the tests, but you’ll learn that from the elm-test readme.
Unrepresentable states, property testing, the F# articles and Elm tools. How do I make all of it work together?
So, it has come to this. You know something about modeling the state, about property-based tests and you know the tools you can use in Elm. Now is the time to read the Types + Properties = Software series.
The way to approach the series is to read an article, understand its contents (Mark is a good writer) and try to implement what has been shown in the article, but in Elm instead of F#.
You may be terrified that there’s another new thing to learn while you were just trying to practice Elm. Don’t be scared, you can do this! Although I have a bit of a prior experience with languages similar to F#, I never did F# myself. Code in F# is very similar to Elm code in terms of how it looks and how it works.
If you ever get lost, I prepared a repo with my solution to KataTennis in Elm. I tried to make one commit for each article from the series, so it shouldn’t be too hard to find out how I solved a particular challenge. Don’t be afraid to ask questions – I’m new to Elm too, so I’d love to get some feedback and discuss alternative solutions!
What are the biggest differences between F# and Elm implementations?
While I was going through KataTennis, it hit me how explicit Elm is. There’s no magic. I was able to figure out everything by looking at the type signatures, then maybe reading the docs for a particular function and then doing the same with another function.
In case of F#, it’s a slightly different story. As you get to Mark explaining the first property test, you’ll find out that he didn’t have to create a custom fuzzer for his union type. That’s because FsCheck (the F# library) can do that for you.
Does it make either language better than the other? I don’t think so. For me, it’s a matter of tradeoffs the language designers make and whether those tradeoffs fit my preferred way of working. It boils down to what features the designers chose to offer and to the design of particular libraries.
While Mark was happily strolling with the default FsCheck fuzzers, I had to implement a new fuzzer for each of the custom union types. Again, it’s hard for me to make a case for or against a given language out of this comparison. I liked how Elm made me pick the right semantics for each fuzzer, but I bet there are people who would label that as “boilerplate”.
I hope you’re going to enjoy KataTennis as much as I did! If you wish to comment, here’s a link to discussion on /r/elm.
The resources which were mentioned throughout this post are as follows:
- Types + Properties = Software – Mark Seemann @ NDC London 2016
- Mark Seemann’s Types + Properties = Software series
- The “Designing with types” series on F# for fun and profit
- Making Impossible States Impossible – Richard Feldman @ elm-conf 2016
- elm-test
- Testing the Hard Stuff and Staying Sane – John Hughes @ Clojure/West 2014
- My solution to KataTennis written in Elm