【问题标题】:Is that possible to implement a stack with lambda expressions only?是否可以仅使用 lambda 表达式实现堆栈?
【发布时间】:2014-10-28 05:40:55
【问题描述】:

这可能不是一个很实际的问题,我只是好奇我是否可以只用 lambda 表达式实现一个堆栈。

一个栈支持 3 种操作:toppoppush,所以我首先将栈定义为 3 元组:

data Stack a = Stack a (a -> Stack a) (Stack a)
             | Empty

这里Empty 代表空栈,所以我们至少有一个居民开始。

在这个定义下,除了push 操作之外,一切看起来都不错:

import Control.Monad.State
import Control.Monad.Writer
import Data.Maybe

data Stack a = Stack a (a -> Stack a) (Stack a)
             | Empty

safePop :: Stack a -> Maybe (Stack a)
safePop Empty = Nothing
safePop (Stack _ _ s) = Just s

safeTop :: Stack a -> Maybe a
safeTop Empty = Nothing
safeTop (Stack x _ _) = Just x

push :: a -> Stack a -> Stack a
push x s = _

stackManip :: StateT (Stack Int) (Writer [Int]) ()
stackManip = do
    let doPush x = modify (push x)
        doPop    = do
            x <- gets safeTop
            lift . tell . maybeToList $ x
            modify (fromJust . safePop)
            return x
    doPush 1
    void doPop
    doPush 2
    doPush 3
    void doPop
    void doPop

main :: IO ()
main = print (execWriter (execStateT stackManip Empty))

所以当我完成代码时,我应该能够运行它并得到类似[1,3,2]

但是,我发现自己无限扩展了push 的定义:

push 应该构造一个新堆栈,第一个元素是刚刚压入堆栈的项目,第三个元素是当前堆栈:

push :: a -> Stack a -> Stack a
push x s = Stack x _ s

为了填补这个洞,我们需要创建堆栈,所以我需要一个 let 表达式:

push :: a -> Stack a -> Stack a
push x s = let s1 = Stack x (\x1 -> Stack x1 _ s1) s
           in s1

为了填补新的漏洞,我需要另一个 let 表达式:

push :: a -> Stack a -> Stack a
push x s = let s1 = Stack x (\x1 ->
                             let s2 = Stack x1 _ s1
                             in s2) s
           in s1

所以你可以看到我的push 定义中总是有一个漏洞,但是我将其展开。

我有点理解Data.Function.fix 背后的魔力,并且猜想在这里可以应用一些类似的魔力,但无法弄清楚。

我想知道

  • 这可能吗?
  • 如果答案是肯定的,那么它背后的魔力是什么?

【问题讨论】:

  • 请注意,您的 doPop 函数实际上并没有改变状态,所以它更像是一个 doPeek
  • 您也可以只使用[a]pop = tailpush = (:)top = head
  • @Cactus doPop 已修复,我专注于 push 并且没有机会检查我的 stackManip 是否有效。而我只是想通过不使用列表来实现堆栈来获得乐趣。

标签: haskell lambda-calculus


【解决方案1】:

您可以完全使用具有 Church 编码的函数类型来实现它:

{-# LANGUAGE Rank2Types #-}

newtype Stack a = Stack (forall r. (a -> Stack a -> r) -> r -> r)

cons :: a -> Stack a -> Stack a
cons x (Stack f) = Stack (\g nil -> _)

peek :: Stack a -> Maybe a
peek (Stack f) = f (\x _ -> Just x) Nothing

这表示Stack 是一个函数,它接受一个函数,该函数将栈顶元素和堆栈的其余部分作为其参数。 Stack 函数的第二个参数是在堆栈为空时使用的默认值。我实现了peek 函数,但我留下了cons,其余的作为练习(如果你需要更多帮助,请告诉我。另外,你留下我在cons 中输入的下划线,GHC 会告诉你它是什么类型期望并列出一些可能相关的绑定)。

rank-2 类型是说,给定一个Stack a,我们可以给它一个返回任何类型值的函数,不受a 类型变量的约束。这很方便,因为我们可能不想使用相同的类型。考虑一堆列表,我们想使用Stack 中的函数来获取顶部元素的长度。更重要的是,它表示像 cons 这样的函数不能以任何方式操纵结果。它必须返回它从函数中获得的r 类型值(或从默认值,如果堆栈为空),保持不变。

另一个很好的练习是实现toList :: Stack a -&gt; [a]fromList :: [a] -&gt; Stack a 并证明这两个函数形成同构(意味着它们是互为逆的)。

事实上,据我所知,所有 Haskell 数据类型都具有 Church 编码的表示形式。您可以在这个Stack 类型中看到三种组合类型(求和类型、乘积类型和“类型递归”)的基本方式。

【讨论】:

  • 感谢您的回答! Stack 的类型签名看起来介于 foldrfoldMap 之间,也许我也可以将其设为 Foldable 的实例,明天试试。顺便说一句,为什么这被称为“教堂编码”?
  • @Javran 它以 Alonzo Church 的名字命名,他是第一个使用这种技术在 lambda 演算中编码数据结构的人。 Stack a[a] 同构,因此适用于 [a] 的任何内容都应适用于 Stack a(反之亦然)。
  • 我明白了,我错误地认为 Chunrch 编码和 Church 数字是一回事。现在我明白了。
  • 教会数字是data N = Z | S Z的教会编码:)
  • 这是我的回答:answer 有点理解 Church 编码的想法。
【解决方案2】:

push 的结果正是你想要保持pushing 的结果,所以你可以这样打结:

push :: a -> Stack a -> Stack a
push x s = let s' = Stack x (flip push s') s in s'

如果你想通过Data.Function.fix打结,你可以把上面的定义改成这样:

push :: a -> Stack a -> Stack a
push x s = fix $ \s' -> Stack x (flip push s') s

【讨论】:

  • 我没有意识到我可以递归使用push,感觉很愚蠢。
猜你喜欢
  • 1970-01-01
  • 2020-08-30
  • 2017-04-19
  • 2014-09-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-04-30
  • 1970-01-01
相关资源
最近更新 更多