使用镜头组合功能

Using lenses to compose functions

考虑具有以下(伪)签名的 "composition" 函数:

(a1 -> a2 -> ... -> an -> r) -> 
(s -> ai) -> 
a1 -> a2 -> ... -> ai -> ... an -> r
where i in [1..n]

当然不能在Haskell中写出上面的内容,但这里有一个具体的例子:

f4change3 :: 
  (a1 -> a2 -> a3 -> a4 -> r) -> 
  (s -> a3) -> 
  a1 -> a2 -> s -> a4 -> r
f4change3 f g x1 x2 x3 x4 = f x1 x2 (g x3) x4

如您所见,每个 arity n 函数都有一个 n 函数集合,因此我们需要的函数数量随 arity 呈二次方增长。

我可以只写我需要的,但首先,我不想重新发明轮子,所以很高兴知道图书馆是否已经这样做了。而且,虽然我几乎没用过镜头,但我读过一些关于它们的资料,这类问题似乎是他们的盟友,但我不确定到底该怎么做。如果可能的话,一些示例代码会很棒。

这个库叫做 base。而且你甚至不需要导入任何东西,一切都已经在 Prelude 中了。您所需要的只是一点想象力和创造力。然后你可以很容易地用下一种方式编写你的函数:

f4change3 ::
  (a1 -> a2 -> a3 -> a4 -> r) ->
  (s -> a3) ->
  a1 -> a2 -> s -> a4 -> r
f4change3 = flip . ((flip . ((.) .)) .)

经过几年的培训和阅读此类代码后,您将对此类代码感到满意。你只需要几个小时就能理解这个实现的实际作用。但是大师 Haskell 程序员不阅读实现,因为从类型上看一切都很清楚。

好的,这是真正的答案。如果你不想一笑而过,想解决问题,你可以走下一步。

您的实施可以缩短一点:

f4change3 ::
  (a1 -> a2 -> a3 -> a4 -> r) ->
  (s -> a3) ->
  a1 -> a2 -> s -> a4 -> r
f4change3 f g x1 x2 = f x1 x2 . g

这不是很有用,你可以完整 point-free(见我之前的回答)。

我希望 Haskell 有命名参数,这样你就可以简单地写成类似于 -XRecordWildCards:

f4change3 ::
  ((a1 :: a1) -> (a2 :: a2) -> (a3 :: a3) -> (a4 :: a4) -> r) ->
  ((s :: s) -> a3) ->
  (a1 :: a1) -> (a2 :: a2) -> (s :: s) -> (a4 :: a4) -> r
f4change3 f g = f{ a3 = g{..}, .. }

对于这种特殊情况,当我们希望参数名称与类型变量名称相同时,语法甚至可以 much-much 更短。但不幸的是,我们在不久的将来(或永远)不会看到这样好的功能。

但是你问的是带镜头的解决方案。您确实可以使用镜头实现类似的效果。您唯一需要更改类型签名的是将 f 的参数表示为具有四个字段的数据类型。这是完整的代码:

{-# LANGUAGE TemplateHaskell #-}

import           Control.Lens (makeLenses, (%~))

data FArg a1 a2 a3 a4 = FArg
    { _a1 :: a1
    , _a2 :: a2
    , _a3 :: a3
    , _a4 :: a4
    }

makeLenses ''FArg

f4change3 ::
  (FArg a1 a2 a3 a4 -> r) ->
  (s -> a3) ->
  FArg a1 a2 s a4 -> r
f4change3 f g = f . (a3 %~ g)

也许一些使用镜头的复杂解决方案是可以接受的,但在这里我们至少实现了可理解性(当然如果你熟悉镜头)。但通常没有人这样做。

如评论中所述,Conal Elliott's semantic editor combinators 为编写这些函数提供了一个非常漂亮的工具。让我们回顾一下他的两个组合器:

argument :: (a' -> a) -> ((a -> b) -> (a' -> b))
argument = flip (.)

result :: (b -> b') -> ((a -> b) -> (a -> b'))
result = (.)

换言之:argument在函数调用前修改函数的参数,result在调用后修改函数的return值。 (有关这些组合器的进一步 intuition-bolstering 解释,请参阅博客 post 本身。)假设我们有一个类型为

的函数
a1 -> a2 -> a3 -> a4 -> a5 -> b

我们想改变 a5。注意,我们当然可以把这个括起来:

a1 -> (a2 -> (a3 -> (a4 -> (a5 -> b))))

那么如果我们要到达a5,我们应该如何到达这个结构呢?那么,我们将通过重复进入 result,然后作用于 argument 来完成:此函数的结果类型为 a2 -> (a3 -> (a4 -> (a5 -> b))),其结果为 a3 -> (a4 -> (a5 -> b)),其结果是 a4 -> (a5 -> b),其结果是 a5 -> b,其参数是 a5。这直接给了我们代码:

arg5 :: (a5' -> a5)
     -> (a1 -> a2 -> a3 -> a4 -> a5  -> b)
     -> (a1 -> a2 -> a3 -> a4 -> a5' -> b)
arg5 = result . result . result . result . argument

希望很清楚如何将其概括为修改其他参数:只需改变调用 result.

的次数即可