我目前在进行类型级编程时很有趣。考虑以下版本的链表
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
module ExpLinkedList where
import GHC.TypeLits (Nat, KnownNat , type (-), type (+))
import Data.Proxy (Proxy(..))
import Data.Kind (Type)
import Fcf (TyEq, If, Eval)
data LinkedList (n :: Nat) (a :: Type) where
Nil :: LinkedList 0 a
(:@) :: a -> LinkedList n a -> LinkedList (n + 1) a
infixr 5 :@
someList :: LinkedList 2 String
someList = "test" :@ "list" :@ Nil
extend
sa的函数LinkedList
?例如
extend :: forall m n a . LinkedList n a -> a -> LinkedList (n + m) a
extend vec elem = undefined
example :: LinkedList 5 String
example = extend @3 ("foo" :@ "bar" :@ Nil) "hi"
-- could be: "hi" :@ "hi" :@ "hi" :@ "foo" :@ "bar" :@ Nil
我想出了不同的方法,迟早都会陷入困境……这是其中两种:
在这种方法中,结束条件由重叠的typeclass实例编码
class Extend (b :: Nat) where
ex :: a -> LinkedList n a -> LinkedList (n + b) a
instance {-# OVERLAPPING #-} Extend 0 where
ex _ vec = vec
instance Extend n where
ex a vec = nextEx newVec
-- ^
-- • Couldn't match type ‘(n1 + 1) + (n - 1)’ with ‘n1 + n’
-- Expected type: LinkedList (n1 + n) a
-- Actual type: LinkedList ((n1 + 1) + (n - 1)) a
where
newVec = a :@ vec
nextEx = ex @(n - 1) a
type NextElement (n :: Nat) = Just (n - 1)
class BuildHelper (v :: Maybe Nat) (a :: Type) where
type CNE v a :: Type
buildNext :: Proxy v -> a -> CNE v a
instance BuildHelper 'Nothing a where
type CNE 'Nothing a = LinkedList 0 a
buildNext _ a = Nil
instance BuildHelper ('Just m) a where
type CNE ('Just m) a = LinkedList (m + 1) a
buildNext _ a = a :@ buildNext proxy a
-- ^
-- • Couldn't match expected type ‘LinkedList m a’
-- with actual type ‘CNE
-- (If (TyEq m 0) 'Nothing ('Just (m - 1)))
where
proxy = Proxy @(NextElement m)
用纸和笔进行评估似乎可行
-- buildNext (Proxy @(Just 2) True) :: proxy -> Bool -> Vector 3 Bool
-- = a :@ buildNext @(NextElement 2) a
-- = a :@ buildNext @(Just 1) a
-- = a :@ a :@ buildNext @(NextElement 1) a
-- = a :@ a :@ buildNext @(Just 0) a
-- = a :@ a :@ a :@ buildNext @(NextElement 0) a
-- = a :@ a :@ a :@ buildNext @(Nothing) a
-- = a :@ a :@ a :@ Nil
基本上,GHC无法证明m
匹配(m - 1) + 1
。
这是单例的典型用例。
而且,此解决方案依赖于算术属性,该属性在GHC的类型检查器中本身不可用,但由ghc-typelits-natnormalise插件提供。
具体而言,附加长度索引列表利用了关联性的(+)
:在的情况下m = p + 1
,输出列表中的签名的类型extend
就是LList (n + m) = LList (n + (p + 1))
这需要关联等于LList ((n + p) + 1)
使得构造(:@)
都可以使用。我们还需要可交换除非我们在我们的代码谨慎和证明不混淆1 + p
和p + 1
例如。无论如何,安装该软件包并添加以下行将向GHC教授一些基本的算法:
{-# OPTIONS_GHC -fplugin GHC.TypeLits.Normalise #-} -- from the package ghc-typelits-natnormalise
注意,我们不必在代码中显式地进行任何此类推理;该插件在类型检查期间向编译器提供知识。
该函数extend :: forall n m a. a -> LList m a -> LList (n + m) a
需要查看的值m
来知道a
要插入多少s。我们必须更改其类型extend
以提供必要的运行时信息。单例提供了一个通用的解决方案。具体来说,我们可以定义以下单类型的Nat
种类,其具有类型的值的运行时间表示的特性SNat n
(即,看仅在构造SZ
和SS
)唯一地确定所述指标n
:
data SNat (n :: Nat) where
SZ :: SNat 0
SS :: SNat n -> SNat (1 + n)
extend
然后,想法是将签名从更改为,extend :: forall n. ...
从而extend :: SNat n -> ...
增加Nat
(forall n
)上的量化,并SNat n
使用具有具体运行时表示形式的参数在运行时将其删除。然后可以通过对SNat n
参数进行模式匹配来定义函数:
extend :: SNat n -> a -> LList m a -> LList (n + m) a
extend SZ _ ys = ys
extend (SS n) x ys = x :@ extend n x ys
请注意,如果我们忽略类型,则此定义与extend
使用简单Peano自然的简单列表(未按其长度索引)上的变体相同。该函数extend
是带有索引类型的许多示例之一,这些索引类型只是未索引程序的更精确类型的版本:
-- Peano representation of natural numbers
data PNat where
Z :: PNat
S :: PNat -> PNat
-- Non-indexed variant of extend
extendP :: PNat -> a -> [a] -> [a]
extendP Z _ ys = ys
extendP (S n) x ys = x : extendP n x ys
使用示例extend
:
example :: LList 5 String
example = extend (SS (SS (SS SZ))) "hi" ("foo" :@ "bar" :@ Nil)
我们必须用一元数写数字,这不是很有趣。我们可以使用类型类将Nat
文字转换为SNat
单例值。
SNat
隐式构造class ISNat n where
snat :: SNat n
如您所料,将会有两个实例,0
分别是for和后继。0
显而易见的是:
instance ISNat 0 where
snat = SZ
对于后继者,术语级部分很简单(snat = SS snat
),但是类型需要一些技巧。
instance {-# OVERLAPPABLE #-} (ISNat p, n ~ (1 + p)) => ISNat n where
snat = SS snat
首先,OVERLAPPABLE
。在语法上没有简单的方法将类型参数标识n
为“ not 0
”,因此我们使用一个OVERLAPPABLE
实例。(当重叠不可接受时,还有其他一些方法,但是它们并不那么方便。)遇到ISNat n
约束时,类型检查器将始终选择最具体的实例:如果n
是0
,它将选择该0
实例;如果n
是非零文字,它将为后继者选择此可重叠实例,因为该0
实例不适用,并且如果n
不等于文字(因此它是不受约束的类型变量或某些阻塞类型族应用程序),则0
实例可能适用,我们并不十分了解,因此类型检查器将保守地不选择这两个实例中的任何一个,而是将在其上下文中的其他地方寻找合适的约束,如果找不到任何实例,则会引发编译时错误。
其次,SS
要使其结果类型为形式SNat (1 + p)
。因此,我们添加了一个约束n ~ (1 + p)
。
请注意,要解决该约束(使用时 snat
),GHC将需要猜测p
,natnormalise插件将在此处进行处理;否则,我们也可以添加一个约束p ~ (n - 1)
。
这样,我们最终可以使用Nat
文字更方便地编写示例:
extend (snat :: SNat 3) "hi" ("foo" :@ "bar" :@ Nil)
该snat :: SNat
位似乎有点松脆。留给读者的是练习。
完整要点:https : //gist.github.com/Lysxia/cf0f8ae509d36a11ddf58bfcea8abb89
本文收集自互联网,转载请注明来源。
如有侵权,请联系[email protected] 删除。
我来说两句