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 anundefined
value. It's equally easy to represent "up to one choice ofTopping
" in this foolproof way, using aMaybe
type: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 Topping
type, but we're no longer relying on the type system to make sets with duplicates unrepresentable. Instead, we're relying on the implementation of theSet
type in thecontainers
package, and the restricted interface it provides, to guarantee that the set won't contain any duplicates. If we dug into the guts of theSet
implementation, 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 theSet
author to make sure this can't happen.In contrast, the
Topping
type 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 theSet
implementation.)In a similar vein to the
Set
type, it would be possible to use anUpToThree Topping
type 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
UpToThree
values 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 anyUpToThree
value will represent no more than three distincta
values.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-
k
number. We could make the number a field in the data type:but this
UpToK
type is much less useful than it might first appear. The problem is that the smart constructor ensures that a particularUpToK
represents a number of distinct values of typea
that's no more than than the count given by the integer fieldk
encoded 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
UpToThree
that we receive no more than three toppings, provided the implementation of theUpToThree
type is sound, but if we try to write anUpToK
version, 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
Set
directly and didn't need theUpToK
type at all:Fortunately, there is a way to use the
DataKinds
extension, to index a collection of types by a non-negative number. With a few other extensions, this allows us to write:This
UpTo
type is much more useful thanUpToK
. It still relies on a protectedupTo
smart constructor to ensure that only validUpTo
values 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 Topping
value, 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 Topping
enforces 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
DataKinds
extension: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
toList
on 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
LimitedList
s 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
levelUp
function 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
singletons
package 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
Topping
type, as we'll see below.Here, we use
promote
to define type functionsP
andBefore
. 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
a
whose head (smallest) element ish
, and which contains no more thann
elements. We use theBefore
type function here to permit anx
value to be prepended to a list only if it's strictly less than the list's head element.Using this
MinLimitedSet
type 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 typeLimitedSet
that represents aMinLimitedSet
with an unspecified minimum element:With these GADTs in place, we can define an example value:
Note that this definition is valid only because
Pickle
andOnion
are in strictly ascending order (according to theOrd
instance 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 thetoList
function does.The full code: