问题描述
长话短说
当我发现自己在编写像 contramap
这样的代码时,我是否应该考虑使用 (. f) . g
,我在徘徊,其中 f
实际上是将第二个参数预处理为 g
.
长话短说
最初,我有两个输入,a1 :: In
和 a2 :: In
,包裹在一对 (a1,a2) :: (In,In)
中,我需要对这些输入进行两个交互处理。具体来说,我有一个函数 binop :: In -> In -> Mid
来生成一个“临时”结果,还有一个函数 fun :: Mid -> In -> In -> Out
用于输入 binop
的输入和输出。
鉴于上面“用另一个函数的输入和输出馈送的函数”部分,我虽然使用了函数 monad,所以我想出了这个,
finalFun = uncurry . fun =<< uncurry binop
阅读起来并不复杂:binop
将输入作为一对,并将其输出和输入传递给 fun
,后者也将输入作为一对。>
然而,我注意到在 fun
的实现中我实际上只使用了输入的“简化”版本,即我有一个像 fun a b c = fun' a (reduce b) (reduce c)
这样的定义,所以我认为,而不是fun
,我可以在 fun'
的定义中将 reduce
与 finalFun
一起使用;我想出了
finalFun = (. both reduce) . uncurry . fun' =<< uncurry binop
我相信,这远不那么容易阅读,尤其是因为它具有不自然的部分顺序。我只能想到使用一些更具描述性的名称,如
finalFun = preReduce . uncurry . fun' =<< uncurry binop
where preReduce = (. both reduce)
由于 preReduce
实际上是预处理 fun'
的第 2 个和第 3 个参数,所以我在徘徊是否是使用 contramap
的合适时机。
解决方法
lmap f . g
(rather than contramap
,因为它也需要一个 Op
包装器)在清晰度方面确实比 (. f) . g
有所改进。如果您的读者熟悉 Profunctor
,看到 lmap
会立即暗示正在修改某些内容的输入,而无需他们在脑海中执行点管道。请注意,这还不是一个广泛使用的习语。 (作为参考,这里是针对 the lmap
version 和 the dot section one 的 Serokell Hackage 搜索查询。)
至于你更长的例子,惯用的做法可能不是把它写成pointfree。也就是说,我们可以通过更改 fun
/fun'
的参数顺序来获得更易读的 pointfree 版本,以便您可以使用 equivalent Applicative
实例而不是 { {1}} 一:
Monad
Pointfree 函数 binOp :: In -> In -> Mid
fun' :: In -> In -> Mid -> Out
reduce :: In -> In
both f = bimap f f
finalFun :: (In,In) -> Out
finalFun = uncurry fun' . both reduce <*> uncurry binOp
可以说比 pointfree 函数 (<*>)
更容易理解,因为它的两个参数是相关函数函子中的计算。此外,此更改消除了对点部分技巧的需要。最后,由于 (=<<)
是函数的 (.)
,我们可以进一步将 fmap
改写为...
finalFun
... 从而得到一个 applicative 风格的表达,在我看来(不是很流行!),这是使用 applicative 函数的一种合理可读的方式。 (我们可能会使用 finalFun = uncurry fun' <$> both reduce <*> uncurry binOp
进一步简化它,但我觉得在这种特定情况下,发生的事情不太明显。)