Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type-level nats with literals and an injective successor? (N-ary compose)

I'm generalizing this n-ary complement to an n-ary compose, but I'm having trouble making the interface nice. Namely, I can't figure out how to use numeric literals at the type level while still being able to pattern match on successors.

Rolling my own nats

Using roll-my-own nats, I can make n-ary compose work, but I can only pass n as an iterated successor, not as a literal:

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}

module RollMyOwnNats where

import Data.List (genericIndex)

-- import Data.Proxy
data Proxy (n::Nat) = Proxy

----------------------------------------------------------------
-- Stuff that works.

data Nat = Z | S Nat

class Compose (n::Nat) b b' t t' where
  compose :: Proxy n -> (b -> b') -> t -> t'

instance Compose Z b b' b b' where
  compose _ f x = f x

instance Compose n b b' t t' => Compose (S n) b b' (a -> t) (a -> t') where
  compose _ g f x = compose (Proxy::Proxy n) g (f x)

-- Complement a binary relation.
compBinRel :: (a -> a -> Bool) -> (a -> a -> Bool)
compBinRel = compose (Proxy::Proxy (S (S Z))) not

----------------------------------------------------------------
-- Stuff that does not work.

instance Num Nat where
  fromInteger n = iterate S Z `genericIndex` n
-- I now have 'Nat' literals:
myTwo :: Nat
myTwo = 2
-- But GHC thinks my type-level nat literal is a 'GHC.TypeLits.Nat',
-- even when I say otherwise:
compBinRel' :: (a -> a -> Bool) -> (a -> a -> Bool)
compBinRel' = compose (Proxy::Proxy (2::Nat)) not
{-
    Kind mis-match
    An enclosing kind signature specified kind `Nat',
    but `2' has kind `GHC.TypeLits.Nat'
    In an expression type signature: Proxy (2 :: Nat)
    In the first argument of `compose', namely
      `(Proxy :: Proxy (2 :: Nat))'
    In the expression: compose (Proxy :: Proxy (2 :: Nat)) not
-}

Using GHC.TypeLits.Nat

Using GHC.TypeLits.Nat, I get type-level nat literals, but there is no successor constructor that I can find, and using the type function (1 +) doesn't work, because GHC (7.6.3) can't reason about injectivity of type functions:

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}

module UseGHCTypeLitsNats where

import GHC.TypeLits

-- import Data.Proxy
data Proxy (t::Nat) = Proxy

----------------------------------------------------------------
-- Stuff that works.

class Compose (n::Nat) b b' t t' where
  compose :: Proxy n -> (b -> b') -> t -> t'

instance Compose 0 b b' b b' where
  compose _ f x = f x

instance (Compose n b b' t t' , sn ~ (1 + n)) => Compose sn b b' (a -> t) (a -> t') where
  compose _ g f x = compose (Proxy::Proxy n) g (f x)

----------------------------------------------------------------
-- Stuff that does not work.

-- Complement a binary relation.
compBinRel , compBinRel' :: (a -> a -> Bool) -> (a -> a -> Bool)
compBinRel = compose (Proxy::Proxy 2) not
{-
    Couldn't match type `1 + (1 + n)' with `2'
    The type variable `n' is ambiguous
    Possible fix: add a type signature that fixes these type variable(s)
    In the expression: compose (Proxy :: Proxy 2) not
    In an equation for `compBinRel':
        compBinRel = compose (Proxy :: Proxy 2) not
-}
{-
    No instance for (Compose n Bool Bool Bool Bool)
      arising from a use of `compose'
    The type variable `n' is ambiguous
    Possible fix: add a type signature that fixes these type variable(s)
    Note: there is a potential instance available:
      instance Compose 0 b b' b b'
-}
compBinRel' = compose (Proxy::Proxy (1+(1+0))) not
{-
    Couldn't match type `1 + (1 + 0)' with `1 + (1 + n)'
    NB: `+' is a type function, and may not be injective
    The type variable `n' is ambiguous
    Possible fix: add a type signature that fixes these type variable(s)
    Expected type: Proxy (1 + (1 + 0))
      Actual type: Proxy (1 + (1 + n))
    In the first argument of `compose', namely
      `(Proxy :: Proxy (1 + (1 + 0)))'
-}

I agree that semantic editor combinators are more elegant and more general here -- and concretely, it will always be easy enough to write (.) . (.) . ... (n times) instead of compose (Proxy::Proxy n) -- but I'm frustrated that I can't make the n-ary composition work as well as I expected. Also, it seems I would run into similar problems for other uses of GHC.TypeLits.Nat, e.g. when trying to define a type function:

type family   T (n::Nat) :: *
type instance T 0     = ...
type instance T (S n) = ...

UPDATE: Summary and adaptation of the accepted answer

There's a lot of interesting stuff going on in the accepted answer, but the key for me is the Template Haskell trick in the GHC 7.6 solution: that effectively lets me add type-level literals to my GHC 7.6.3 version, which already had injective successors.

Using my types above, I define literals via TH:

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DataKinds #-}

module RollMyOwnLiterals where

import Language.Haskell.TH

data Nat = Z | S Nat

nat :: Integer -> Q Type
nat 0 = [t| Z |]
nat n = [t| S $(nat (n-1)) |]

where I've moved my Nat declaration into the new module to avoid an import loop. I then modify my RollMyOwnNats module:

+import RollMyOwnLiterals
...
-data Nat = Z | S Nat
...
+compBinRel'' :: (a -> a -> Bool) -> (a -> a -> Bool)
+compBinRel'' = compose (Proxy::Proxy $(nat 2)) not
like image 900
ntc2 Avatar asked Dec 28 '13 01:12

ntc2


2 Answers

Unfortunately your question cannot be answered in principle in the currently released version of GHC (GHC 7.6.3) because of a consistency problem pointed out in the recent message http://www.haskell.org/pipermail/haskell-cafe/2013-December/111942.html

Although type-level numerals look like numbers they are not guaranteed to behave like numbers at all (and they don't). I have seen Iavor Diatchki and colleagues have implemented proper type level arithmetic in GHC (which as as sound as the SMT solver used as a back end -- that is, we can trust it). Until that version is released, it is best to avoid type level numeric literals, however cute they may seem.

like image 117
Oleg Avatar answered Oct 22 '22 00:10

Oleg


EDIT: Rewrote answer. It was getting a little bulky (and a little buggy).

GHC 7.6

Since type level Nats are somewhat... incomplete (?) in GHC 7.6, the least verbose way of achieving what you want is a combination of GADTs and type families.

{-# LANGUAGE GADTs, TypeFamilies #-}

module Nats where

-- Type level nats
data Zero
data Succ n

-- Value level nats
data N n f g where
    Z :: N Zero (a -> b) a
    S :: N n f g -> N (Succ n) f (a -> g)

type family Compose n f g
type instance Compose Zero (a -> b) a = b
type instance Compose (Succ n) f (a -> g) = a -> Compose n f g

compose :: N n f g -> f -> g -> Compose n f g
compose Z f x = f x
compose (S n) f g = compose n f . g

The advantage of this particular implementation is that it doesn't use type classes, so applications of compose aren't subject to the monomorphism restriction. For example, compBinRel = compose (S (S Z)) not will type check without type annotations.

We can make this nicer with a little Template Haskell:

{-# LANGUAGE TemplateHaskell #-}

module Nats.TH where

import Language.Haskell.TH

nat :: Integer -> Q Exp
nat 0 = conE 'Z
nat n = appE (conE 'S) (nat (n - 1))

Now we can write compBinRel = compose $(nat 2) not, which is much more pleasant for larger numbers. Some may consider this "cheating", but seeing as we're just implementing a little syntactic sugar, I think it's alright :)

GHC 7.8

The following works on GHC 7.8:

-- A lot more extensions.
{-# LANGUAGE DataKinds, FlexibleContexts, FlexibleInstances, GADTs, MultiParamTypeClasses, PolyKinds, TypeFamilies, TypeOperators, UndecidableInstances #-}

module Nats where

import GHC.TypeLits

data N = Z | S N

data P n = P

type family Index n where
    Index 0 = Z
    Index n = S (Index (n - 1))

-- Compose is defined using Z/S instead of 0, 1, ... in order to avoid overlapping.
class Compose n f r where
    type Return n f r
    type Replace n f r
    compose' :: P n -> (Return n f r -> r) -> f -> Replace n f r

instance Compose Z a b where
    type Return Z a b = a
    type Replace Z a b = b
    compose' _ f x = f x

instance Compose n f r => Compose (S n) (a -> f) r where
    type Return (S n) (a -> f) r = Return n f r
    type Replace (S n) (a -> f) r = a -> Replace n f r
    compose' x f g = compose' (prev x) f . g
      where
        prev :: P (S n) -> P n
        prev P = P

compose :: Compose (Index n) f r => P n -> (Return (Index n) f r -> r) -> f -> Replace (Index n) f r
compose x = compose' (convert x)
  where
    convert :: P n -> P (Index n)
    convert P = P

-- This does not type check without a signature due to the monomorphism restriction.
compBinRel :: (a -> a -> Bool) -> (a -> a -> Bool)
compBinRel = compose (P::P 2) not

-- This is an example where we compose over higher order functions.
-- Think of it as composing (a -> (b -> c)) and ((b -> c) -> c).
-- This will not typecheck without signatures, despite the fact that it has arguments.
-- However, it will if we use the first solution.
appSnd :: b -> (a -> b -> c) -> a -> c
appSnd x f = compose (P::P 1) ($ x) f

However, this implementation has a few downsides, as annotated in the source.

I attempted (and failed) to use closed type families to infer the composition index automatically. It might have been possible to infer higher order functions like this:

-- Given r and f, where f = x1 -> x2 -> ... -> xN -> r, Infer r f returns N.
type family Infer r f where
    Infer r r = Zero
    Infer r (a -> f) = Succ (Infer r f)

However, Infer won't work for higher order functions with polymorphic arguments. For example:

ghci> :kind! forall a b. Infer a (b -> a)
forall a b. Infer a (b -> a) :: *
= forall a b. Infer a (b -> a)

GHC can't expand Infer a (b -> a) because it doesn't perform an occurs check when matching closed family instances. GHC won't match the second case of Infer on the off chance that a and b are instantiated such that a unifies with b -> a.

like image 29
YellPika Avatar answered Oct 21 '22 23:10

YellPika