Есть ли причина, по которой экземпляр 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 спросил, что происходит без оптимизации. С оговоркой, что «потому что это улучшает
-O0
code" не кажется мне веским аргументом ни для чего , я посмотрел.
В неоптимизированном коде явное определение дает лучшее ядро, избегая дополнительной композиции с
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)