1

Long story short

I'm wandering if I should think of using contramap when I find myself writing code like (. f) . g, where f is in practice preprocessing the second argument to g.

Long story longer

I will describe how I came up with the code that made me think of the question in the title.

Initially, I had two inputs, a1 :: In and a2 :: In, wrapped in a pair (a1, a2) :: (In,In), and I needed to do two interacting processing on those inputs. Specifically, I had a function binOp :: In -> In -> Mid to generate a "temporary" result, and a function fun :: Mid -> In -> In -> Out to be fed with binOp's inputs and output.

Given the part "function fed with inputs and output of another function" above, I though of using the function monad, so I came up with this,

finalFun = uncurry . fun =<< uncurry binOp

which isn't very complicated to read: binOp takes the inputs as a pair, and passes its output followed by its inputs to fun, which takes the inputs as a pair too.

However, I noticed that in the implementation of fun I was actually using only a "reduced" version of the inputs, i.e. I had a definition like fun a b c = fun' a (reduce b) (reduce c), so I thougth that, instead of fun, I could use fun', alongside reduce, in the definition of finalFun; I came up with

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

which is far less easy to read, especially because it features an un-natural order of the parts, I believe. I could only think of using some more descriptive name, as in

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

Since preReduce is actually pre-processing the 2nd and 3rd argument of fun', I was wandering if this is the right moment to use contramap.

Enlico
  • 23,259
  • 6
  • 48
  • 102
  • 1
    Not sure about `contramap`, but another alternative you might want to consider is `finalFun = curry $ fun' <$> uncurry binOp <*> (reduce.fst) <*> (reduce.snd)`. – bradrn Feb 21 '21 at 10:31
  • This actually sounds somewhat profunctor-ish, where you want to transform the input *and* the output. For functions, `dimap f g h == h . g . f` – chepner Feb 21 '21 at 14:25
  • 1
    @chepner I think you meant `dimap f g h == g . h . f` – duplode Feb 21 '21 at 14:31
  • Yeah, I always have trouble remembering whether `f` comes first or last, then I got lazy and just rattled them off in right-to-left order. – chepner Feb 21 '21 at 15:28
  • @bradrn it looks like the applicative style just doesn't stick in my head. – Enlico Feb 21 '21 at 15:31
  • 2
    But there's no `Contravariant` instance for functions...? – Daniel Wagner Feb 21 '21 at 15:40
  • @DanielWagner Oops! Thanks for correcting this collective thinko. I guess we can follow chepner's lead and use `lmap` instead. – duplode Feb 21 '21 at 15:49

1 Answers1

2

lmap f . g (rather than contramap, as that would also require an Op wrapper) might indeed be an improvement over (. f) . g in terms of clarity. If your readers are familiar with Profunctor, seeing lmap will promptly suggest the input to something is being modified, without the need for them to perform dot plumbing in their heads. Note it isn't a widespread idiom yet, though. (For reference, here are Serokell Hackage Search queries for the lmap version and the dot section one.)

As for your longer example, the idiomatic thing to do would probably be not writing it pointfree. That said, we can get a more readable pointfree version by changing the argument order of fun/fun', so that you can use the equivalent Applicative instance instead of the Monad one:

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

Pointfree function (<*>) is arguably less hard to make sense of than pointfree function (=<<), as its two arguments are computations in the relevant function functor. Also, this change removes the need for the dot section trick. Lastly, since (.) is fmap for functions, we can further rephrase finalFun as...

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

... thus getting an applicative style expression, which in my (not so popular!) opinion is a reasonably readable way of using the function applicative. (We might further streamline it using liftA2, but I feel that in this specific scenario makes it less obvious what is going on.)

Enlico
  • 23,259
  • 6
  • 48
  • 102
duplode
  • 33,731
  • 7
  • 79
  • 150