Skip to content

Add type-level natural numbers #2899

@no-longer-on-githu-b

Description

@no-longer-on-githu-b

Currently various encodings of type-level natural numbers are in use, which both have downsides in terms of performance:

  1. Peano encoding quickly results in huge types, and reflection requires O(n) time.
  2. Base-10 encoding results in smaller types, but still requires O(log_10 n) time.

They also don't result in the prettiest types, such as S (S (S (S Z))) for 4 or D4 :* D2 for 42. It would be nice to have type-level natural numbers with nice syntax and better performance, both at compile-time and at runtime. Similar to GHC.TypeLits.

Natural number types

  • Add a new kind to Prim called Nat.
  • Allow nonnegative integer literals in types. Each distinct such literal is a distinct type. The kind is Prim.Nat.

Reflection and arithmetic library

Add a new PureScript library to core with the following module:

module Data.Nat where

import Data.Int as Int
import Prelude

class IsNat (n :: Nat) where
  reflectNat :: proxy n -> Int

instance isNatMagic :: IsNat <magic> where
  reflectNat = <magic>

foreign import data Add :: Nat -> Nat -> Nat
foreign import data Sub :: Nat -> Nat -> Nat
foreign import data Mul :: Nat -> Nat -> Nat
foreign import data Div :: Nat -> Nat -> Nat
foreign import data Pow :: Nat -> Nat -> Nat

instance isNatAdd :: (IsNat a, IsNat b) => IsNat (Add a b) where
  reflectNat _ = reflectNat @a + reflectNat @b

instance isNatSub :: (IsNat a, IsNat b) => IsNat (Sub a b) where
  reflectNat _ = clamp 0 top $ reflectNat @a - reflectNat @b

instance isNatMul :: (IsNat a, IsNat b) => IsNat (Mul a b) where
  reflectNat _ = reflectNat @a * reflectNat @b

instance isNatDiv :: (IsNat a, IsNat b) => IsNat (Div a b) where
  reflectNat _ = reflectNat @a / reflectNat @b

instance isNatPow :: (IsNat a, IsNat b) => IsNat (Pow a b) where
  reflectNat _ = Int.pow (reflectNat @a) (reflectNat @b)

infixl 6 type Add as +
infixl 6 type Sub as -
infixl 7 type Mul as *
infixl 7 type Div as /

Where <magic> means "any non-negative type-level integer literal".

Example

module Data.Vect
  ( Vect
  , empty
  , cons
  , uncons
  , append
  ) where

newtype Vect (n :: Nat) a = Vect (List a)

empty :: forall a. Vect 0 a
empty = Vect Nil

cons :: forall a n. a -> Vect n a -> Vect (n + 1) a
cons a (Vect b) = Vect (a : b)

uncons :: forall a n. Vect (n + 1) a -> {head :: a, tail :: Vect n a}
uncons (Vect a) =
  let {head, tail} = unsafePartial fromJust $ List.uncons a
  in {head, tail: Vect tail}

append :: forall a n m. Vect n a -> Vect m a -> Vect (n + m) a
append (Vect a) (Vect b) = Vect (a <> b)

Open issues

  • How does reflectNat deal with integer overflow?
  • How does isNatSub deal with b > a?
  • How does isNatDiv deal with division by zero?
  • Logic languages such as Mercury are aware of arithmetic and unify accordingly.
    • Should unification be aware of the associativity and commutativity of addition and multiplication?
    • Should unification perform constant folding?
  • We can bikeshed elsewhere about the exact names of modules, types, and kinds.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions