(如何)你能柯里化组合单子函数吗?

(How) Can you curry compose monadic functions?

我有以下功能:

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 其中 andThenmonadic composition 运算符一样使用。例如:

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

我假设在 Haskell(例如 Kliesli >=>)等语言中可以创建这样的 andThen 函数。在 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))
    }
}

鉴于此,似乎如果我们柯里化这些函数,我们 可能 能够实现这样的组合(或者至少它 看起来 可能):

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!")

这还possible/achievable吗?如果不是 Scala,我们如何在 Haskell 或一般情况下 curry 组合单子函数

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

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

首先,您的 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.ArrowHere 您可以找到大量运算符来实现您的目标:

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 -> c -> m d 应该很清楚,我们想要一些行为类似于 m 但需要 c 的 monad。
  • f :: a -> m b 的类型我们知道 f 无法访问 c 并且不知何故应该将其纳入范围。否则,fh 永远不可能是同一个单子。
  • 谢天谢地,我们可以使用 const . f :: a -> c -> 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 使用一些 monadic 运算符,但问题是我们需要保留 monad m 而那不能除非我们将结果包装成某种新的数据类型,否则我们将要引用的 monad 是函数 monad (->)(这是 haskell 特定的吗?我认为不是)。显而易见的选择是使用 Kleisli monad (ghc >= 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 不同的单子来完成。可能所有解决方案都同构到 curry / uncurry。只要你能把 c 带到 f 的范围内,并找到一个保留 m 行为的 monad,你就可以应用它。