可以"继续"后处理其继续的结果

时间:2017-09-11 15:38:13

标签: haskell monads continuations

Cont r a类型代表一个函数,该函数采用延续a->r并生成类型为r的结果。因此,延续和整个Cont r a都会生成相同类型 r的结果。

我的问题是:两个结果是否必然相同,或者Cont r a可以对延续结果进行后处理并生成不同的值,尽管属于同一类型r

我尝试使用(+1)进行后期处理(请注意+ 1 --<--):

c1 :: Int -> Cont r Int
c1 x = let y = 2*x
       in cont $  \k -> (k y)  + 1 --<--

现在没有进行类型检查,因为我的后处理函数(+1)只接受其类型属于Num类型类的参数。但是,我传递了某些类型(k y)的延续r的结果,该结果不保证属于Num类型类。

无论我对(k y)做什么,它都必须是r->r类型的函数。对所有r执行此操作的唯一功能是id功能,使用id进行后处理根本不需要后处理。

但是,如果我将r限制为Num类型类甚至具体类型Int,那么整个过程就会进行类型检查。然后它产生预期的结果:

*Main> runCont (c1 1) id
3

我很不确定,

  • 如果这样的后处理和限制r的类型是正常的事情,如果是这样,在什么情况下这可能是有用的
  • 或者如果必须将类型变量r读作所有r,并且限制r的类型将导致各种麻烦。

有人可以对此有所了解吗?

2 个答案:

答案 0 :(得分:2)

从技术上讲,我认为没关系。专门Cont r aNum r => Cont r a似乎没有问题比将Reader r a专门化到Num r => Reader r a更加困难。

这样做的一个含义是,生成的CPS计算只能针对产生数字的(最终)延续运行,但这很明显 - 如果你有一个将连续结果后处理为数字的计算,它只能用于产生数字的延续!

作为至少在某种程度上受到制裁的其他证据,请注意有一个功能:

mapCont :: (r -> r) -> Cont r a -> Cont r a

如果要使用此函数而对r没有限制,则第一个参数的唯一有效值为id或未终止的函数,如您所知。

使用c1的{​​{1}}版本可能如下所示:

mapCont

似乎工作正常:

c2 :: (Num r) => Int -> Cont r Int
c2 x = mapCont (+1) $ return (2*x)

至于何时有用,我不确定。我可以想到一些有些蹩脚的应用程序。您可以定义一个覆盖最终结果的计算(假设没有使用其他类型的后处理):

> runCont (c2 10) id
21
> runCont (c2 10) (const 5)
6
> runCont (c2 10) show
... No instance for (Num String) arising from a use of 'c2' ...

用作:

override x = cont (const x)

或模拟编写器以添加日志功能的计算转换器:

> runCont (return 2 >>= \x -> cont (\f -> f (x*3))) id
6
> runCont (return 2 >> override 1000 >>= \x -> cont (\f -> f (x*3))) id
1000
>

您可以这样使用:

annotate note comp = mapCont (\(a, w) -> (a, note:w)) comp

得到以下特性:

runCont (annotate "two" (return 2)
        >>= \x -> annotate "times three" (cont (\f -> f (x*3))))
   (\a -> (a, []))

但这些似乎不是非常引人注目的应用程序。

答案 1 :(得分:0)

@KABuhr已经表明,在普通Cont中进行后处理是可行的,但是没有找到“非常引人注目的应用程序”。我将向您展示后处理 的用处,但是只有在概括Cont时,它才最有效。首先,介绍一些标题(大多数用于示例中):

{-# LANGUAGE RebindableSyntax #-}

import Prelude(Num(..), Eq(..), Enum(..))
import Data.Bool
import Data.Function
import Data.Functor.Identity
import Data.List
import Data.Maybe
import Data.Tuple

import Control.Lens(_1, _2, traversed)

现在,一个广义的Cont

newtype Cont r f a = Cont { runCont :: (a -> r) -> f }

您的问题是“ Cont是否允许进行后处理?”答案是肯定的。如果不希望这样,可以使用newtype ContS a = { runContS :: forall r. (a -> r) -> r },这是完全不允许的。实际上,ContS aa同构。我刚刚定义的Cont处于相反的位置:甚至允许更改类型的后处理器。我们可以定义标准的Functor ial (<$>)

infixl 1 <$>
(<$>) :: (a -> b) -> Cont r f a -> Cont r f b
f <$> Cont x = Cont $ \cont -> x $ \realX -> cont (f realX)

在继续之前,让我们了解Cont背后的隐喻。 Cont r f a是可以产生a的计算。它会给您a,但会要求您产生r。完成后,将生成f个。有点像(r -> f, a),但使用上有严格的限制。如果我们尝试定义一个Applicative-ish运算符,则会看到一些有趣的东西。

infixl 1 <*> 
(<*>) :: Cont m f (a -> b) -> Cont r m a -> Cont r f b
Cont f <*> Cont x = Cont $ \cont -> x $ \realX -> f $ \realF -> cont (realF realX)

(<*>)可以一次执行两项操作。它正在将a -> b应用于a以得到b,但是它也将m -> fr -> m方面组成了r -> f部分。但是,(<*>)的类型不再适合常规的Applicative格式。这就是为什么我们使用Cont r a而不是Cont r f a的原因。前者的功能较弱,但是适合我们现有的框架。为了使我们的Cont工作,我们必须抛弃一些已建立的基础架构。

在进入RebindableSyntax级内容之前,有一些用法。

complete :: Cont a f a -> f
complete (Cont x) = x id

amb :: [a] -> Cont (Maybe b) (Maybe (a, b)) a
amb [] = Cont (const Nothing)
amb (x : xs) = Cont $ \test -> case test x of
                                    Nothing -> runCont (amb xs) test
                                    Just y -> Just (x, y)
poly :: Num a => a -> a -> a -> a
poly x y z = sq x * y + sq y + z + sq z * x
    where sq x = x * x
solution :: (Num a, Enum a, Eq a) => Maybe (a, (a, (a, ())))
solution = complete $ testRoot <$> amb [-5..5]
                               <*> amb [-10 .. -5]
                               <*> amb [5..10]
    where testRoot x y z = case poly x y z of
                                0 -> Just ()
                                _ -> Nothing

complete在实际上没有阻碍的情况下完成了计算。 amb取一个[a],并逐个遍历每个a。它将每个传递到test中,并进行搜索,直到找到成功的一个。它以两种方式对test的结果进行后处理。它将结果重置为Just(或放弃),然后将Just结果与构建它的输入配对。

solution中,complete界定了传递给amb的延续的程度。每个amb都被传递给它和complete之间的代码。例如,赋予amb [-5..5]的延续是\x -> testRoot x <*> amb [-10 .. -5] <*> amb [10..5]。这种连续样式称为shift / resetContshiftcompletereset。这个想法是amb [-5..5]是“骗子”;它“看起来像” Num a => a,因为它已传递给testRoot,但实际上它是一个控制结构,可以将其周围的所有内容全部翻过来。与普通Cont r a相比,Cont中允许的控制结构功能更强大。

现在,这是我们RebindableSyntax所需要的:

(=<<) :: (a -> Cont r m b) -> Cont m f a -> Cont r f b
f =<< Cont x = Cont $ \cont -> x $ \realX -> runCont (f realX) cont
(>>=) = flip (=<<)
return :: a -> Cont r r a
return x = Cont ($ x)

(=<<)Monad样式的函数应用程序运算符。同样,我们的版本不适合通常的类型。使用(>>=)returndo-标记现在已重新定义为可与Cont一起使用。您可以返回并以solution表示法重写do,以查看它是否有效。

我们真的要出去了。功能性光学器件背后的想法是,数据结构产生了“变压器变压器”。例如。 Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t在“较小”结构ab之间采用变压器,并在“较大” st之间制造变压器。看看距离flip就在哪里...

editing :: ((a -> Identity b) -> s -> Identity t) -> s -> Cont b t a
editing optic x = Cont (runIdentity . flip optic x . (Identity .))

editing作为控制结构,引用了结构内部的字段(要在其上使用的结构),然后使用“程序的其余部分”对该结构进行了变异。使用它,您可以编写以下内容:

example :: (a -> a) -> [(Bool, (a, a))] -> [(Bool, (a, a))]
example f xs = complete $ do x <- editing traversed xs
                             n2 <- editing _2 x
                             n <- case fst x of
                                       True -> editing _1 n2
                                       False -> editing _2 n2
                             return (f n)

我希望,即使有这些人为的例子,您也相信后处理在Cont中很有用。这样做没有错。但是,如果要充分利用它,则必须突破现有的ApplicativeMonad形式。这很痛苦,因此我们削弱了Cont使其适合,从而禁用了改变类型的后处理作为一种折衷方案。