有没有办法在Haskell中表示原始递归函数(PRF)的指称语义?
答案 0 :(得分:5)
排序-的。我们可以使用Haskell类或GADT对原始递归函数进行编码。然后我们可以认为原始递归函数是数据类型的等价类。最简单的等价是关于PRF解释的Haskell外延语义。由于Haskell的指称语义,这种表示最终将是不精确的,但让我们探讨我们能够接近的程度。
我们将使用definition of primitive recursive functions from Wikipedia。 PRF a
是具有arity a的原始递归函数,其中a是自然数。
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE GADTs #-}
data PRF (a :: Nat) where
Const :: PRF 'Z
Succ :: PRF ('S 'Z)
Proj :: BNat n -> PRF n
Comp :: PRF k -> List k (PRF m) -> PRF m
PRec :: PRF k -> PRF (S (S k)) -> PRF (S k)
Const
构造常量函数或arity零,它始终返回0. Succ
是arity one的后继函数。 Proj
构造了一系列投影函数,每个函数在跳过提供的参数数量后都会选出一个参数。 Comp
用一个提供其参数的其他函数列表组成一个函数。 PRec
构建一个模式匹配第一个参数的函数。如果第一个参数为零,则PRec
将第一个函数应用于其余参数。如果第一个参数不为零,它将第一个参数的前一个作为第一个参数递归到自身,并返回应用于第一个参数的前一个的第二个函数的结果,递归的结果,以及其余的参数。从PRF
到Haskell函数的编译器定义中更容易看到。
compile :: PRF n -> List n Nat -> Nat
compile Const = const Z
compile Succ = \(Cons n Nil) -> S n
compile (Proj n) = go n
where
go :: BNat n -> List n a -> a
go BZero (Cons h _) = h
go (BSucc n) (Cons _ t) = go n t
compile (Comp f gs) = \ns -> f' . fmap ($ ns) $ gs'
where
gs' = fmap compile gs
f' = compile f
compile (PRec f g) = h
where
h (Cons Z t) = f' t
h (Cons (S n) t) = g' (Cons n (Cons (h (Cons n t)) t))
f' = compile f
g' = compile g
以上要求自然数Nat
的定义,由类型级自然数BNat
限定的自然数,以及类型级已知长度{{1 }}
List
我们现在有能力编写我们的第一个原始递归函数。我们将编写两个用于标识和添加的示例。
import qualified Data.Foldable as Foldable
import System.IO
data Nat = Z | S Nat
deriving (Eq, Show, Read, Ord)
data List (n :: Nat) a where
Nil :: List 'Z a
Cons :: a -> List n a -> List ('S n) a
instance Functor (List n) where
fmap f Nil = Nil
fmap f (Cons h t) = Cons (f h) (fmap f t)
-- A natural number in the range [0, n-1]
data BNat (n :: Nat) where
BZero :: BNat ('S n)
BSucc :: BNat n -> BNat ('S n)
请注意,我们在Haskell中重用了声明来简化这些函数的编写;我们在ident :: PRF (S Z)
ident = Proj BZero
add :: PRF (S (S Z))
add = PRec ident (Comp Succ (Cons (Proj (BSucc BZero)) Nil))
的定义中重复使用了ident
。最终,使用Haskell声明的能力将允许我们创建无限或非完整的递归结构,我们可以潜入add
类型。
我们可以编写一些示例代码来试用我们的PRF
函数。我们对add
和seq
的评估顺序有点偏执,以便我们可以看到我们的表示在以后有多么错误。
hFlush
如果我们使用mseq :: Monad m => a -> m a
mseq a = a `seq` return a
runPRF :: PRF n -> List n Nat -> IO ()
runPRF f i =
do
putStrLn "Compiling function"
hFlush stdout
f' <- mseq $ compile f
putStrLn "Running function"
hFlush stdout
n <- mseq $ f' i
print n
运行示例,我们会得到一个很好的,令人满意的输出
add
我们可以使用Haskell声明做一些有趣且最终具有破坏性的事情。首先,我们将使模式匹配更容易。能够使用runPRF add (Cons (S (S Z)) (Cons (S (S (S Z))) Nil))
Compiling function
Running function
S (S (S (S (S Z))))
中的模式匹配而不提供使用递归结果的函数是很好的。 PRec
将为我们添加额外的伪参数。
match
要做到这一点,它需要一个辅助函数,它添加参数match :: (Depths List k) => PRF k -> PRF (S k) -> PRF (S k)
match fz fs = PRec fz (addArgument (BSucc BZero) fs)
和一些其他实用程序来测量具有已知类型的列表的大小addArgument
,比较和转换{{ 1}} s,并证明递增的自然数仍然在新的界限之内。
Depths
在编写完全合理的内容(例如
)时,这非常有用BNat
我们还可以编写非常具有破坏性的事情,而不仅仅是{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE StandaloneDeriving #-}
class Depths f (n :: Nat) where
depths :: f n (BNat n)
instance Depths List 'Z where
depths = Nil
instance (Depths List n) => Depths List ('S n) where
depths = Cons BZero (fmap BSucc depths)
deriving instance Eq (BNat n)
deriving instance Show (BNat n)
deriving instance Ord (BNat n)
bid :: BNat n -> BNat (S n)
bid BZero = BZero
bid (BSucc x) = BSucc (bid x)
addArgument :: (Depths List k) => BNat (S k) -> PRF k -> PRF (S k)
addArgument n f = Comp f . fmap p $ depths
where
p d =
if d' >= n
then Proj (BSucc d)
else Proj d'
where d' = bid d
。首先,我们使用递归定义{{1}}构造。我们知道用nonZero :: PRF (S Z)
nonZero = match Const (Comp Succ (Cons (Comp Const Nil) Nil))
isZero :: PRF (S Z)
isZero = match (Comp Succ (Cons Const Nil)) (Comp Const Nil)
isOdd :: PRF (S Z)
isOdd = PRec Const (addArgument BZero isZero)
构建的东西不应该存在于原始递归函数的闭包中。
undefined
这使我们可以编写一个仅针对某些输入进行非终止的循环。
while
如果我们为偶数(例如while
或while :: (Depths List k) => PRF (S k) -> PRF (S k) -> PRF (S k)
while test step = goTest
where
--goTest :: PRF (S k)
goTest = Comp goMatch (Cons test (fmap Proj depths))
--goMatch :: PRF (S (S k))
goMatch = match (Proj BZero) (addArgument BZero goStep)
--goStep :: PRF (S k)
goStep = Comp goTest (Cons step (fmap (Proj . BSucc) depths))
)运行此操作,则会终止返回输入。如果我们以奇数运行它,它永远不会完成。
infiniteLoop :: PRF (S Z)
infiniteLoop = while isOdd (Comp Succ (Cons Succ Nil))
因为我们对Z
和S (S Z)
小心谨慎,所以我们可以确定编译后的值是以星期正常形式存在的,这种形式不是原始的递归函数,并且不是#39}。 ; t只是runPRF infiniteLoop (Cons (S Z) Nil)
Compiling function
Running function
。这是因为seq
步骤并不严格,减少到星期正常形式并没有导致减少一直到正常形式。我们可以通过将hFlush
添加到undefined
来解决此问题。我只改变了需要它的两种模式。
compile
这基本上会在编译时检查seq
是否有限。
compile
我们所讨论的所有类型都没有真正代表一对一的原始递归函数。 compile (Comp f gs) = f' `seq` gs' `seq` go
where
go = \ns -> f' . fmap ($ ns) $ gs'
gs' = fmap compile gs
f' = compile f
compile (PRec f g) = f' `seq` g' `seq` h
where
h (Cons Z t) = f' t
h (Cons (S n) t) = g' (Cons n (Cons (h (Cons n t)) t))
f' = compile f
g' = compile g
除了上面定义的递归结构和PRF
之外还有其他内容。它也存在于相同原始递归函数的多个表示中。例如,身份函数具有其他定义,包括具有后继函数的前趋函数(我没有定义)的组成。编译的结果runPRF infiniteLoop (Cons Z Nil)
Compiling function
GHC stack-space overflow: current limit is 33632 bytes.
Use the `-K<size>' option to increase it.
由任何具有相同类型的Haskell函数居住,其中也包括所有部分递归函数。
为了隐藏同一个函数的多个表示,我们可以使用与Haskell相同的技巧:隐藏函数的内部。如果有人可以检查PRF a
的唯一方法是严格编译它并将其应用于某些东西,那么没有人能够区分出相同的原始递归函数之间的区别。
将我们的GADT转换为类型类,只导出类和undefined
就足以隐藏构造函数。
List n Nat -> Nat
定律,PRF
没有compile
,可以找到要导出的 Another interface (实际上它与Category
相反),以及仅适用于自然数的有限形式的循环。
这足以让你相信这几乎是可能的。无论我们做什么,我们仍然会被额外的居民所困扰,Arrow
。关于how to make it nice would belong to a different question的进一步讨论,包括对它应该如何好的具体问题。