Function (>;)的Profunctor实例定义dimap和lmap/rmap有什么原因吗?
我在书中读到:Function (>;)的Profunctor实例定义dimap和lmap/rmap有什么原因吗?,function,haskell,functional-programming,arrows,profunctor,Function,Haskell,Functional Programming,Arrows,Profunctor,我在书中读到: 实例Profunctor(->),其中 dimap ab cd bc=cd。公元前。ab {-#内联dimap} lmap=翻转(.) {-#内联lmap} rmap=() {-#内联rmap} 但是Profunctor的dimap/lmap/rmap的默认实现要求只定义lmap和rmap,或者dimap;定义所有这些都是不必要的 是否有理由将其全部定义?正如@FyodorSoikin评论的那样,其目的可能是lmap和rmap手工编码的定义比基于dimap的默认定义更有效 但是
实例Profunctor(->),其中
dimap ab cd bc=cd。公元前。ab
{-#内联dimap}
lmap=翻转(.)
{-#内联lmap}
rmap=()
{-#内联rmap}
但是Profunctor
的dimap
/lmap
/rmap
的默认实现要求只定义lmap
和rmap
,或者dimap
;定义所有这些都是不必要的
是否有理由将其全部定义?正如@FyodorSoikin评论的那样,其目的可能是
lmap
和rmap
手工编码的定义比基于dimap
的默认定义更有效
但是,使用下面的测试程序,我尝试使用dimap
/rmap
/lmap
,dimap
,和rmap
/lmap
,以及测试函数的核心l
,r
,来定义实例,当使用-O2
编译时,b
在所有三种情况下都是完全相同的:
b = \ x -> case x of { I# x1 -> I# (+# 15# (*# 6# x1)) }
r = \ x -> case x of { I# x1 -> I# (+# 15# (*# 3# x1)) }
l = \ x -> case x of { I# x1 -> I# (+# (*# x1 2#) 5#) }
虽然对于更复杂的示例,编译器可能无法优化lmap f=dimap f id
和rmap=dimap id
的默认定义,但我觉得这极不可能,因此手工编码的lmap
和rmap
没有任何区别
我认为原因是,即使是像Edward Kmett这样的非常熟练的Haskell程序员仍然低估了编译器,并对代码执行了不必要的手工优化
更新:在一篇评论中,@4castle问没有优化会发生什么。我注意到“因为它改进了-O0
code”并不能作为一个合理的论据来支持任何事情,于是我看了一眼
在未优化的代码中,显式的rmap
定义通过避免使用id
的额外组合来产生更好的核心:
-- explicit `rmap`
r = . (let { ds = I# 3# } in \ ds1 -> * $fNumInt ds1 ds)
(let { ds = I# 5# } in \ ds1 -> + $fNumInt ds1 ds)
-- default `rmap`
r = . (let { ds = I# 3# } in \ ds1 -> * $fNumInt ds1 ds)
(. (let { ds = I# 5# } in \ ds1 -> + $fNumInt ds1 ds) id)
而显式的lmap
定义最终产生的核心是大致相同的,或者更糟
-- explicit `lmap`
$clmap = \ @ a @ b1 @ c -> flip .
l = $clmap
(let { ds = I# 2# } in \ ds1 -> * $fNumInt ds1 ds)
(let { ds = I# 5# } in \ ds1 -> + $fNumInt ds1 ds)
-- default `lmap`
l = . id
(. (let { ds = I# 5# } in \ ds1 -> + $fNumInt ds1 ds)
(let { ds = I# 2# } in \ ds1 -> * $fNumInt ds1 ds))
由于上述定义,显式的dimap
优于默认值,因为有额外的flip
-- explicit `dimap`
b = . (let { ds = I# 3# } in \ ds1 -> * $fNumInt ds1 ds)
(. (let { ds = I# 5# } in \ ds1 -> + $fNumInt ds1 ds)
(let { ds = I# 2# } in \ ds1 -> * $fNumInt ds1 ds))
-- default `dimap`
$clmap = \ @ a @ b1 @ c -> flip .
b = . ($clmap (let { ds = I# 2# } in \ ds1 -> * $fNumInt ds1 ds))
(. (let { ds = I# 3# } in \ ds1 -> * $fNumInt ds1 ds))
(let { ds = I# 5# } in \ ds1 -> + $fNumInt ds1 ds)
在另一篇评论中,@oisdk斥责我的考试不切实际。我将指出,内联递归的失败在这里并不是一个真正的问题,因为dimap
、lmap
或rmap
都不是递归的。特别是,以递归方式简单地“使用”其中一个,如foo=foldr rmap id
不会干扰内联或优化,并且为foo
生成的代码与显式和默认的rmap
相同
此外,将类/实例从l
/r
定义中拆分为单独的模块对我的测试程序没有任何影响,也不会将其拆分为三个模块,即类、实例和l
/r
,因此跨模块边界内联似乎不是一个很大的问题
对于非专门化的多态用法,我想应该归结为生成的Profunctor(->)
字典。我看到以下内容似乎表明,显式dimap
和默认lmap
和rmap
生成的代码比备选方案更好。问题似乎是flip(.)
在这里没有得到适当的优化,因此显式的lmap
定义适得其反
-- explicit `dimap`, `rmap`, and `lmap`
$fProfunctor->
= C:Profunctor $fProfunctor->_$cdimap $fProfunctor->_$clmap .
$fProfunctor->_$cdimap
= \ @ a @ b @ c @ d ab cd bc x -> cd (bc (ab x))
$fProfunctor->_$clmap = \ @ a @ b @ c x y -> . y x
-- explicit `lmap`, `rmap`, default `dimap`
$fProfunctor->
= C:Profunctor $fProfunctor->_$cdimap $fProfunctor->_$clmap .
$fProfunctor->_$cdimap
= \ @ a @ b @ c @ d eta eta1 x eta2 -> eta1 (x (eta eta2))
$fProfunctor->_$clmap = \ @ a @ b @ c x y -> . y x
-- explicit `dimap`, default `lmap`, `rmap`
$fProfunctor->
= C:Profunctor
$fProfunctor->_$cdimap $fProfunctor->_$clmap $fProfunctor->1
$fProfunctor->_$cdimap
= \ @ a @ b @ c @ d ab cd bc x -> cd (bc (ab x))
$fProfunctor->_$clmap = \ @ a @ b @ c eta bc x -> bc (eta x)
$fProfunctor->1 = \ @ b @ c @ a cd bc x -> cd (bc x)
如果有人举了一个例子,其中这些显式定义生成了更好的-O2
代码,那么这将是一个很好的替代答案
这是我的测试程序。我使用ghc-O2 Profunctor.hs-fforce recomp-ddump siml-dsuppress all-dsuppress uniques编译
module Profunctor where
class Profunctor p where
dimap :: (a -> b) -> (c -> d) -> p b c -> p a d
dimap f g = lmap f . rmap g
{-# INLINE dimap #-}
lmap :: (a -> b) -> p b c -> p a c
lmap f = dimap f id
{-# INLINE lmap #-}
rmap :: (b -> c) -> p a b -> p a c
rmap = dimap id
{-# INLINE rmap #-}
instance Profunctor (->) where
-- same core if dimap is commented out or if lmap/rmap are commented out
dimap ab cd bc = cd . bc . ab
lmap = flip (.)
rmap = (.)
l :: Int -> Int
l = lmap (*2) (+5)
r :: Int -> Int
r = rmap (*3) (+5)
b :: Int -> Int
b = dimap (*2) (*3) (+5)
虽然原则上可以根据dimap
定义rmap
和lmap
,但有时存在更有效的实现。这就是为什么它们首先是类的方法,而不是独立的函数。如果没有启用优化怎么办?(no-O2
flag)您的示例程序实际上是一个很差的内联测试:它都是一个模块(尤其是内联在不跨越模块边界时效果最好),没有递归(通常GHC不能内联),也没有多态函数(即抽象地引用Profunctor
类,而不仅仅是使用函数的类).在实际代码中,我绝对希望这种手动优化能够产生加速效果。talk非常擅长揭穿一些关于内联GHC可以做什么的神话。一件有趣的事情是总是看到-ddump siml-O
在每种情况下产生了什么,包括内联和内联指导。