2

Hackage 的源代码中,我读到:

instance Profunctor (->) where
  dimap ab cd bc = cd . bc . ab
  {-# INLINE dimap #-}
  lmap = flip (.)
  {-# INLINE lmap #-}
  rmap = (.)
  {-# INLINE rmap #-}

dimap但是// for the的默认实现只需要定义and , or lmap; 定义所有这些是不必要的。rmapProfunctorlmaprmapdimap

相反,是否有理由将它们全部定义?

4

1 回答 1

3

lmap正如@FyodorSoikin 评论的那样,其意图可能是rmap手动编码的定义比基于dimap.

dimap但是,使用下面的测试程序,我尝试使用/ rmap/ lmapdimaponly 和rmap/ only的所有三个定义实例lmap,以及测试功能的核心 、lr,并且b在编译时在所有三种情况下都完全相同-O2

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#) }

虽然对于更复杂的示例,编译器可能无法优化 and 的默认定义lmap f = dimap f idrmap = dimap id但在我看来这是极不可能的,因此手工编码lmaprmap没有任何区别。

我认为解释是,即使是像 Edward Kmett 这样非常熟练的 Haskell 程序员仍然低估了编译器并对其代码进行了不必要的手动优化。

更新: @4castle 在评论中询问没有优化会发生什么。警告说“因为它改进了-O0代码”并没有让我觉得任何事情都是合理的论据,我看了一下。

在未优化的代码中,显式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具有默认值的显式代码lmaprmap产生比替代方案更好的代码。问题似乎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-simpl -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)
于 2021-01-24T19:46:54.613 回答