Context
I’m writing a Haskell module that represents SI prefixes:
module Unit.SI.Prefix where
Each SI prefix has a corresponding data type:
data Kilo = Kilo deriving Show
data Mega = Mega deriving Show
data Giga = Giga deriving Show
data Tera = Tera deriving Show
-- remaining prefixes omitted for brevity
Problem
I would like to write a function that, when applied with two SI prefixes, determines statically which of the two prefixes is smaller. For example:
-- should compile:
test1 = let Kilo = smaller Kilo Giga in ()
test2 = let Kilo = smaller Giga Kilo in ()
-- should fail to compile:
test3 = let Giga = smaller Kilo Giga in ()
test4 = let Giga = smaller Giga Kilo in ()
Initial Solution
Here’s a solution that uses a type class together with a functional dependency:
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
class Smaller a b c | a b -> c where smaller :: a -> b -> c
instance Smaller Kilo Kilo Kilo where smaller Kilo Kilo = Kilo
instance Smaller Kilo Mega Kilo where smaller Kilo Mega = Kilo
instance Smaller Kilo Giga Kilo where smaller Kilo Giga = Kilo
instance Smaller Kilo Tera Kilo where smaller Kilo Tera = Kilo
instance Smaller Mega Kilo Kilo where smaller Mega Kilo = Kilo
instance Smaller Mega Mega Mega where smaller Mega Mega = Mega
instance Smaller Mega Giga Mega where smaller Mega Giga = Mega
instance Smaller Mega Tera Mega where smaller Mega Tera = Mega
instance Smaller Giga Kilo Kilo where smaller Giga Kilo = Kilo
instance Smaller Giga Mega Mega where smaller Giga Mega = Mega
instance Smaller Giga Giga Giga where smaller Giga Giga = Giga
instance Smaller Giga Tera Giga where smaller Giga Tera = Giga
instance Smaller Tera Kilo Kilo where smaller Tera Kilo = Kilo
instance Smaller Tera Mega Mega where smaller Tera Mega = Mega
instance Smaller Tera Giga Giga where smaller Tera Giga = Giga
instance Smaller Tera Tera Tera where smaller Tera Tera = Tera
The above solution appears to solve the problem correctly, however it has a downside: the number of type class instances is quadratic w.r.t. the number of types.
Question
Is there any way to reduce the number of type class instances to be linear w.r.t. the number of types, perhaps by exploiting symmetry?
It may be that it’s more appropriate to use Template Haskell here, in which case, feel free to suggest that as a solution.
Thanks!
It could probably be argued that TH is more appropriate in cases like this. That said, I’ll do it with types anyhow.
The problem here is that everything is too discrete. You can’t iterate through the prefixes to find the right one, and you’re not expressing the transitivity of the ordering you want. We can solve it by either route.
For a recursive solution, we first create natural numbers and boolean values at the type level:
A bit of simple arithmetic:
A “less than or equal to” predicate, and a simple conditional function:
And conversions from the SI prefixes to the magnitude they represent:
…etc.
Now, to find the smaller prefix, you can do this:
Given that everything here has a one-to-one correspondence between the type and the single nullary constructor inhabiting it, this can be translated to the term level using a generic class like this:
Filling in the details as needed.
The other approach involves using functional dependencies and overlapping instances to write a generic instance to fill in gaps–so you could write specific instances for Kilo < Mega, Mega < Giga, etc. and let it deduce that this implies Kilo < Giga as well.
This gets deeper into treating functional dependencies as what they are–a primitive logic programming language. If you’ve ever used Prolog, you should have the rough idea. In some ways this is nice, because you can let the compiler figure things out based on a more declarative approach. On the other hand it’s also kind of terrible because…
UndecidableInstancesbecause of GHC’s very conservative rules about what it knows will terminate; but you then have to take care not to send the type checker into an infinite loop. For instance, it would be very easy to do that by accident given instances likeSmaller Kilo Kilo Kiloand something like(Smaller a s c, Smaller t b s) => Smaller a b c–think about why.Fundeps and overlapping instances are strictly more powerful than type families, but they’re clumsier to use overall, and feel somewhat out of place compared to the more functional, recursive style the latter uses.
Oh, and for completeness’ sake, here’s a third approach: This time, we’re abusing the extra power that overlapping instances gives us to implement a recursive solution directly, rather than by converting to natural numbers and using structural recursion.
First, reify the desired ordering as a type-level list:
Implement an equality predicate on types, using some overlapping shenanigans:
The alternate “is less than” class, with the two easy cases:
And then the recursive case, with an auxiliary class used to defer the recursive step based on case analysis of the type-level boolean:
In essence, this takes a type-level list and two arbitrary types, then walks down the list and returns
Yesif it finds the first type, orNoif it finds the second type or hits the end of the list.This is actually kind of buggy (you can see why if you think about what happens if one or both types aren’t in the list), as well as prone to failing–direct recursion like this uses a context reduction stack in GHC that is very shallow, so it’s easy to exhaust it and get a type-level stack overflow (ha ha, yes, the joke writes itself) instead of the answer you wanted.