如何从Haskell中的内部参数中提取信息?

时间:2013-08-13 03:23:11

标签: haskell

在大多数支持可变变量的编程语言中,可以轻松实现类似 Java 的例子:

interface Accepter<T> {
    void accept(T t);
}

<T> T getFromDoubleAccepter(Accepter<Accepter<T>> acc){
    final List<T> l = new ArrayList<T>();
    acc.accept(new Accepter<T>(){

        @Override
        public void accept(T t) {
            l.add(t);
        }

    });
    return l.get(0); //Not being called? Exception!
}

对于那些不了解Java的人来说,上面的代码收到的东西可以提供一个带有一个参数的函数,它应该把这个参数作为最终结果。

这与callCC不同:没有控制流交替。仅涉及内部函数的参数。

我认为 Haskell 中的等效类型签名应为

getFromDoubleAccepter :: (forall b. (a -> b) -> b) -> a

因此,如果有人可以为您选择的某种类型提供函数(a -> b) -> b,那么他必须已经拥有a。所以你的工作就是给他们一个“回调”,而不是保留他们发送给你的任何东西,一旦他们回到你身边,将那个值返回给你的来电者。

但我不知道如何实现这一点。我能想到几种可能的解决方案。虽然我不知道它们中的每一个是如何工作的,但我可以根据预期的困难对它们进行评级和排序:

  • ContContT monad。我认为这是最简单的。

  • RWS monad或类似。

  • 任何其他monad。 monad,如Maybe我认为更难。

  • 仅使用标准的功能特性,如延迟评估,模式匹配,定点污染等。我认为最难(甚至不可能)。

    < / LI>

我希望看到使用上述任何技术的答案(并且更喜欢更难的方法)。

注意:不应对类型签名进行任何修改,解决方案应该与 Java 代码相同。

更新

有一次我看到有人评论getFromDoubleAccepter f = f id我意识到我犯了错误。基本上我使用forall只是为了让游戏更容易,但看起来这种扭曲太容易了。实际上,上面的类型签名强制调用者传回我们给他们的任何内容,因此如果我们选择a作为b,那么该实现会给出相同的预期结果,但它只是......没想到。

实际上,我想到的是类型签名,如:

getFromDoubleAccepter :: ((a -> ()) -> ()) -> a

而这一次更难。

另一位评论作者要求推理。让我们看一下类似的函数

getFunctionFromAccepter :: (((a -> b) -> b) -> b) -> a -> b

这个有一个天真的解决方案:

getFunctionFromAccepter f = \a -> f $ \x -> x a

但是在下面的测试代码中它在第三个测试中失败了:

exeMain = do
    print $ getFunctionFromAccepter (\f -> f (\x -> 10)) "Example 1" -- 10
    print $ getFunctionFromAccepter (\f -> 20) "Example 2" -- 20
    print $ getFunctionFromAccepter (\f -> 10 + f (\x -> 30)) "Example 3" --40, should be 30

在失败的情况下,我们传递一个返回30的函数,我们期望得到该函数。但最终结果依次为40,因此失败。有没有办法实现只是我想要的东西?

如果可以在Haskell中完成,那么会有很多有趣的序列。例如,元组(或其他“代数”类型)也可以定义为函数,因为我们可以说type (a,b) = (a->b->())->()之类的内容,并在此方面实现fstsnd。这就是我在其他几种语言中使用的方式,这些语言没有本地“元组”支持,但具有“闭包”功能。

3 个答案:

答案 0 :(得分:10)

accept的类型为void accept(T),因此等效的Haskell类型为t -> IO ()(因为Java中的每个函数基本上都是IO)。因此getFromDoubleAccepted可以直接翻译为

import Data.IORef

type Accepter t = t -> IO ()

getFromDoubleAccepter :: Accepter (Accepter a) -> IO a
getFromDoubleAccepter acc = do
    l <- newIORef $ error "Not called"
    acc $ writeIORef l
    readIORef l

如果您想在Haskell中使用惯用的非IO解决方案,除了尝试模仿某些Java模式之外,还需要更具体地了解您的实际最终目标。

编辑:关于更新

getFromDoubleAccepter :: ((a -> ()) -> ()) -> a

对不起,但这个签名绝不等同于Java版本。你所说的是对于任何a,给定一个函数,该函数接受一个a但不返回任何或做任何副作用的函数,你想以某种方式让人联想到a类型的值。满足给定签名的唯一实现基本上是:

getFromDoubleAccepter :: ((a -> ()) -> ()) -> a
getFromDoubleAccepter f = getFromDoubleAccepter f

答案 1 :(得分:7)

首先,我会尽可能地音译。我将这些计算提升为monad,因为accept返回void(在Haskell-land中读取()),除非有一些影响,否则这是没用的。

type Accepter m t = t -> m ()

getFromDoubleAccepter :: (MonadSomething m) => Accepter m (Accepter m t) -> m t
getFromDoubleAccepter acc = do
    l <- {- new mutable list -}
    acc $ \t -> add l t
    return (head l)

当然,我们不能制作这样的可变列表,所以我们必须在这里使用一些直观的火花。当一个动作只是向某个累加器添加一个元素时,我会想到Writer monad。所以也许那条线应该是:

    acc $ \t -> tell [t]

由于你只是在最后返回列表的头部而没有任何效果,我认为签名应该成为:

getFromDoubleAccepter :: Accepter M (Accepter M t) -> t

其中M是合适的monad。它需要能够写[t] s,这样才能给我们:

type M t = Writer [t]

getFromDoubleAccepter :: Accepter (M t) (Accepter (M t) t) -> t

现在这个函数的类型告诉我们如何编写其余部分:

getFromDoubleAccepter acc = 
    head . execWriter . acc $ \t -> tell [t]

我们可以检查它是否有所作为......

ghci> getFromDoubleAccepter $ \acc -> acc 42
42

所以这似乎是正确的,我想。我还不清楚这段代码应该是什么意思。

类型签名中的显式M t对我来说有点美观。如果我知道我解决了什么问题,我会仔细看看。如果你的意思是参数可以是一系列命令,但是没有可用的计算功能,那么你可以将类型签名专门化为:

getFromDoubleAccepter :: (forall m. (Monad m) => Accepter m (Accepter m t)) -> t

仍然适用于我们的示例。当然,这有点傻。考虑

   forall m. (Monad m) => Accepter m (Accepter m t))
=  forall m. (Monad m) => (t -> m ()) -> m ()

此类型的函数唯一能做的就是按顺序调用其各种t的参数,然后返回()。这些函数中的信息由t完全表征[1],因此我们可以很容易地使用

getFromDoubleAccepter :: [t] -> t
getFromDoubleAccepter = head

[1]只要我一无所获,我不妨说在面对无限时这不太准确。计算

crazy :: Integer -> Accepter m (Accepter m Integer)
crazy n acc = crazy (n+1) >> acc n

可用于形成无限序列

... >> acc 3 >> acc 2 >> acc 1 >> acc 0

没有第一个元素。如果我们试图将其解释为列表,那么在尝试查找第一个元素时,我们会得到无限循环。然而,这个计算比无限循环有更多的信息 - 如果不是列表,我们使用Last monoid来解释它,我们将能够提取0。真的是

forall m. (Monad m) => Accepter m (Accepter m t)

与某些比列表更通用的东西是同构的;特别是一个免费的幺半群。

答案 2 :(得分:0)

感谢上述答案,我最后得出结论,在Haskell中,我们可以做一些与其他语言不同的事情。

实际上,这篇文章的动机是翻译着名的“single axiom classical logic reduction system”。我用其他一些语言实现了这个。

实现应该没问题
Axiom: (a|(b|c)) | ((d|(d|d)) | ((e|b) | ((a|e) | (a|e))))

但是,由于缩小规则看起来像

Rule: a|(b|c), a |-- c

有必要提取内部参数作为最终结果。在其他语言中,这是通过使用像可变插槽这样的副作用来完成的。但是,在Haskell中,我们没有可变的插槽,涉及IO会很难看,所以我一直在寻找解决方案。

乍一看(在我的问题中显示),getFromDoubleAccepter f = f id似乎是无稽之谈,但我意识到它实际上在这种情况下有效!例如:

rule :: (forall r.a -> (b -> c -> r) -> r) -> a -> c
rule abc a = abc a $ flip const

诀窍仍然是相同的:因为存在性限定从调用者隐藏r,并且由被调用者为其获取类型,我们可以指定c为{ {1}},所以我们只需应用给定的函数来获得结果。另一方面,给定的函数必须使用我们的输入来产生最终答案,因此它有效地将实现限制为我们真正想要的!

将它们放在一起,让我们看看我们能用它做些什么:

r

这里我使用命名约定来跟踪类型签名:变量名称由“有效”类型变量组合(意味着它不是结果类型 - 所有newtype I r a b = I { runI :: a -> b -> r } rule :: (forall r. I r a (I r b c)) -> a -> c rule (I abc) a = abc a (I (\b c -> c)) axiom :: I r0 (I r1 a (I r2 b c)) (I r0 (I r3 d (I r3 d d)) (I r4 (I r2 e b) (I r4 (I r1 a e) (I r1 a e)))) axiom = let a1 (I eb) e = I $ \b c -> eb e b a2 = I $ \d (I dd) -> dd d d a3 (I abc) eb = I $ \a e -> abc a (a1 eb e) a4 abc = I $ \eb aeae -> runI a2 (a3 abc eb) aeae in I $ \abc (I dddebaeae) -> dddebaeae a2 (a4 abc) 类型变量)。

我不会重复在选址文章中所代表的证明,但我想展示一些东西。在r*的上述定义中,我们使用一些let绑定变量来构造结果。毫不奇怪,可以使用axiomrule提取这些变量本身。让我们看看如何:

axiom

还有一件事需要证明,我们可以--Equal to a4 t4 :: I r0 a (I r1 b c) -> I r2 (I r1 d b) (I r2 (I r0 a d) (I r0 a d)) t4 abc = rule axiom abc --Equal to a3 t3 :: I r0 a (I r1 b c) -> I r1 d b -> I r0 a d t3 abc eb = rule (t4 abc) eb --Equal to a2 t2 :: I r a (I r a a) t2 = rule (t3 axiom (t3 (t4 axiom) axiom)) axiom --Equal to a1 t1 :: I r a b -> a -> I r b c t1 ab a = rule (t3 t2 (t3 (t3 t2 t2) ab)) a 使用t1来证明所有重言式。我觉得情况确实如此,但还没有证明。

与其他语言相比,Haskell的称呼似乎更有效和富有表现力。