(如何)你能咖喱组合一元函数吗?
我有以下功能:
f: a -> m[b]
g: (b,c) -> m[d]
h: (a,c) -> m[d]
怎样才能h被表示为一个组成f和g?
使用do/for符号,我们可以h像这样轻松实现:
h: (a,c) => {
for {
b <- f(a)
d <- g(b,c)
} yield (d)
}
但是,我很好奇我们是否可以这样表达:h = f andThen gwhereandThen像一元组合运算符一样使用。例如:
f: a -> m[b]
g: b -> m[c]
h: a -> m[c] = f andThen g
我假设andThen在像 Haskell(例如 Kliesli >=>)这样的语言中创建这样的函数是可能的。在 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!")
这甚至可能/可以实现吗?如果不是 Scala,我们如何在 Haskell 或一般情况下使用咖喱组合一元函数?
这是已知的事情还是我们是否明确区分了适用于非 monadic 函数的柯里化和为 monadic 函数保留andThenlike 运算符,但避免将两者混合?如果是这样,我可以看到一个强有力的do/for符号案例。但是,我并不完全相信这是不可能的,并且想进一步了解这一点。也许代码会很混乱,没关系 - 我只是好奇。我在解决现有问题的过程中偶然发现了这种情况,但我无法像这样抛出它。
回答
在 Haskell 中有一些标准(即在baselib 中)运算符。
首先,您的andThen函数是众所周知的Kleisli 组合:
>=> :: (a -> m b) -> (b -> m c) -> a -> m c
a -> m b
b -> m c
-----------------
a -> m c
>=> :: (a -> m b) -> (b -> m c) -> a -> m c
a -> m b
b -> m c
-----------------
a -> m c
</c
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)
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)
ode>
由于g在元组中操作f而不返回元组,此运算符与您的类型不完全匹配。这可以通过do/for符号轻松克服
我会去寻找上面的解决方案,但出于好奇,这个问题已经面临,Haskell 的base库引入了一个面向类别理论的模块,称为Control.Arrow. 在这里,您可以找到大量运算符来实现您的目标:
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
--}
编辑
关于您的评论:最初的问题是:我们如何编写f和g在柯里化之后g?而且我的解决方案看起来更像是让我们不客气f地使用,g所以我同意这不是一个完整的解决方案。好的,让我们解决您的问题,但首先要注意一些事项:
- 从类型
h :: a -> c -> m d应该很清楚,我们想要一些行为类似m但考虑c在内的monad 。 - 从
f :: a -> m b我们知道的类型中,f无法访问c并且以某种方式应该将其纳入范围。否则,f而且h可能永远是相同的单子。 - 坦率地说,我们可以使用以下方法为 f 添加一个额外的参数
const . f :: a -> c -> m b
到目前为止我们有
现在,它似乎很明显,我们需要使用一些一元运算符const . f和curry g,但问题是,我们需要保存的单子m,除非我们总结的结果到了一些新的数据类型,它不能achive,否则,单子我们将指的是函数monad (->)(这是特定于haskell 的吗?我认为不是)。显而易见的选择是使用Kleislimonad ( ghc >= 8.10)。所以现在我们有:
请注意,这可以使用与Kleisli. 可能所有解决方案都是同构的,直到咖喱/非咖喱。只要你能c进入范围f并找到一个保留m你行为的 monad就可以应用它。