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
在每种情况下产生了什么,包括内联和内联指导。