I demonstrated how to make kilobyte/megabyte constants in Haskell and a Lobsters user asked how it worked. This is a bloggification of my reply to them explaining the trick.
The original code:
data Bytes =
B
| KB
| MB
instance (b ~ Double) => Num (Bytes -> b) where
fromInteger i B =
fromInteger i
fromInteger i KB =
fromInteger $ i * 1024
fromInteger i MB =
fromInteger $ i * 1024 * 1024
instance (b ~ Double) => Fractional (Bytes -> b) where
fromRational r B =
fromRational r
fromRational r KB =
fromRational $ r * 1024
fromRational r MB =
fromRational $ r * 1024 * 1024
Using this from the REPL:
Prelude> 100 B
100.0
Prelude> 100 KB
102400.0
Prelude> 100 MB
1.048576e8
The Lobsters user asked me about the demonstration.
Do you need any language extensions?
You need flexible instances and ~
, usually from TypeFamilies
. These are things I have turned on by default in all of my application projects.
Would you mind explaining a bit more in detail how it works?
Warning: This explanation won't work great if you know literally zero Haskell. If that's the case, uhhhh, buy my book?
The Num
and Fractional
type classes are the basis for integral and fractional literals in Haskell. Every numeric literal in Haskell is polymorphic until the instance is resolved to a concrete type. 1 :: Num a => a
and 1.0 :: Fractional a => a
, where ::
is type ascription and can be read as "has type."
The (b ~ Double)
isn't strictly necessary if you don't care about type inference. You could elide it and then the examples would look like:
Prelude> 100 B :: Double
The essence of it is in the weird shape of the instance type: Num (Bytes -> b)
. We're making an instance for numerical literals that is function-typed. We've said the inputs must be values from our Bytes
type. This makes it so that in the expression: 100 B
, the 100
gets resolved to a function which multiples the underlying number by the magnitude expressed in the data constructor passed to it as an argument, be it B
, KB
, or whatever else.
Now, hypothetically, we might know that we always want Bytes -> Double
. If we don't care about type inference, then our instance can be pretty simple:
instance Num (Bytes -> Double) where
And we're done, but that didn't satisfy me. I wanted to see if I could get fancy syntax. If you try it unqualified, you get:
Prelude> 100 KB
<interactive>:7:1: error:
• No instance for (Num (Bytes -> ())) arising from a use of ‘it’
(maybe you haven't applied a function to enough arguments?)
• In the first argument of ‘print’, namely ‘it’
In a stmt of an interactive GHCi command: print it
GHCi will default the return type to ()
when the instance head is Bytes -> Double
. So the stages of evolution are:
Bytes -> Double
Bytes -> b
(b ~ Double) => Bytes -> b
The weird part is, (b ~ Double) => Bytes -> b
and Bytes -> Double
are the same type. b ~ Double
just means, "b is Double
". However, that part of the type-checker runs at a different time than instance resolution.
What we really want is "instance local functional dependencies" and that's exactly what this trick accomplishes. See more about functional dependencies here: https://wiki.haskell.org/Functional_dependencies
What the type (b ~ Double) => Bytes -> b
lets us do is "capture" uses of function-typed numerical literals whose inputs are Bytes
and that have a return type whose type is unknown, then we supply a concrete type for the return type with the type equality b ~ Double
after the instance is already resolved.
You can't overlap them:
Duplicate instance declarations:
instance (b ~ Double) => Num (Bytes -> b)
-- Defined at /home/callen/work/units/src/Lib.hs:40:10
instance (b ~ Integer) => Num (Bytes -> b)
-- Defined at /home/callen/work/units/src/Lib.hs:49:10
But if the input types are different, totally kosher:
data Bytes =
B
| KB
| MB
instance (b ~ Double) => Num (Bytes -> b) where
fromInteger i B =
fromInteger i
fromInteger i KB =
fromInteger $ i * 1024
fromInteger i MB =
fromInteger $ i * 1024 * 1024
data BytesI =
Bi
| KBi
| MBi
instance (b ~ Integer) => Num (BytesI -> b) where
fromInteger i Bi =
i
fromInteger i KBi =
i * 1024
fromInteger i MBi =
i * 1024 * 1024
Prelude> 100 B
100.0
Prelude> 100 Bi
100
Prior art
- I have an older application of this trick where I originally explored it for time literals.
- Chris Done has also written about this method
- In the Reddit thread about Chris Done's post some users report where they learned the trick from.
The irony of this method given the "fundeps vs. type families" debate is that instance local functional dependencies are achieved using a mechanism introduced by type families.