An early compelling case for open unions are extensible exceptions, which have been part of Haskell for many years (Simon Marlow, Haskell Workshop 2006). To permit throwing exceptions of arbitrarily many types, the thrown exception value is an open union (see SomeException in Control.Exception ). Raising an exception puts -- injects -- a particular exception value into the open union. When we handle an exception, we project: check if the thrown exception is of a particular desired type. (Extensible effects operate in the same manner; supporting, in addition, the resumption of an `exception'.) Thus, at its core, an open union should let us inject a value of any type into the union and to project a type from the union, that is, to find find out if the union value was previously injected with a particular specific type. These operations are familiar from OOP as downcast and upcast.

The open-union type of exceptions, SomeException (or the similar exn in ML), gives no indication of possible summands -- that is, which particular exception types may be in the union. Therefore, neither Haskell nor ML can ensure that a program handles all exceptions that could be raised during its execution.

To do better, the type of an open union should be annotated with the set of possible summands. The injection function will add the type of the injected value to the union type, unless it was there already. As always with types, the type of the open union is an approximation for the type of the value therein. Consider the simplest union Either Int Bool : at run time, the union value is either a single Int or a single Bool . The union type is an approximation: we cannot generally determine at compile-time the specific type of the value stored in the union. We are sure however that this type is not a String and hence attempting to project a String value from an Either Int Bool union is a compile-time error. Such type-annotated union is called a type-indexed co-product.

The familiar data type (Either a b) is the simplest example of typed unions, but it is not extensible. The constructors Left and Right are injections, and the projections are realized via pattern-matching:

prj1:: Either a b -> Maybe a prj1 (Left x) = Just x prj1 _ = Nothing

a

b

Either a b

a

b

Either a b

Left

Right

Either Int Bool

Either Bool Int

Heeding the drawbacks of Either , we arrive at the following interface for open unions:

data Union (r :: [*]) -- abstract type family Member (t :: *) (r :: [*]) :: Constraint inj :: Member t r => t -> Union r prj :: Member t r => Union r -> Maybe t decomp :: Union (t ': r) -> Either (Union r) t

The union type Union r is tagged with r , which is meant to be a set of summands. For the lack of type-level sets, it is realized as a type-level list, of the kind [*] . The injection inj and the projection prj ensure that the type t to inject or project must be a member of the set r , as determined by the type-level function Member t r . The function decomp performs the orthogonal decomposition. It checks to see if the union value given as the argument is a value of type t . If so, the value is returned as Right t . Otherwise, we learn that the received union value does not in fact contain the value of type t . We return the union, adjusting its type so that it no longer has t . The function decomp thus decomposes the open union into two orthogonal ``spaces:'' one with the type t and the other without. The decomposition operation, which shrinks the type of open unions, is the crucial distinction of our interface from the previous designs (Liang et al. 1995, Swierstra 2008, polymorphic variants of OCaml). It is this decomposition operation, used to `subtract' handled exceptions/effects, that insures that all effects are handled. The constraint Member t r may be seen as the interface between inj and prj on one hand and decomp on the other hand: for each injection or projection at type t there shall be a decomp operation for the type t .

This basic interface of open unions has several variations and implementations. One is OpenUnion1 , presented in the Haskell 2013 extensible-effects paper. It is essentially the one described in Appendix C of the full HList paper, published in 2004. In the version for extensible-effects, the summands of the open union have the kind * -> * rather than * . This implementation uses Dynamic as the open union. Therefore, all operations take constant time -- like polymorphic variants of OCaml and unlike open unions used by Liang et al. 1995 and Swierstra 2008.

One may notice a bit of asymmetry in the above interface. The functions inj and prj treat the open union index r truly as a set of types. The operations assert that the type t to inject or project is a member of the set, without prescribing where exactly t is to occur in the concrete representation of r . On the other hand, decomp specifies that the type t must be at the head of the list that represents the set of summand types. It is unsatisfactory, although has not presented a problem so far for extensible effects. If the problem does arise, it may be cured with an easily-defined conversion function of the type conv :: SameSet r r' => Union r -> Union r' , akin to an annotation. The other solutions to the problem (based on constraint kinds, for example) are much more heavier-weight, requiring many more annotations. Perhaps implicit parameters may help:

e1 = if ?x then ?y else (0::Int) -- inferred: e1 :: (?x::Bool, ?y::Int) => Int f :: ((?x::Bool) => r) -> r -- explicit signature required f x = let ?x = True in x t1 = f e1 -- inferred: t1 :: (?y::Int) => Int

t1

?x::Bool

f

One may notice that the open union interface, specifically, the function decomp , does not check for duplicates in the set of summands r . This check is trivially to add -- in fact, the HList implementation of type-indexed co-products did have such a check and so implemented true rather than disjoint unions. In case of extensible effects, the duplicates are harmless, letting us nest effect handlers of the same type. The dynamically closest handler wins -- which seems appropriate: think of reset in delimited control. There is even a test case for nested handlers in Eff.hs .

The implementation OpenUnion1 was received with significant controversy, often derailing discussions of extensible effects, which work with any open union implementation. Although OpenUnion1 provides constant-time operations, it relies on Dynamic , which requires the Typeable constraint all throughout extensible-effects. Although, with few exceptions, it is a minor annoyance, it is the annoyance still. The OverlappingInstances extension used by OpenUnion1 was also objected to, although without much reason since the extension does not leak from the open union implementation to the rest of the code. To address the concerns, OpenUnion41 was developed. It has the same interface as OpenUnion1 but uses neither Typeable nor OverlappingInstances . It relies on GADTs and closed type families. Alas, its operations in the worst case take time proportional to the number of summands in the union. The recent OpenUnion5 implementation brought back the constant-time union operations. It realizes ``strong sums'': existentials over a finite type universe that do not hide the type of the containing value.

We have thus seen the design space for typed open unions and a few sample implementations. Hopefully more experience will help choose an optimal implementation and introduce it into Haskell.