使用镜头组合功能
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
.
的次数即可
考虑具有以下(伪)签名的 "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
.