Constrained Types

July 15, 2020

Constrained Types

I’m a permanent learner. I’m always tinkering, reading, trying things, and building projects I know I’ll never finish. I first started programming with F♯ back in 2009, but I’m still learning that, too. Over the years, most anyone who spends any real effort trying to learn F♯ will encounter domain-driven design (DDD) principles. Scott Wlashcin has written pretty extensively about this, both on his website and in his book, Domain Modeling Made Functional.

Domain Modeling

One of the things I picked up in my exposure to DDD was the idea of modeling types and using the type system to best describe your domain model to be correct, and allow incremental improvement while being readable and intuitive. ScottW talks about this in his series “Designing With Types”.

For example, let’s say I want to model a restaurant menu, or maybe part of it (I work with restaurant menu data in my day job. One might consider how to represent the domain concept of a “product”:

type Product =
  { Id : Guid
    Name : string
    Price : decimal }

Of course, real products can be (and are) quite a bit more complex than this, but this is the level of complexity we need for a post like this. This represents the basic domain concept adequately in many cases.

Illegal States

One of the concepts ScottW explains very ably is the idea of making illegal states unrepresentable. He defends this idea thusly:

…[I]f the logic is represented by types, it is automatically self documenting. You can look at the union cases below and immediate see what the business rule is. You do not have to spend any time trying to analyze any other code.

and

…[I]f the logic is represented by a type, any changes to the business rules will immediately create breaking changes, which is a generally a good thing.

I won’t spend too much more time defining and arguing for this principle; here I accept it as a given. We want to make illegal states unrepresentative.

In our case, what would be an illegal state, and how would represent it in the model we have so far?

Notice the Name label. A .NET string is unbounded, which means that the label could hold a 64-character string, or a 6400-character string. In many systems, a 6400-character string would be too big for the database, where the schema would often specify a fixed-length string field (recognizing, of course, that there are some exceptions, such as fields of type text). Also, in the restaurant business, every system that has to handle products has some limitation. The receipt printer can only print as wide as the receipt paper (which isn’t very wide). The POS systems can only display so much at a time. Product names therefore have to be limited - and probably moreso than an engineer would want. Let’s say that in the real world, product names have to be less than 35 characters.

With that new requirement in the domain model, representing an illegal state is now trivial:

let product = 
  { Id = Guid.NewGuid()
    Name = "Ben's Brie, Biscuits, Bacon, and Benedict"
    Price = 25.75 }

This is now a valid F♯ record of course, but also an illegal domain state. My product is 41 characters long, and it will either get truncated in the various integrated components in a restaurant operation, or it will cause errors. It would be best if the error occurred before the data went anywhere.

Improvement

Fortunately for us ScottW already has an approach to this: constrained strings. Using his definition, we could update our model to something like this (see Scott’s site for implementation detail).

/// A string of length 35
type String35 = String35 of string with
    interface IWrappedString with
        member this.Value = let (String35 s) = this in s

 type Product =
   { Id : Guid
     Name : String35
     Price : decimal }

This is better, but also a bit intractable: do you really want to define a wrapper type for every possible length of string in your system? I started thinking about this recently, and it seemed to me that we could leverage type providers to help out with this, so I wrote one called BoundedString. With my type provider, the above code would become this:

type BoundedString35 = ConstrainedTypes.BoundedString<35>

type Product =
  { Id : Guid
    Name : BoundedString35
    Price : decimal }

You could, I suppose, skip aliasing the type and just use BoundedString<35> directly. The type provider is an erasing type provider, so any runtime operations will just be performed on the underlying type, string. In some ways this is only a marginal improvement over the implementation provided by ScottW, but in other ways, it clarifies and simplifies this way of designing. I hope you find it useful.

My code is available on GitHub at aggieben/ConstrainedTypes).