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:
The FileReadErr
might be a sum type that has multiple data constructors:
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:
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:
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:
extensible
freer
vinyl
fastsum
open-union
union
(this is the library that world-peace is based on)
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
orfastsum
. 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
tags: haskell