部分应用 Haskell 中的多个函数

Partially apply several functions in Haskell

假设,在 Haskell 中,我有一堆函数都依赖于相同的参数类型:

f :: Par -> a -> b
g :: Par -> b -> c

由于我正在编写更多仍依赖于此参数类型的函数,因此我可以执行类似

的操作
h :: Par -> a -> c
h par = myg . myf
    where myf = f par
          myg = g par

但是我一直不得不写这些 where 行。问题是:这可以避免吗?

[编辑:我试图提供一个最小的例子来说明问题,但显然这个例子太小而无法说明我想要的东西。在实际问题中h当然不仅仅是f和g的合成。所以这是一些实际的代码:

有函数

apply :: ChamberLattice -> ChLatword -> ChLatWord
reduce :: ChamberLattice -> ChLatWord -> ChLatWord

我正在定义一个函数

chaseTurn :: ChamberLattice -> Turn -> Parity -> ChLatWord -> ChLatWord
chaseTurn cl Straight _ xs = xs
chaseTurn cl t parity xs = if ((turn parity xs) == t)
                           then case myApply xs of
                               (y1:y2:ys) -> (y1:y2:(myChaseTurn t parity ys))
                               ys -> ys
                           else myReduce xs
where myApply = apply cl
      myChaseTurn = chaseTurn cl
      myReduce = reduce cl

]

(这​​道题本质上和 Grouping functions in Haskell 但我在那里使用了一些令人分心的不幸词。)

在Haskell中,所有函数都接受一个输入参数。但有时,应用函数的 return 值是一个新函数。那么,作为第一步,您可以通过将函数 fg:

的 return 值放在方括号中来使其更加明确
f :: Par -> (a -> b)
g :: Par -> (b -> c)

函数也是类型,因此我们可以任意决定将 a -> b 别名为 φphi 而不是 f) 和 b -> cγgamma 而不是 g)。 (是的,当你 运行 没有字母时,你可以找到希腊字母表!)

这意味着您可以将函数视为具有类型

f :: Par -> φ
g :: Par -> γ

这些都是所谓的 reader monad 的自动实例,它也是一个(应用)函子。特别是,(->) Par,或者,如果有帮助,Par ->,是一个 Applicative 实例。这意味着您可以使用 pure<*>

作为第一次尝试,你可以这样写

pure (\x y -> (x, y)) <*> f <*> g

为了简单地了解该组合的工作原理。可以说,该表达式的类型为 Par -> (φ, γ)。该 lambda 表达式只是从 f 'container' 中获取 x 并从 g 'container' 中获取 y,并将它们组合在一个元组中。元组的第一个元素的类型为 φ,第二个元素的类型为 γ.

插入 φγ 的定义,你得到类型 Par -> (a -> b, b -> c).

您想要组合这些函数,而不是将 return 值作为函数元组。您可以为此使用函数组合运算符 .

h = pure (\x y -> y . x) <*> f <*> g

请注意函数是从右到左组合的,因此 x (a -> b) 首先出现,然后是 y (b -> c)。

但是,您可以翻转 fg

h = pure (\y x -> y . x) <*> g <*> f

然后可以将显式 lambda 表达式简化为:

h = pure (.) <*> g <*> f

最后,您可以使用中缀 <$> 运算符代替 pure (.) <*>

h = (.) <$> g <*> f

此函数的类型为 Par -> a -> c

h par = f par . g par 做了很多,par 东西开始变得混乱。

你不能做 h = f . g,因为 par 参数也必须传递。

所以你想出了一个强大的组合运算符来为你做这件事:

-- (.) :: (b -> c) -> (a -> b) -> a -> c
(§) :: (par -> b -> c) -> (par -> a -> b) -> par -> a -> c
(§) f g par = f par . g par

现在您可以做到 h = f § g。这个运算符可能是以前发明的。

顺便说一句,部分应用的函数是instances of Monad。这意味着您可以:

(§) f g par = (do { fpar <- f; gpar <- g; return (fpar . gpar) }) par

或者只是:

(§) f g = do { fpar <- f; gpar <- g; return (fpar . gpar) }

(这里,fparf,隐式 par 已应用。monad 实例使 par 隐式。)

如果我们要参数化这个 do-block:

(§) f g = ( \f m1 m2 -> do { x1 <- m1; x2 <- m2; return (f x1 x2) } ) (.) f g

和eta-reduce参数:

(§) = ( \f m1 m2 -> do { x1 <- m1; x2 <- m2; return (f x1 x2) } ) (.)

然后在 Hoogle 上寻找类似于此 do 块的内容,您会发现 liftM2:

(§) = liftM2 (.)

此时我们真的不需要给它起一个特殊的名字,因为 liftM2 (.) 已经很短了。

你已经发现了 Reader monad 的用例,如果你可以稍微调整你的签名。如果你有

f :: a -> Par -> b
g :: b -> Par -> c

您可以将它们重新定义为

import Control.Monad.Trans.Reader

f :: a -> Reader Par b
g :: b -> Reader Par c

然后您可以使用普通的 Kleisli 组合运算符定义 h

import Control.Monad

h :: a -> Reader Par c
h = f >=> g

(就算不改签名,我觉得你也可以写成h = flip (flip f >=> flip g)。)

这可以使用隐式参数(不是纯粹的 Haskell 而是 ghc 语言扩展,参见 https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#implicit-parameters)来完成。

上面的代码就变成了

f :: (?p :: Par) => a -> b

g :: (?p :: Par) => b -> c

h :: (?p :: Par) => a -> c
h = g . f