问题描述
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 g
其中 andThen
的使用就像一个 monadic 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,C]]): Function1[A,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 函数保留类似 andThen
的运算符,但避免将两者混合?如果是这样,我可以看到 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.Arrow
的面向类别理论的模块。 Here 您可以找到大量运算符来实现您的目标:
import Control.Arrow
hKleisli :: Monad m => (a -> m b) -> ( (b,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
永远不会是同一个 monad。 - 坦率地说,我们可以使用
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 . f
和 curry g
的单子运算符,但问题是我们需要保留单子 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,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 来完成。可能所有解决方案都是同构的,直到咖喱/非咖喱。只要您可以将 c
带入 f
的范围并找到一个保留 m
行为的 monad,您就可以应用它。