function - (->) 的 Profunctor 实例同时定义 dimap 和 lmap/rmap 是否有任何原因?

标签 function haskell functional-programming arrows profunctor

source code on Hackage我读到了这个:

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

但是 Profunctordimap/lmap/rmap 的默认实现需要定义lmaprmap,或 dimap;没有必要定义所有这些。

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

最佳答案

正如 @FyodorSoikin 评论的那样,其意图可能是 lmaprmap 手动编码定义比基于 dimap< 的默认定义更有效.

但是,使用下面的测试程序,我尝试使用 dimap/rmap/lmap 的所有三个来定义实例仅 dimap,仅 rmap/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#) }

虽然对于更复杂的示例,编译器可能无法优化 lmap f = dimap f idrmap = dimap id 的默认定义,但它让我震惊因为这种情况极不可能发生,因此手工编码的 lmaprmap 没有任何区别。

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

更新:在评论中,@4caSTLe 询问如果不进行优化会发生什么情况。需要注意的是,“因为它改进了 -O0 代码”并没有让我觉得这是任何事情的合理论据,我看了一下。

在未优化的代码中,显式的rmap定义通过避免与id的额外组合来产生更好的Core:

-- 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 定义最终生成的 Core 大致相同,或者可以说更糟。

-- 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 责骂我的测试不切实际。我要指出的是,内联递归失败在这里并不是真正的问题,因为 dimaplmaprmap 都不是递归的。特别是,简单地以递归方式“使用”其中之一,例如 foo =foldr rmap id 不会干扰内联或优化,并且为 foo 生成的代码与显式和默认的rmap相同。

此外,将类/实例从 l/r 定义拆分为单独的模块对我的测试程序没有任何影响,将其拆分为三个模块也没有什么区别,类、实例和 l/r,因此跨模块边界内联似乎并不是什么大问题。

对于非专门的多态用法,我想它会归结为生成的 Profunctor (->) 字典。我看到以下内容似乎表明,具有默认 lmaprmap 的显式 dimap 会比替代方案生成更好的代码。问题似乎是 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)

关于function - (->) 的 Profunctor 实例同时定义 dimap 和 lmap/rmap 是否有任何原因?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/65872479/

相关文章:

python - 传递带有非字符串关键字的字典以在 kwargs 中运行

python - TypeError:强制转换为 Unicode,需要字符串或缓冲区,找不到 NoneType

没有参数的 Haskell Maybe 加法器

javascript - 如何拆分字符串数组并返回具有键/值对的单个对象

javascript - 单击时终止所有正在运行的 JavaScript 函数

jQuery - 如何在其内部重复一个函数以包含嵌套文件

haskell - 声明一个具有完整定义的子类

haskell - QuickCheck 如何测试每个 sample 的所有属性

functional-programming - java8流样式将键值列表转换为 map ?

Javascript 映射 native 对象方法