我想在Haskell中实现yin-yang puzzle。这是我的尝试(不成功):
-- The data type in use is recursive, so we must have a newtype defined
newtype Cl m = Cl { goOn :: MonadCont m => Cl m -> m (Cl m) }
yinyang :: (MonadIO m, MonadCont m) => m (Cl m)
yinyang = do
yin <- (callCC $ \k -> return (Cl k)) >>= (\c -> liftIO (putStr "@") >> goOn c)
yang <- (callCC $ \k -> return (Cl k)) >>= (\c -> liftIO (putStr "*") >> goOn c)
goOn yin yang
查看类型时,显然callCC $ \k -> return (Cl k)
会提供m (Cl m)
,因此yin
的类型为Cl m
。 yang
是一回事。所以我希望goOn yin yang
给出最终类型m (Cl m)
。
这个实现看起来不错,但问题是它不能编译!这是我得到的错误:
Couldn't match kind `*' against `* -> *'
Kind incompatibility when matching types:
m0 :: * -> *
Cl :: (* -> *) -> *
In the first argument of `goOn', namely `yin'
In a stmt of a 'do' block: goOn yin yang
有什么想法解决这个问题吗?
更新
虽然我自己找到了答案,但我仍然不明白该错误信息的含义。任何人都可以向我解释一下吗?我所知道的是,在有问题的版本中,goOn c
会返回类似Cl m -> m (Cl m)
的内容,而不是预期的m (Cl m)
。但这不是您可以从错误消息中获得的内容。
答案 0 :(得分:7)
代码中有一个愚蠢的错误。这是正确的实现
newtype CFix m = CFix { goOn :: CFix m -> m (CFix m) }
yinyang :: (MonadIO m, MonadCont m) => m (CFix m)
yinyang = do
yin <- (\c -> liftIO (putStr "@") >> return c) =<< (callCC $ \k -> return (CFix k))
yang <- (\c -> liftIO (putStr "*") >> return c) =<< (callCC $ \k -> return (CFix k))
goOn yin yang
这很容易。
main :: IO ()
main = runContT yinyang $ void.return
甚至
main :: IO ()
main = runContT yinyang undefined
虽然后者看起来很可怕,但它是安全的,因为延续永远不会有机会被评估。 (整体表达式将被评估为值_|_
,因为它永远不会停止)
输出预期结果
@*@**@***...
<强>解释强>
最初的尝试是直接翻译Scheme版本
(let* (
(yin
((lambda (cc) (display #\@) cc) (call-with-current-continuation (lambda (c) c))))
(yang
((lambda (cc) (display #\*) cc) (call-with-current-continuation (lambda (c) c)))))
(yin yang))
进入Haskell。对于类型化语言,进行上述类型检查的关键是具有与t
同构的类型t -> t
。在Haskell中,这是通过使用newtype
关键字完成的。另外,为了产生副作用,我们需要IO
,但它不支持callCC
。为了支持以后我们需要MonadCont
。因此,要同时使用MonadIO
和MonadCont
。此外,newtype
必须知道它正在处理哪个Monad
,因此它应将Monad
作为其类型参数。所以现在我们写
newtype CFix m = ...
yinyang :: (MonadIO m, MonadCont m) => m (CFix m)
由于我们在Monad
上徘徊,因此使用do
表示法很方便。因此,let*
作业应转换为yin <-
和yang <-
。在MonadIO
到display
我们使用liftIO.putStr
。 call-with-current-continuation
转换为callCC
,但显然我们无法翻译为id
等。我们暂时离开吧。
我的错误是天真地将显示块和callCC
块的组合运算符转换为>>=
。在Scheme或其他严格语言中,参数将在表达式之前进行求值,因此callCC
块应在显示块之前执行。因此,我们将使用=<<
代替>>=
。代码现在看起来
newtype CFix m = ...
yinyang :: (MonadIO m, MonadCont m) => m (CFix m)
yinyang = do
yin <- (\c -> liftIO (putStr "@") >> return c) =<< (callCC $ ...)
yang <- (\c -> liftIO (putStr "*") >> return c) =<< (callCC $ ...)
...
现在是时候进行类型检查,看看我们应该在...
中添加什么。 callCC
签名是
MonadCont m => ((a -> m b) -> m a) -> m a
所以它的参数有类型
MonadCont m => (a -> m b) -> m a
某些类型a
和b
的。通过查看到目前为止编写的代码,我们可以很容易地得出结论:yin
和yang
具有相同类型的callCC
s返回值m a
。但是,原始架构版本使用yin
和yang
作为函数,因此它们的类型为p -> r
。所以我们需要递归类型和newtype
。
要获得m a
直接方法,请使用return
,我们需要类型为a
的内容。我们假设这是来自我们要定义的类型构造函数。现在要为callCC
提供参数,我们需要从a
构建(a -> m b)
。所以这就是构造函数的样子。但是什么是b
?一个简单的选择是使用相同的a
。所以我们有CFix
:
newtype CFix m = CFix { goOn :: CFix m -> m (CFix m) }
以及callCC
参数的实现
\k -> return (CFix k)
我们使用CFix
构造函数从给定参数构造CFix
,并使用return
将其包装到所需类型。
现在,我们如何使用yin
(类型m (CFix m)
)作为函数?类型析构函数goOn
允许我们提取内部函数,因此我们有最后一个...
的定义。
newtype CFix m = CFix { goOn :: CFix m -> m (CFix m) }
yinyang :: (MonadIO m, MonadCont m) => m (CFix m)
yinyang = do
yin <- (\c -> liftIO (putStr "@") >> return c) =<< (callCC $ \k -> return (CFix k))
yang <- (\c -> liftIO (putStr "*") >> return c) =<< (callCC $ \k -> return (CFix k))
goOn yin yang
这是该计划的最终版本。