【问题标题】:(How) Can you curry compose monadic functions?(如何)你能 curry 组成一元函数吗?
【发布时间】:2021-05-14 06:47:52
【问题描述】:

我有以下功能:

f: a -> m[b]
g: (b,c) -> m[d]
h: (a,c) -> m[d]

h如何表示为fg的组合?

使用do/for 表示法,我们可以像这样轻松实现h

h: (a,c) => {
 for {
  b <- f(a)
  d <- g(b,c)
 } yield (d)
}

但是,我很好奇我们是否可以这样表达:h = f andThen g 其中andThen 被用作monadic composition 运算符。例如:

f: a -> m[b]
g: b -> m[c]
h: a -> m[c] = f andThen g

我假设在 Haskell 等语言中创建这样的 andThen 函数是可能的(例如,Kliesli &gt;=&gt;)。在 Scala 中,我们可以这样写:(在 Scala 中命名为 andThenE,因为 andThen 已经在 Function1 的实例上定义)。

implicit class AndThenEither[A,B](val e: Function1[A,Either[_,B]]) {
    def andThenE[C](f:Function1[B, Either[_,C]]): Function1[A, Either[_,C]] = {
      (v1: A) => e.apply(v1).flatMap(b => f.apply(b))
    }
}

鉴于此,如果我们对函数进行 curry,我们可能能够实现这样的组合(或者至少 看起来是可能的):

f: a -> m[b]
g: b -> c -> m[d]
h: a -> c -> m[d] = f andThen g

理论上这可以工作,但我不知道这是否可能或如何在 Scala(或 Haskell,尽管我更熟悉前者)中实现类似的东西。

假设我们有以下函数:

case class Error(e:String)
case class Output(i: Int, f: Float, s: String)
case class IntermediateOutput(i:Int, f:Float)

def f(i:Int): Either[Error, IntermediateOutput] = Right(IntermediateOutput(i+1, i*0.33)
def g(io: IntermediateOutput, s: String): Either[Error, Output] = Right(Output(io.i, io.f, "hello "+s)) 

val h: (Int, String) => Either[Error, Output] = f andThen g

val result = h(1, "world!") //Right(Output(2, 0.33, "hello world!")

这甚至可能/可以实现吗?如果不是 Scala,我们如何在 Haskell 或一般情况下curry 组合单子函数

这是一个已知的事情,还是我们明确区分适用于非单子函数的柯里化和为单子函数保留 andThen like 运算符,但避免将两者混合?如果是这样,我可以看到do/for 符号的有力案例。但是,我并不完全相信这是不可能的,并且想进一步了解这一点。也许代码会很混乱,没关系 - 我只是好奇。由于处理现有问题,我偶然发现了这种情况,但我不能这样投射。

【问题讨论】:

  • 在 Haskell 中,您在 Control.Monad 中定义了 Kleisi Arrow composition operator (&gt;=&gt;),这正是您的 andThen 函数
  • 我猜是 h = flip (\c -&gt; f &gt;=&gt; flip g c) 或者类似 Haskell 的东西。如果您不介意f :: a -&gt; m b; g :: c -&gt; b -&gt; m d; h :: c -&gt; a -&gt; m d(注意gh 的预翻转参数),那么h c = f &gt;=&gt; g c 并不算太可怕。不过,我不确定其中任何一个是否比 h a c = do { b &lt;- f a; g b c } 更具可读性或更可取。
  • @Ismor - 是的。我很清楚。我应该对此更明确/清楚。我会更新问题。
  • 偶然出现的一些事情,但我认为值得明确指出:不,这里没有任何障碍。在 Haskell 中,gh 将被写成 curried,除非你正在做一些非常具体的事情。您可以将 andThen 写成 andThen_1_2 :: Monad m =&gt; (a -&gt; m b) -&gt; (b -&gt; c -&gt; m d) -&gt; (a -&gt; c -&gt; m d) ; andThen_1_2 f g = \a c -&gt; do b &lt;- f a ; g b c,这是 Daniel Wagner 所写内容的通用版本。它使用do 表示法,但使用它来实现所需的功能;确实,现有的无点操作方法有点笨拙,但没关系。

标签: scala haskell monads currying function-composition


【解决方案1】:

在 Haskell 中有一些标准(即在 base lib 中)运算符。

首先,您的andThen 函数是众所周知的Kleisli composition

>=> :: (a -> m b) -> (b -> m c) -> a -> m c

        a -> m b
               b -> m c
       -----------------
        a        -> m c

由于g 在元组中操作并且f 未返回元组,因此该运算符与您的类型不完全匹配。这可以通过do/for 符号轻松克服

h :: Monad m => (a -> m b) -> ( (b,c) -> m d ) -> (a,c) -> m d
h f g (a, c) = do
  b <- f a
  g (b, c)

我会选择上面的解决方案,但出于好奇,这个问题已经遇到过,Haskell 的base 库引入了一个名为Control.Arrow 的面向类别理论的模块。 Here你可以找到大量的运营商来实现你的目标:

import Control.Arrow

hKleisli :: Monad m => (a -> m b) -> ( (b,c) -> m d ) -> (a,c) -> m d
hKleisli f g = runKleisli $ 
  first (Kleisli f) >>> Kleisli g
--|                 |   |- this is just boilerplate
--|                 |- This composes Categories
--|- this converts f into a function operating in tuples

{--
       Kleisli f  :: Kleisli m  a     b         --  a    -> m  b
---------------------------------------------
first (Kleisli f) :: Kleisli m (a,c) (b,c)      -- (a,c) -> m (b,c)
        Kleisli g :: Kleisli m       (b,c) d    --            (b,c) -> m d
---------------------------------------------
first (Kleisli f)  
    >>> Kleisli g :: Kleisli m (a,c)       d    -- (a,c)            -> m d
--}

编辑

关于您的评论:最初的问题是:在柯里化g 之后,我们如何编写fg?我的解决方案看起来更像 让我们取消 fg 合作 所以我同意这不是一个完整的解决方案。好的,让我们解决您的问题,但首先,请注意:

  • 来自h :: a -&gt; c -&gt; m d 类型应该很清楚,我们想要一些行为类似于m 但考虑c 的monad。
  • f :: a -&gt; m b 的类型我们知道f 无法访问c,并且应该以某种方式将其纳入范围。否则,fh 永远不会是同一个 monad。
  • 坦率地说,我们可以使用 const . f :: a -&gt; c -&gt; m b 为 f 添加一个额外的参数

到目前为止我们已经

{-- 
The name of the type variables are chosen to match the ones used in this post, but are different in ghci

        f :: a      -> m b
        g :: (b,c)  -> m d

const . f :: a -> c -> m b
  curry g :: b -> c -> m d
--}

现在很明显,我们需要使用一些带有 const . fcurry g 的单子运算符,但问题是我们需要保留单子 m,除非我们将结果包装成一些新的数据类型,否则,我们将引用的 monad 是函数 monad (-&gt;) (这是 Haskell 特定的吗?我认为不是)。显而易见的选择是使用Kleisli monad (ghc &gt;= 8.10)。所以现在我们有:

{-- 
The name of the type variables are chosen to match the ones used in this post, but are different in ghci

        f :: a      -> m b
        g :: (b,c)  -> m d

const . f :: a -> c -> m b
curry   g :: b -> c -> m d
                 |- This result lives in the -> monad

Kleisli . const . f :: a -> Kleisli m c b
Kleisli . curry   g :: b -> Kleisli m c b
--}

import Control.Monad
import Control.Arrow

f :: Monad m => a -> m b
f = undefined

g :: Monad m => (b, c) -> m d
g = undefined

-- And now, We have curryed and composed g
h :: Monad m => a -> c -> m b
h = runKleisli . (f' >=> g')
  where
    f' :: Monad m => a -> Kleisli m c b
    f' = Kleisli . const . f
    g' :: Monad m => b -> Kleisli m c d
    g' = Kleisli . curry g

请注意,这可以使用与 Kleisli 不同的 monad 来完成。可能所有解决方案都是同构的 curry / uncurry。只要您可以将c 带入f 的范围并找到一个保留m 行为的monad,您就可以应用它。

【讨论】:

  • @Ismor - 这是通过(概念上)“包装”f 到另一个函数以通过(a,c)-&gt;m(b,c) 获得类型对齐来回避问题的有趣方式,以便链可以继续。我理解它解决了 组合问题,但没有回答我关于“currying + composition”的问题,以及在返回 monad 的“所有 curried 单 arg 函数”的假设下是否存在解决方案最后。有什么想法吗?
  • @PhD 我添加了一个编辑部分
  • @Ismor - 这是您非常感谢的努力。我正在消化你发布的内容,以了解它是如何运作的。我只能感谢您为帮助澄清这一点所做的努力。为此,我(我相信社区)非常感谢您!
猜你喜欢
  • 2016-07-17
  • 1970-01-01
  • 1970-01-01
  • 2021-09-14
  • 2015-06-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多