Open Sum Types in Haskell with world-peace

2019-07-11

I recently released the Haskell library world-peace-1.0.0.0. This library provides open sum types.

world-peace is not as fast as some other libraries providing open sum types, but it does have much better documentation than other libraries.

In this article I answer the following questions:

  • What are open sum types?
  • When would you want to use open sum types?
  • When would you want to use world-peace instead of other libraries?

What are open sum types?

Open sum types are used to represent "extensible" sum types. The opposite is a "closed" sum type. An example of a "closed" sum type in Haskell is Either.

Imagine we have a readFile function, which tries to read a file and return the contents:

readFile
  :: FilePath
  -> IO (Either FileReadErr String)

The FileReadErr might be a sum type that has multiple data constructors:

data FileReadErr = FileReadErrDoesNotExist | FileReadErrPermissions

readFile returns FileReadErrDoesNotExist when the file doesn't exist, and FileReadErrPermissions when the file exists but the permissions are too strict for it to be read.

We could write something similar using open sum types to represent the different cases of FileReadErr. This uses the OpenUnion type:

readFileOpenUnion
  :: FilePath
  -> IO (Either (OpenUnion '[FileDoesNotExist, PermissionsErr]) String)

data FileDoesNotExist = FileDoesNotExist

data PermissionsErr = PermissionsErr

Since the type signature for readFileOpenUnions is longer than readFile, this looks significantly more complicated. Using type-level lists also make this more difficult. However, using open sum types gives us more flexibility in some situations.

When would you want to use open sum types?

Open sum types track the cases in the type system:

data FileDoesNotExist = FileDoesNotExist

data PermissionsErr = PermissionsErr

readFileResult
  :: OpenUnion '[FileDoesNotExist, PermissionsErr]

Here, readFileResult can either be a value of FileDoesNotExist or PermissionsErr. This is exactly the same as the following:

readFileResult
  :: Either FileDoesNotExist PermissionsErr

However, open sum types are more flexible because it is easy to add cases:

data ErrorReadingFile = ErrorReadingFile

readFileResult'
  :: OpenUnion '[FileDoesNotExist, PermissionsErr, ErrorReadingFile]

If we wanted to represent this with an Either, it would start to look pretty messy:

readFileResult
  :: Either FileDoesNotExist (Either PermissionsErr ErrorReadingFile)

At this point, most people would create a new datatype for holding these three cases:

data AllFileReadErrs
  = AFRErrsNotExist FileDoesNotExist
  | AFRErrsPerms PermissionsErr
  | AFRErrsReadErr ErrorReadingFile

This works really well in many situations. It is simple.

However, it is not flexible. If you wanted to add another error type, you'd need to add another constructor. If you wanted to handle just one of the error types (and keep the others), you'd need a new data type holding the remaining errors.

Open sum types make it easy to handle just one case:

handlePermsErr
  :: OpenUnion '[FileDoesNotExist, PermissionsErr, ErrorReadingFile]
  -> IO (OpenUnion '[FileDoesNotExist, ErrorReadingFile])
handlePermsErr errors =
  case openUnionRemove errors of
    Right (permsErr :: PermissionsErr) -> error "got a permissions error!"
    Left newErrors -> pure newErrors

This uses openUnionRemove to peel off just the PermissionsErr, resulting in an OpenUnion with just two cases: OpenUnion '[FileDoesNotExist, ErrorReadingFile].

It is also possible to make this function polymorphic. The body of the function is the same as for handlePermsErr, but the type is a little more general.

handlePermsErrPolymorphic
  :: ElemRemove PermissionsErr es
  => OpenUnion es
  -> IO (OpenUnion (Remove PermissionsErr es))
handlePermsErrPolymorphic errors = ...

handlePermsErrPolymorphic takes any OpenUnion that contains a PermissionsErr. It returns an OpenUnion with all the original cases, but without the PermissionsErr. This is quite convenient.

It is also easy to add a new error type.

Open sum types are nice to use when you want flexibility in constructing and handling multiple error types.

Other functionality for OpenUnion can be found in the haddocks.

When to use world-peace vs. other libraries

There are many other libraries in the Haskell ecosystem that provide open sum types. Here are just a few:

The big advantage over these libraries is that world-peace has really good documentation. It should be easy to figure out how to use. There are many, many examples in the haddocks.

The main disadvantage of world-peace is that is it not as fast as libraries like extensible or fastsum because of the way it represents open sum types internally1.

If you're thinking of using open sum types, I'd recommend the following:

  • If you're a beginner, I recommend you stick to normal ("closed") sum types like Either.

  • If you're an intermediate Haskeller and want to play around with open sum types, I recommend world-peace because of its documentation.

  • If you're looking to use open sum types in an actual application, I recommend starting with world-peace because of its documentation. If you find that performance is a problem, I recommend switching to a faster library like extensible or fastsum. These libraries should be much easier to use if you are already familiar with world-peace.

Real-world uses of world-peace

world-peace is used extensively in the library servant-checked-exceptions.

servant-checked-exceptions uses open sum types to represent errors returned by Servant APIs. Using open sum types makes it easy to specify which APIs return which errors.

Other terms and other programming languages

There are many different terms used to describe "open sum types". Here are just a few:

  • open unions
  • extensible sum types
  • polymorphic variants

If you want to read more about open sum types, I recommend you google some of these terms.

Open sum types are also available natively in other programming languages. For example, in OCaml, they are called polymorphic variants2 (search in this page for the term "polymorphic").

PureScript has extensible row types. These make it natural to define open sum types (called "polymorphic variants" here too).

Why the name "world-peace"?

All the other good package names were taken :-)

Conclusion

Writing extensive documentation is quite time-consuming. However, I think it is a big help for people using libraries that make use of Haskell's advanced type-level functionality.

Having examples for each function makes the library much easier to understand and use. I hope more Haskell libraries adopt this approach.

Postscript

I posted this on Reddit. There are a couple good comments you may want to check out.

/u/hsyl20 posted about a package supporting open sum types they are working on called haskus-utils-variant. It has quite a lot of documentation, including a completely separate tutorial!

Footnotes


  1. This is not a fundamental limitation. If someone wanted to send a PR updating world-peace to have a better internal representation, I'd happily accept it.↩︎

  2. In OCaml, sum types are called "variants".↩︎

tags: haskell