Подсказывает ли использование правоприменительного оператора композиции, что можно использовать contramap?

Короче говоря

Я блуждаю, должен ли я думать об использовании contramap, когда я пишу код, подобный (. f) . g, где f на практике выполняет предварительную обработку второго аргумента для g.

Длинная история длиннее

Я опишу, как я придумал код, который заставил меня задуматься над вопросом в заголовке.

Изначально у меня было два входа, a1 :: In и a2 :: In, заключенных в пару (a1, a2) :: (In,In), и мне нужно было выполнить две взаимодействующие обработки этих входов. В частности, у меня была функция binOp :: In -> In -> Mid для создания временного результата и функция fun :: Mid -> In -> In -> Out для передачи входных и выходных данных binOp.

Учитывая, что функция части получает входные данные и выходные данные другой функции выше, я думал об использовании функциональной монады, поэтому я придумал это,

finalFun = uncurry . fun =<< uncurry binOp

что не очень сложно прочитать: binOp принимает входные данные как пару и передает свой вывод, а затем свои входные данные в fun, который также принимает входные данные как пару.

Однако я заметил, что в реализации fun я фактически использовал только сокращенную версию входных данных, т. е. у меня было определение, подобное fun a b c = fun' a (reduce b) (reduce c), поэтому я подумал, что вместо fun я мог бы использовать fun' вместе с reduce в определении. finalFun; я придумал

finalFun = (. both reduce) . uncurry . fun' =<< uncurry binOp

который гораздо труднее читать, особенно потому, что в нем, как мне кажется, неестественный порядок частей. Я мог только подумать об использовании более описательного имени, как в

finalFun = preReduce . uncurry . fun' =<< uncurry binOp
    where preReduce = (. both reduce)

Поскольку preReduce на самом деле предварительно обрабатывает 2-й и 3-й аргумент fun', я сомневался, подходит ли сейчас момент для использования contramap.


person Enlico    schedule 21.02.2021    source источник
comment
Не уверен насчет contramap, но другой вариант, который вы, возможно, захотите рассмотреть, это finalFun = curry $ fun' <$> uncurry binOp <*> (reduce.fst) <*> (reduce.snd).   -  person bradrn    schedule 21.02.2021
comment
На самом деле это звучит несколько профункторно, когда вы хотите преобразовать ввод и вывод. Для функций dimap f g h == h . g . f   -  person chepner    schedule 21.02.2021
comment
@chepner Я думаю, ты имел в виду dimap f g h == g . h . f   -  person duplode    schedule 21.02.2021
comment
Да, мне всегда трудно вспомнить, идет ли f первым или последним, потом я поленился и просто отбарабанил их в порядке справа налево.   -  person chepner    schedule 21.02.2021
comment
@bradrn похоже, что аппликативный стиль просто не укладывается у меня в голове.   -  person Enlico    schedule 21.02.2021
comment
Но нет экземпляра Contravariant для функций...?   -  person Daniel Wagner    schedule 21.02.2021
comment
@DanielWagner Ой! Спасибо за исправление этого коллективного мышления. Думаю, мы можем последовать примеру Чепнера и использовать вместо него lmap.   -  person duplode    schedule 21.02.2021


Ответы (1)


lmap f . g ( а не contramap, поскольку для этого также потребуется обертка Op) действительно может быть улучшением по сравнению с (. f) . g с точки зрения ясности. Если ваши читатели знакомы с Profunctor, то, увидев lmap, сразу же предложат, что входные данные для чего-то изменены, без необходимости выполнять точечную сантехнику в своих головах. Обратите внимание, что это еще не широко распространенная идиома. (Для справки, вот поисковые запросы Serokell Hackage для версия lmap и первая точка.)

Что касается вашего более длинного примера, идиоматично, вероятно, было бы не писать его без точек. Тем не менее, мы можем получить более удобочитаемую версию без точек, изменив порядок аргументов fun/fun', чтобы вы могли использовать эквивалент экземпляр Applicative вместо экземпляра Monad:

binOp :: In -> In -> Mid
fun' :: In -> In -> Mid -> Out
reduce :: In -> In

both f = bimap f f

finalFun :: (In, In) -> Out
finalFun = uncurry fun' . both reduce <*> uncurry binOp

Бесточечная функция (<*>), возможно, менее сложна для понимания, чем бесточечная функция (=<<), поскольку два ее аргумента являются вычислениями в функторе соответствующей функции. Кроме того, это изменение устраняет необходимость в трюке с точечным сечением. Наконец, поскольку (.) для функций равно fmap, мы можем перефразировать finalFun как...

finalFun = uncurry fun' <$> both reduce <*> uncurry binOp

... таким образом получая выражение аппликативного стиля, которое, по моему (не столь популярному!) мнению, является достаточно читаемым способом использования функции аппликатив. (Мы могли бы упростить его, используя liftA2, но я чувствую, что в этом конкретном сценарии становится менее очевидным, что происходит.)

person duplode    schedule 21.02.2021
comment
Действительно, очень элегантное решение. Я попробую это! - person Enlico; 21.02.2021