ramda / ramda

:ram: Practical functional Javascript

Home Page:https://ramdajs.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Proposal: contramap

mimshwright opened this issue · comments

On a recent project I found myself trying to create a function that applies a function to the results of another function. Something like:
(g) => (f) => (...argsForF) => g(f(...argsForF))

But after some research I found that this is similar to contravariant's contramap.

This is the function I ended up using in the end.

// contramap :: (b -> c) -> (a -> b) -> a -> c
const contramap =
  <ReturnG, ReturnF>(f: Unary<ReturnG, ReturnF>) =>
  <ParamsG>(g: Variadic<ParamsG, ReturnG>): Variadic<ParamsG, ReturnF> =>
  (...xs) =>
    f(g(...xs));
// without types:  
// (f) => (g) => (...xs) => f(g(...xs));

This allows things like:

const contramap = (f) => (g) => (...xs) => f(g(...xs));
const square = x => x * x;
const squareUnder100 = contramap(R.min(100))(square);
squareUnder100(7) // 49
squareUnder100(12) // 100

(Not claiming that this should be the final code, and not even sure it satisfies the FantasyLand spec, but it should illustrate my intention)

I did some searching on NPM and couldn't find much that matches this outside of bigger libraries so I'm bringing this to you so you can tell me "We already have that and it's called ______ lol!"

Props to @forrest-akin for the intuition to use contravariant.

Is there a difference between the proposed contramap and compose/pipe? For instance, couldn't you do:

const square = x => x * x;
const squareUnder100 = R.compose(R.min(100), square);

squareUnder100(7) // 49
squareUnder100(12) // 100

If your second function is a unary, you can also use R.o which only accepts two functions and only passes one argument

const square = x => x * x;
const squareUnder100 = R.o(R.min(100), square);

squareUnder100(7) // 49
squareUnder100(12) // 100

@jlissner, I think you're right. I was really overthinking it.
Actually, I read more about contravariants, then i got sick and had fever nightmares about contramap all night long! 💀 But somehow, I think I understand it all better.

Some takeaways:

  1. In the same way that fmap is equivalent to compose when the functors are unary functions, contramap is equivalent to a pipe when the contravariant functors are a unary functions. I just came to this conclusion the long, hard way.
  2. Contravariant may not make much sense after all in a library with no ADTs. I am now thinking of it as a type that encapsulates a partially applied compose. When you're already working with compose and pipe, this is probably not really necessary.
  3. i think a curried 2-arity compose is what I need. As you pointed out, R.o does this so that's what I'll use.

This article on Type Classes was really helpful.