Partial Type Constructors Or, Making ad hoc datatypes less ad hoc

ion. Because they are not properly supported by current languages, partial type constructors do not work well in combination with other important features or abstractions. For example, the inability to define certain type constructors, such as UArray or Set, as instances of standard type classes like Functor or Monad has been an almost constant source of frustration for Haskell programmers. Evidence for this appears in many forms, from informal requests and queries in online discussions, to several proposals for extensions to Haskell to address this specific shortcoming [Hughes 1999; Orchard and Schrijvers 2010; Sculthorpe et al. 2013]. Error reporting. Skeptics may argue that types like UArray Integer are at best a minor annoyance: they pose no immediate threat to type safety. However, developers already rely on types to identify and prevent common forms of programming error—even kind checking itself is unnecessary to assure type safety. Supporting partial type constructors would allow us to report errors earlier, upon, say, spotting UArray Integer instead of reporting an error only when some function tries to populate that type. Technical foundations and limitations. A proper accounting of partiality requires great care. What does it mean, for example, to instantiate a polymorphic type scheme at a type of the form F t when the latter only exists for certain choices of t? We point to work on injective type families [Stolarek et al. 2015] as an example where the designers of a language feature were able to avoid such complications by treating type families as total, but then, to avoid contradictions, were forced to impose syntactic restrictions that prevent it from being used in some practical applications [Morris and Eisenberg 2017, Section 3.3]. 3 LANGUAGE DESIGN FOR PARTIAL TYPE CONSTRUCTORS 3.1 Datatype Contexts in Haskell The syntax for datatype definitions in Haskell includes a feature that, at first glance, seems to have been designed specifically for the purpose of supporting partial type constructors like UArray. In particular, Haskell allows definitions of algebraic datatypes to include type class constraints that specify restrictions on how their parameters can be instantiated. The following example illustrates the concrete syntax for this, using an IArray a constraint at the start of the definition to suggest that any parameter of the UArray type constructor must be an instance of the IArray class, and hence must have an unboxed representation: , Vol. 1, No. 1, Article . Publication date: September 2019. 6 Mark P. Jones, J. Garrett Morris, and Richard A. Eisenberg data IArray a ⇒ UArray a = MkU . However, following the actual definition in the Haskell Report [Marlow 2010, Section 4.2. 1], the IArray a constraint shown here is interpreted by associating it with operations on UArray values rather than the UArray type itself, and this fails to give the behavior that we want from a partial type constructor. For example, even with the above definition, the type UArray Integer is still accepted as valid and can be used in other types, such as UArray Integer → UArray Integer, even though Integer is not an instance of the IArray class. With the definition in the Haskell Report, the presence of an IArray a constraint in the type definition does not itself provide a proof of this constraint in functions that work with unboxed arrays. For example, it is not possible to define a working map function of the following type; the type makes the impossible demand for a fully polymorphic implementation that will work with all combinations of a and b: mapUArray :: (a → b) → UArray a → UArray b Instead, the programmer is forced to insert explicit IArray constraints as part of the type: mapUArray :: (IArray a, IArray b) ⇒ (a → b) → UArray a → UArray b Similar difficulties arise in constructing monadic embeddings of domain-specific languages. These embeddings may rely on limiting the range of possible values, such as by requiring that values be serializable, or meet some other domain-specific criteria. However, while deep embeddings can capture such constraints, using GADTs [Cheney and Hinze 2003; Xi et al. 2003], the resulting types do not fit the standard notion of monad (because they require restrictions on the type of return). Judging from both recurring postings and complaints and from the research effort surrounding this problem [Orchard and Schrijvers 2010; Sculthorpe et al. 2013], many Haskell programmers have been frustrated and confused by the inability of the language to support such examples. 3.2 A Constraint for Well-Formed Applications The partiality we seek to tame arises when one type is applied to another. We thus wish to use a predicate to specify when one type is applicable to another, and we write this as an infix @. Intuitively, f @ a holds when its right-hand argument is a valid parameter for the constructor in its left-hand argument. When f is a known type constructor (like List or UArray), we replace f @ a with the constraints (if any) in the datatype context of f’s declaration. For example, List @ a holds for any type a, because the List type constructor is total (declared without constraints), but the constraint UArray @ Integer does not hold because the argument type on the right of the @ symbol is not an instance of the IArray class. We need to preserve@ constraints in the types of polymorphic functions where the definedness of type expressions depends on the quantified variables. The elem function on lists does not need an @ constraint in its type elem :: Eq a ⇒ a → List a → Bool but we must capture the fact that UArray is partial in the types of polymorphic unboxed array operations: arrayElem :: (UArray @ a, Eq a) ⇒ a → UArray a → Bool While the UArray @ a constraint is formally necessary, it is also implied by the structure of the type: occurrences of the type UArray a must always be guarded by UArray @ a predicates. We can take advantage of this to automatically elaborate such constraints, rather than requiring programmers to write them explicitly; we give our elaboration function in Section 4.2. Interestingly, this process does not remove the need for all such explicit constraints: see Section 6.2 for further exploration. Using this elaboration, we would be able to write , Vol. 1, No. 1, Article . Publication date: September 2019. Partial Type Constructors 7 arrayElem :: Eq a ⇒ a → UArray a → Bool making it fully parallel with the list elem operation. We can see further advantages of fully embracing partial type constructors as part of the type system when we consider higher-order abstractions. One of the biggest advantages of accepting partiality in the type language is that it allows us to accommodate partial type constructors in abstractions that were originally designed with only total type constructors in mind. To see why, recall the mapping function for unboxed arrays mapUArray :: (IArray a, IArray b) ⇒ (a → b) → UArray a → UArray b With our approach, we could rewrite this type to rely on the partiality of UArray: mapUArray :: (UArray @ a, UArray @ b) ⇒ (a → b) → UArray a → UArray b These types (and indeed the type that omits the definedness constraints entirely) are all considered equivalent in our system—we neither require programmers to write out definedness constraints, nor penalize them for doing so. We could not use this function to make UArray an instance of Functor in Haskell today, as the type of fmap: fmap :: Functor f ⇒ (a → b) → f a → f b must work on arbitrary a and b. However, our system provides a uniform approach for code that abstracts over type constructors to reflect the possibility that those type constructors may be partial. The Functor class, for example, would have the following definition: class Functor f where fmap :: (f @ a, f @ b) ⇒ (a → b) → f a → f b Again, the@ constraints here are required by the structure of the type, and could be omitted by the programmer. With this definition, we can see that mapUArray is a candidate for fmap, and the following instance would be accepted: instance Functor UArray where