Choosing Simplicity - not the easy option

This is a short post on a few recent events that are examples of a common idea.

Expecto - testing library vs testing framework

Expecto is a new testing library that has chosen a different approach to existing testing frameworks. It's designed as a library to be used in a testing exe project instead of code written to run inside a framework. Tomas Petricek hits the nail on head with his post on why frameworks are limited compared to libraries.

In Expecto, tests are constructed as values, so normal code is used to filter, parameterise, reuse and compose them. By backing up and choosing a simpler evolutionary path it's hoped the library can go further than current testing frameworks. This was not the easy option and several components like the Visual Studio Plugin and Visual Studio Code integration have had to be built. There has also been some scepticism that a new approach is needed.

Because of its simplicity Expecto already has some unique features:

Serialization - library vs hand coding

On one of my own event sourcing projects I've taken the decision to hand code the serialization and not use a library.

You've done what? You are crazy.

I need to make sure the serialization will cope with schema migration and always be backwardly compatible. I also have specific serialization compression I want to make use of e.g. FastPFOR.

I took inspiration from the Haskell Data.Serialize library.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
type Resize = byte[] -> int -> byte[]
type State = byte[] * int
type 'a SerializeGet = State -> 'a * int
type SerializePut = Resize -> State -> State

type 'a Serialize =
    {
        Put: 'a -> SerializePut
        Get: 'a SerializeGet
    }

Monads can be made for SerializePut and SerializeGet. This makes composing a type serializer from more primitive serializers very easy. Essentially after the primitives have been built it takes only two lines of simple code per field.

Using a great testing library (see what I did there) serialization is surprisingly easy to test thoroughly. Property based testing is used to ensure all serialization roundtrips correctly. This includes tests to cover schema migration and backward compatibility.

For the cost of a little extra code on schema change a simple serialization library can be built. It has the advantage of not needing any reflection or code generation. Also, because it is bespoke it should have great performance and produce smaller messages.

Support for type classes and HKTs in F#

Don Syme recently commented on adding type classes and HKTs to F#.

Like many others I've never designed a programming language but that is not going to stop me commenting on its evolution. From what I can see language design has a greater proportion of irreversible decisions than other areas of software engineering. It's well know that people spend too much time on reversible decisions and too little on irreversible ones.

Sometimes you need to simmer an idea down and add it at the right point to get the tastiest result.

There is some pressure from the community to get something in after it was announced that C# was exploring adding type classes. The decision to hold off can't be an easy one.

Personally, I'm happy to wait if it ensures F# is kept as simple and coherent as possible.

Conclusion

Simple solutions are easier to understand, generalise more naturally, and are more amenable to change.

Simplicity is not the easy option but it is worth fighting for.

module Main
type Resize = byte [] -> int -> byte []

Full name: Main.Resize
Multiple items
val byte : value:'T -> byte (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.byte

--------------------
type byte = System.Byte

Full name: Microsoft.FSharp.Core.byte
Multiple items
val int : value:'T -> int (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.int

--------------------
type int = int32

Full name: Microsoft.FSharp.Core.int

--------------------
type int<'Measure> = int

Full name: Microsoft.FSharp.Core.int<_>
type State = byte [] * int

Full name: Main.State
type 'a SerializeGet = State -> 'a * int

Full name: Main.SerializeGet<_>
type SerializePut = Resize -> State -> State

Full name: Main.SerializePut
type 'a Serialize =
  {Put: 'a -> SerializePut;
   Get: 'a SerializeGet;}

Full name: Main.Serialize<_>
Serialize.Put: 'a -> SerializePut
Serialize.Get: 'a SerializeGet