Есть ли причина, по которой экземпляр Profunctor (->) определяет как dimap, так и lmap/rmap?

В исходном коде на Hackage я прочитал это:

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

но стандартные реализации // для Profunctorпотребуется просто определить либо оба lmapа также rmap, или же dimap; определять их все необязательно.

Есть ли причина, по которой они все определены?

1 ответ

Как комментирует @FyodorSoikin, намерение, вероятно, состояло в том, чтобы определения и, закодированные вручную, были бы более эффективными, чем определения по умолчанию, основанные на .

Однако, используя приведенную ниже тестовую программу, я попытался определить экземпляр со всеми тремя из //, только и только / и ядром для тестовых функций , и 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, это кажется мне крайне маловероятным, поэтому кодируется вручную и не имеет никакого значения.

Я думаю, причина в том, что даже чрезвычайно опытные программисты на Haskell, такие как Эдвард Кметт, все еще недооценивают компилятор и выполняют ненужную ручную оптимизацию своего кода.

Обновление: в комментарии @4castle спросил, что происходит без оптимизации. С оговоркой, что «потому что это улучшает -O0code" не кажется мне веским аргументом ни для чего , я посмотрел.

В неоптимизированном коде явное определение дает лучшее ядро, избегая дополнительной композиции с 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)

в то время как явное определение в конечном итоге дает ядро, которое примерно такое же или, возможно, хуже.

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

Как следствие приведенных выше определений, явное лучше, чем по умолчанию, из-за дополнительных 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 отругал меня за нереалистичный тест. Я укажу, что отказ от встроенной рекурсии здесь не является проблемой, поскольку ни один из , или не является рекурсивным. В частности, просто "использование" одного из них рекурсивным образом, например foo = foldr rmap idне мешает встраиванию или оптимизации, а сгенерированный код для fooто же самое с явным и по умолчанию.

Кроме того, разделение класса/экземпляра из определений / на отдельные модули не имеет значения для моей тестовой программы, равно как и разделение ее на три модуля: класс, экземпляр и l/ r, так что не похоже, что встраивание через границы модулей здесь является большой проблемой.

Для неспециализированного полиморфного использования, я думаю, все сводится к Profunctor (->)созданный словарь. Я вижу следующее, которое, кажется, показывает, что явный dimapпо умолчанию и 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-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)
Другие вопросы по тегам