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
的类型将导致各种麻烦。 有人可以对此有所了解吗?
答案 0 :(得分:2)
从技术上讲,我认为没关系。专门Cont r a
到Num 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 a
与a
同构。我刚刚定义的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 -> f
和r -> 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
/ reset
。 Cont
是shift
,complete
是reset
。这个想法是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
样式的函数应用程序运算符。同样,我们的版本不适合通常的类型。使用(>>=)
和return
,do
-标记现在已重新定义为可与Cont
一起使用。您可以返回并以solution
表示法重写do
,以查看它是否有效。
我们真的要出去了。功能性光学器件背后的想法是,数据结构产生了“变压器变压器”。例如。 Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
在“较小”结构a
和b
之间采用变压器,并在“较大” s
和t
之间制造变压器。看看距离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
中很有用。这样做没有错。但是,如果要充分利用它,则必须突破现有的Applicative
和Monad
形式。这很痛苦,因此我们削弱了Cont
使其适合,从而禁用了改变类型的后处理作为一种折衷方案。