Algebraic data types make it easy to allow one item from a set to be selected: just use an appropriate sum type.
I'm curious how one would implement the option of selecting strictly n items or fewer from a set. I can see how it might be possible to implement this with Data.Set, but I wonder if there's a pattern or an algebraic structure that would be more robust.
Say a burger can have three toppings from the set Pickle | Onion | Lettuce | Tomato etc. It's reasonable to want to display all the options in a UI, so if we use Data.Set, right away I notice that Data.Set.all is missing, so there's no easy way to print all the values that can be selected. But Data.Set.powerSet is available, so I'm thinking maybe I should be getting the user to choose an element of the subset of the power set with cardinality <= 3.
Using an element of the power set to represent a selection from the base set seems like a good idea. It doesn't look much like any UI I can think of, though. The power set functor is a monad, though I'm not sure if this is relevant (See discussion about power set as a functor here Sets, Functors and Eq confusion).
Maybe someone who has solved a similar problem before will have advice on how to best make this work. I'm really looking for conceptual insights into what an "n choose k" type looks like, if that makes sense.
Here's an answer, of sorts.
Here are a few extensions and imports needed for the rest of the code below.
As you're probably aware, the nice thing about representing a single item as an ADT:
is that the definition of the type makes it impossible to represent anything other than exactly one choice of
Topping, provided we ignore the possibility of having anundefinedvalue. It's equally easy to represent "up to one choice ofTopping" in this foolproof way, using aMaybetype:We can also represent zero or more (including a possibly infinite number of) toppings, with possible duplicates, using a list:
However, it's difficult to represent a set of toppings without duplicates this same way. We can use the
Set Toppingtype, but we're no longer relying on the type system to make sets with duplicates unrepresentable. Instead, we're relying on the implementation of theSettype in thecontainerspackage, and the restricted interface it provides, to guarantee that the set won't contain any duplicates. If we dug into the guts of theSetimplementation, we'd probably discover that the internal representation can allow sets with duplicates, and we're relying on the correctness of the code written by theSetauthor to make sure this can't happen.In contrast, the
Toppingtype above makes a stronger guarantee than this, making anything other than a single type unrepresentable by construction. (Well, I guess you could say we're relying on the correctness of the compiler, but it's common to think of this kind of guarantee as "stronger" than one that relies on the correctness of theSetimplementation.)In a similar vein to the
Settype, it would be possible to use anUpToThree Toppingtype to represent a set of zero to three distinct toppings, but the guarantee would be provided by the implementation and its sanctioned interface, not by the data representation itself. That is, we could write:If we only create
UpToThreevalues via thisupToThree"smart constructor" (either through programmer discipline or using Haskell's package system to limit the interface exported to users of the data type), then we can provide a guarantee of sorts that anyUpToThreevalue will represent no more than three distinctavalues.In practical Haskell code, this is probably the most reasonable approach. One limitation is that it requires a separate new type for each up-to-
knumber. We could make the number a field in the data type:but this
UpToKtype is much less useful than it might first appear. The problem is that the smart constructor ensures that a particularUpToKrepresents a number of distinct values of typeathat's no more than than the count given by the integer fieldkencoded in the value, so it ensures consistency between the two fields in the value, but the type system doesn't enforce anything aboutk, so if we have a function that requires no more than three toppings using the originalUpToThree:we can ensure through a type signature using
UpToThreethat we receive no more than three toppings, provided the implementation of theUpToThreetype is sound, but if we try to write anUpToKversion, we end up having to inspect the field anyway to ensure our precondition is met:which means we could have just checked the length of a
Setdirectly and didn't need theUpToKtype at all:Fortunately, there is a way to use the
DataKindsextension, to index a collection of types by a non-negative number. With a few other extensions, this allows us to write:This
UpTotype is much more useful thanUpToK. It still relies on a protectedupTosmart constructor to ensure that only validUpTovalues are produced, but the upper limit is now part of the type, so we can write:and we have a strong, type system guarantee that this version of
placeOnBurger'''will only be called with anUpTo 3 Toppingvalue, which implies a (weaker) smart constructor guarantee that the number of toppings passed will be no more than three.If you want the stronger guarantee of encoding the topping limit in the type so that it's enforced by the type system itself, the same way
Maybe Toppingenforces a zero-or-one topping rule, then it's possible, but it can quickly get unwieldy.One method is to use a GADT in combination with a Peano natural that's automatically promoted to the type level by the
DataKindsextension:It's also helpful to have a type-level function for converting from plain type-level naturals to these Peano type-level naturals:
Then we can easily create a GADT representing a list of "no more than
n" elements:It takes a little study to understand why this representation works, but observe what happens in GHCi when we try to create a 3-element list whose type-level limits are 3, 5, and 2:
The downside of this GADT is that it allows duplicates and doesn't provide a unique representation, so if we had multiple representations of pickles and onions as a no-more-than-3 list of toppings:
but wanted them to be treated uniformly (compare as equal, generate the same list of toppings in the same order when we run
toListon them, etc.), we'd end up having to write some more code to provide these (weak) guarantees.To answer another part of your question, it's possible to write a function to generate all valid
LimitedLists of a certain size for any enumerable type, though the output will include these duplicates and alternative representations. Writing such a function requires the use of a so-called "singleton" representation for the Peano naturals:The rather strange
levelUpfunction here is an expensive identity function, at least at the term level. (At the type level, it's not the identity, which is kind of the point.) Anyway, it's an unfortunate artifact of the choice of representation ofLimitedList. Ironically, it can be safely replaced withlevelUp = unsafeCoerce. At least, I think it can. It seems to work, at any rate.Before I give one last approach, here's all the code so far in case you want to play with it:
Finally, it is possible to enforce a unique, duplicate-free representation at the type level, but it adds another layer of complexity and makes the type much more difficult to use. Here's one way to do it by encoding the minimum element in the type and using a constraint to permit only smaller elements to be prepended. I've used the
singletonspackage to take care of some of the boilerplate, so we'll require quite a few extensions plus some imports:We'll need singletons for both our Peano naturals and our
Toppingtype, as we'll see below.Here, we use
promoteto define type functionsPandBefore. These could have been defined using type families, but promoting plain Haskell functions gives nicer syntax.Our limited set will be represented by the following GADT, which represents a (strictly) ascending list of elements of type
awhose head (smallest) element ish, and which contains no more thannelements. We use theBeforetype function here to permit anxvalue to be prepended to a list only if it's strictly less than the list's head element.Using this
MinLimitedSettype in code would require specifying the minimum element of the list in the type signatures. That's not very convenient, so we provide an existential typeLimitedSetthat represents aMinLimitedSetwith an unspecified minimum element:With these GADTs in place, we can define an example value:
Note that this definition is valid only because
PickleandOnionare in strictly ascending order (according to theOrdinstance forTopping) and there are no more than three elements being combined. If you try to create a list of too many toppings, or a list with duplicates or out of strictly ascending order, you'll get a compile-time type error. To use such lists in programs, you'll typically want to convert them to plain term-level lists, and that's what thetoListfunction does.The full code: