Weakly typed APIs can bite you, particularly when you’re using a language like F# where you’re for the most part blessed with strongly typed APIs. Thankfully, strongly typed wrappers are often one‐​liners.

This may seem obvious, but I have been bitten by this several times, so I just want to briefly share the problems and solutions.

Case 1: ToString/string/%O

ToString (and by extension %O in the printf family of functions, as well as the string function) works on any object. This means that things can easily break when you change the type of the value you call ToString() on, since the string representation then likely changes but you don’t get a compilation error.

Say you have a domain model like this:

type Document = { Id: Guid }

You have a web API that provides access to a document, and the URL is constructed like this:

let getDocumentUrl (d: Document) =
  sprintf "/documents/%O" d.Id

The system is in production, and all is well. But later you figure out that you’d like to avoid primitive obsession and follow the recommended practice of creating more strongly typed single‐​case DU wrappers. So you do this:

type DocumentId = DocumentId of Guid with
  member this.Val = let (DocumentId x) = this in x

type Document = { Id: DocumentId }

This likely leads to plenty of wonderful compilation errors across your code base, and as is often the case in F#, when you have fixed them, things have a tendency to just work.

Not in this case though, because you don’t get a compilation error in getDocumentUrl. This is because ToString, and by extension %O, will just as happily eat a DocumentId as a Guid. But now, your URLs look like /documents/DocumentId dc2f650d-... instead of /documents/dc2f650d-.... And if you don’t have a test in place to catch this before deploying… Well, let’s just say I have been burned by this.

One solution is to create a simple wrapper you can use instead of ToString/string/%O. For example:

let guidToString (g: Guid) = g.ToString()

This is a function that calls ToString, but only accepts a Guid. You’d write similar functions for int and any other primitive type you needed to stringify.

If you originally had the guidToString helper, you would write getDocumentUrl as

let getDocumentUrl (d: Document) =
  sprintf "/documents/%s" (guidToString d.Id.Val)

This time, however, you would have gotten a compilation error when you changed Document.Id from Guid to DocumentId. Wonderful!

Case 2: .Value property

If you write a lot of DU wrappers, you may write them with convenience inner value accessors like this:

type DocumentName = DocumentName of string with
  member this.Value = let (DocumentName x) = this in x

Such an accessor is convenient because a property is often less verbose than destructuring. Furthermore, if the case is private because it’s a validated type, you can’t destructure it directly anyway.

But .Value is a dangerous name to pick, because there’s another commonly used type that has a property of the same name: The Option type.

To show how this can bite you, say you have this type:

type Document = { Name: DocumentName }

And somewhere, you use inner string value of the document name in a weakly typed API that accepts obj:

Log.Information(document.Name.Value)

(Yes, logging isn’t the most critical example, but you get the point.)

It’s in production and everything works, but now it turns out that not all documents have names. So you wrap Document.Name in option:

type Document = { Name: DocumentName option }

Again, you probably get a lot of compilation errors and fix them, but crucially, the document.Name.Value usage above still compiles. However, now the .Value property is Option.Value, not DocumentName.Value. Not only will the output be incorrect as described in Case 1; additionally, Option.Value will throw an exception if the value is None.

Again, I’ve been burned by this. Fool me once, shame on you. Fool me twice, time to refactor.

My solution is simply to use another name than Value for the single‐​case DU accessors. I use Val because I haven’t seen that name anywhere and it’s short, and I haven’t had the problem since.

Of course, no matter the name you choose, you can’t guarantee that you won’t ever come across the same problem. But wrapping something in Option is a common refactoring, and by avoiding .Value on my DU wrappers, I have eliminated this problem from my code.

I guess you could say that property accessors themselves are (semi-)weakly typed APIs, because a property accessor accepts a value of any type that has the property you’re trying to access. Normally it’s not a problem since the return type of the property is likely different, and you’d get a compilation error, but in this case, you feed the property value into an API accepting obj, so you lose that safety check.

The moral

This is not a problem inherent to F#, but in F# we’re so used to the wonders of a flexible strong type system that it’s easy to forget certain pitfalls of .NET’s numerous weakly typed APIs.

Compile‐​time safety is great, but you have to use strongly typed APIs in order for it to be relevant.

Join the Conversation

  1. Christer van der Meeren
  2. Avatar

2 Comments

Your email address will not be published.

Notify me of via e-mail. You can also subscribe without commenting.

Your email will not be published. It may be used to look up your Gravatar, and is used if you subscribe to replies or new comments. The data you enter in this form may be shared with Akismet for spam filtering.

  1. This is exact point is more pronounced in immutable vs mutable types, for example sharing an array in a record.