【问题标题】:Nice representation of primitive recursive functions in haskellhaskell 中原始递归函数的良好表示
【发布时间】:2015-02-07 21:00:51
【问题描述】:

我在answer to a previous question 中争论说,可以在 Haskell 中表示 primitive recursive functions (PRF) 和 ⊥ 或 undefined 的单个额外值的并集。这个论点是基于对原始递归函数的公理构造的直接翻译;它需要一些语言扩展和关于函数数量的类型级推理。是否可以在更惯用的 Haskell 中表示一组等效的原始递归函数?

理想情况下,PRF 的惯用表示应该能够满足以下所有条件:

  • 提供Category 实例
  • 无需纠结于函数数量的类型级推理

除了原始递归的要求之外

  • 对于输入为undefined 的任何函数对于所有输入都是undefined。这将 PRF 集限制为单个不可避免的额外值 ⊥,而不是包含多个部分递归函数。这意味着 while 循环或类似部分递归函数的任何定义都应该是 undefined

我注意到原始递归函数的公理类似于Category 定律,Arrow 没有arr(实际上它与arr 正好相反),以及仅适用于的有限循环形式自然数。

【问题讨论】:

  • 我看到您在不到一分钟后就回答了自己的问题,而且很好。据我所知,虽然 SO 规则允许,但它看起来仍然很奇怪——该页面看起来更像是一篇博客文章,而不是一个真正的问题(恕我直言)。不过,谢谢你分享这个。
  • @chi 我只针对讨论另一个答案产生的问题这样做,但点击“分享您的知识,问答式”按钮仍然感觉很奇怪。

标签: haskell recursion


【解决方案1】:

Haskell 中有一个非常直接的原始递归函数表示。这是一个newtype,我们将断言它是一个构造正确的原始递归函数。我们不导出构造函数以防止构造可能部分递归的任意函数。这种技术称为smart constructor

module Data.PRF (
    -- We don't export the PRF constructor
    PRF (runPRF),
) where

newtype PRF b c = PRF {runPRF :: b -> c}

我们还需要提供一个接口来构建PRFs。 Category 实例将提供 PRF 所需的扩展组合的组合部分。

import Prelude hiding (id, (.), fst, snd, succ)
import Control.Category

instance Category PRF where
    id = PRF id
    PRF f . PRF g = PRF $ f `seq` g `seq` (f . g)

seqs 要求 fg 在计算任何结果之前处于弱头范式;如果任一函数是undefined,那么组合也将是undefined

原始递归函数也需要投影以从多个参数中选择一个参数。我们将其视为从数据结构中选择一个数据。如果我们使用元组而不是已知长度的列表,则投影函数变为fstsnd。连同Arrow(&&&) 之类的东西来构建元组,我们可以满足扩展投影的所有要求。 PRF 就像“没有arr 的箭头”; arr 将允许将任意部分递归函数制成PRFs。我们将定义 ArrowLike 类别的类。

class Category a => ArrowLike a where
    fst   :: a (b, d) b
    snd   :: a (d, b) b
    (&&&) :: a b c -> a b c' -> a b (c,c')

    first :: a b c -> a (b, d) (c, d)
    first = (*** id)

    second :: a b c -> a (d,b) (d,c)
    second = (id ***)

    (***) :: a b c -> a b' c' -> a (b,b') (c,c')
    f *** g = (f . fst) &&& (g . snd)

投影函数fstsnd 代替了arr。当与扇出(&&&) 结合使用时,它们是描述ArrowLike 行为所需的唯一函数。

在我们为PRF 提供ArrowLike 实例之前,我们将说明(->) 的普通函数与ArrowLike 的区别

import qualified Prelude (fst, snd)

instance ArrowLike (->) where
    fst = Prelude.fst
    snd = Prelude.snd
    f &&& g = \b -> (f b, g b)

对于PRFs,我们将使用我们在(.) 的定义中为Category 实例使用的相同归纳步骤,并要求这两个函数都是弱头范式。

instance ArrowLike PRF where
    fst = PRF fst
    snd = PRF snd
    PRF f &&& PRF g = PRF $ f `seq` g `seq` (f &&& g)

最后,我们将提供原始递归本身。我们将使用元组直接从公理化定义中转换原始递归,而不是增加函数数量。

class ArrowLike a => PrimRec a where
    zero :: a b   Nat
    succ :: a Nat Nat
    prec :: a e c -> a (c, (Nat,e)) c -> a (Nat, e) c

Natdata Nat = Z | S Nat 给出的自然数。我选择将常量函数zero 和后继函数视为原始递归的一部分,唯一可以解构或检查它们构造的Nat 值的方法是使用prec。用const :: c -> a b c 替换zero 很诱人;这将是一个致命的缺陷,因为有人可以将infinity = S infinityconst 一起引入,从而将prec 变成无限循环。

部分递归函数(->) 支持原始递归。

instance PrimRec (->) where
    zero = const Z
    succ = S
    prec f g = go
        where
            go (Z, d) = f d
            go (S n, d) = g (go (n, d), (n, d))

我们将使用与 (.)(&&&) 相同的归纳技巧为 PRF 定义原始递归。

instance PrimRec PRF where
    zero = PRF zero
    succ = PRF succ
    prec (PRF f) (PRF g) = PRF $ f `seq` g `seq` prec f g

原始递归函数是Category,具有构造和解构元组和自然数的能力。

示例

add这样的原始递归函数更容易用这个接口定义。

import Prelude hiding (id, (.), fst, snd, succ)

import Control.Category
import Data.PRF

add :: PrimRec a => a (Nat, Nat) Nat
add = prec id (succ . fst)

我们仍然可以定义有用的函数,例如 match,它有助于构建原始递归函数,该函数根据自然值是否为零进行分支。

match :: PrimRec a => a b c -> a (Nat, b) c -> a (Nat, b) c
match fz fs = prec fz (fs . snd)

使用match,我们可以轻松快速地测试一个值是否为Z,并最终测试它是否为奇数

one :: PrimRec a => a b Nat
one = succ . zero

nonZero :: PrimRec a => a Nat Nat
nonZero = match zero one . (id &&& id)

isZero :: PrimRec a => a Nat Nat
isZero = match one zero . (id &&& id)

isOdd :: PrimRec a => a Nat Nat
isOdd = prec zero (isZero . fst) . (id &&& id)

我们仍然可以编写通常是递归的 Haskell 声明,但是以这种方式构建的所有 PRFs 都将是 undefined

while :: PrimRec a => a s Nat -> a s s -> a s s
while test step = goTest
    where
        goTest = goMatch . (test &&& id)
        goMatch = match id (goStep . snd)
        goStep = goTest . step

这个函数,infiniteLoop,只会在奇数输入时无法终止。

infiniteLoop :: PrimRec a => a Nat Nat
infiniteLoop = while isOdd (succ . succ)

在运行我们的示例时,我们会注意评估顺序,例如 previous answer

import System.IO

mseq :: Monad m => a -> m a
mseq a = a `seq` return a

run :: Show b => PRF a b -> a -> IO ()
run f i = 
    do
        putStrLn "Compiling function"
        hFlush stdout
        f' <- mseq $ runPRF f
        putStrLn "Running function"
        hFlush stdout
        n <- mseq $ f' i
        print n

我们可以评估用match 方便地定义的PRFs。

run isOdd (S $ S $ S Z)

Compiling function
Running function
S Z

infiniteLoop定义的函数一般是undefined,不只是奇数值。

run infiniteLoop (Z)

Compiling function

【讨论】:

  • 这让我想起了Generalized Arrows are Multi Level Languages。非常好!
  • @J.Abrahamson 这是一篇非常有趣的论文,谢谢。 ArrowLike 大约是一个 GArrow,具有重复、复制和交换功能,因此对于简单键入的 kappa 演算,它与 GArrowSTKC 的位置大致相同。他们分享了使用“标准实例Arrow (-&gt;) ...作为恰巧是Haskell子集的来宾语言的评估机制”的方法。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-02-12
  • 1970-01-01
  • 2023-03-20
  • 2016-07-17
  • 2023-04-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多