unsplash / sum-types

Safe, ergonomic, non-generic sum types in TypeScript.
https://unsplash.github.io/sum-types/
MIT License
42 stars 2 forks source link

Enumeration #39

Open samhh opened 2 years ago

samhh commented 2 years ago

It's a common use case to want to enumerate members of a sum type. This probably warrants inclusion in this library, but in what shape?

In Haskell, for all-nullary constructors, this might look something like this (GHCi):

> data X = A | B | C deriving (Enum, Bounded, Show)
> [minBound..] :: [X]
[A, B, C]

I believe that whilst derived instances of Enum and Bounded have a predictable order with relation to the data type definition, this isn't guaranteed for all possible implementations nor would otherwise be unlawful.

We have some helpers in Unsplash web with these type signatures and usages:

// `ExactMatch`: https://github.com/Microsoft/TypeScript/issues/13298#issuecomment-468733257
declare const enumerateNullary: <A extends Sum.Member<string>>() => <B>(ks: ExactMatch<Tag<A>, B>) => Array<A>
Sum.enumerateNullary<Weather>()(['Sun', 'Rain']) == [Weather.mk.Sun(), Weather.mk.Rain()]

type Values<A extends Sum.AnyMember> = {
  readonly [B in A as Tag<B>]: Value<B>;
};
declare const enumerate: <A extends Sum.AnyMember>() => (xs: Values<A>) => Array<A>
Sum.enumerate<Weather>()({ Sun: null, Rain: 123 }) == [Weather.mk.Sun(), Weather.mk.Rain(123)]

Unlike in Haskell, we can produce an enumeration provided an associated value for each constructor of a non-all-nullary sum type.

Because we've made the design decision to first and foremost define our sums at the type level, we don't know the tags at runtime except when provided by the consumer. In this case that means providing us with an array or object of tags. Sums of all-nullary constructors can benefit from a shorthand definition as above, however by moving from object keys to an array of keys we lose a uniqueness guarantee on the input. Additionally ExactMatch is required to ensure every key is present at least once.

fp-ts has a Bounded typeclass, but no Enum, nor any helpers. Notably in fp-ts, unlike in Haskell, Bounded is a subclass of Ord.

samhh commented 2 years ago

These could return NonEmptyArray as our sum types must always have at least one constructor. (Fun note: Apparently they needn't in Haskell, data X is a valid declaration.)

Member<string> (non-literal) allows {} in enumerate, making the function partial, however that's arguably reasonable in the sense that by not providing literals to Member the library's contract has been violated.