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

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.