Ramda with FP Types
Asked Answered
C

1

6

Recently I have decided to switch from lodash to ramda to play with functional way of composing my logic. I love it! After some extensive digging into FP I have found that's it's not only about handy pure/point free utilities (ramda), but more about complex (at least for me) math abstractions (fantasy-land). I don't get all of it, but Either and Task pattern looks very handy. Problem is that I am not sure how to merge it with ramda utilities. I know about ramda-fantasy, but it's no longer maintained. Ramda-fantasy suggested libraries doesn't work the same way as ramda-fantasy. With all this new information about Monads/Monoids/Functors types I am completely lost.

For example, what the convention for this?

Right('boo')
  .map(x => x + '!')
  .map(x => x.toUpperCase())

vs 

R.pipe(
  R.map(x => x + '!')
  R.map(x => x.toUpperCase())
)(Right('boo'))

Am I don't need ramda If I will decide to switch to Monads all the way?

Conceited answered 16/7, 2021 at 2:53 Comment(0)
F
4

One way to think about it is to think about types versus functions.

Ramda offers a large collection of utility functions. They operate on arrays, on objects, on functions, on strings, on numbers, etc. But they also operate on user-defined types. So in your example, R.map operates on anything which matches the Functor specification. If the implementation of Either you use matches that specification, then Ramda's map will operate on it.

But Ramda does not supply types. It works with the built-in types such as Object, Array, Function, etc. But -- arguably outside Lens -- it does not supply any types of its own. Libraries such as Folktale provide large collections of types, such as Maybe, Result, Validation, Task and Future; more dedicated ones such as Fluture provide powerful versions of one specific type (Future). All of these types implement the Functor specification. A very incomplete list of such implementations is supplied by FantasyLand.

These two notions, functions on an abstract type and collections of types are complementary. A Ramda function which works on any functor will work on whatever version of Either you use (so long as it matches the specification.) More on this relationship is in this StackOverflow Q+A.

The question compared these two snippets:

Right('boo')
  .map(x => x + '!')
  .map(x => x.toUpperCase())

and

R.pipe(
  R.map(x => x + '!')
  R.map(x => x.toUpperCase())
)(Right('boo'))

But neither is how I would think of the problem from a Ramda perspective. Ramda is all about functions. It supplies functions and expects you to use them to build more sophisticated functions, and then to use those to build even higher level functions.

If we wrote this function:

const bigBang = pipe(
  map (x => x + '!'),
  map (x => x .toUpperCase ())
)

or perhaps this version

const bigBang = pipe (
  map (concat (__, '!')),
  map (toUpper)
)

Then this function is now available to use on many types. For example:

bigBang (['boo', 'scared', 'you'])     //=> ['BOO!', 'SCARED!', 'YOU!']
bigBang ({a: 'boo', b: 'ya'})          //=> {a: 'BOO!', b: 'YA!}
bigBang ((s) => s .repeat (2)) ('boo') //=> 'BOOBOO!'
bigBang (Right ('boo'))                //=> Right ('BOO!') 
bigBang (Left ('oops'))                //=> Left ('oops') 
bigBang (Just ('boo'))                 //=> Just ('BOO!') 
bigBang (Nothing())                    //=> Nothing ()
bigBang (Future ('boo'))               //=> Future ('BOO!')

The first three -- Array, Object, and Function implementations -- are supplied by Ramda. But the others still work since Ramda interoperates with the FantasyLand Functor specification. And it will work if you supply your own map method on your type (or even better a fantasy-land/map method.)

So no, you don't need Ramda to work with Monads or with other abstract types. You can work directly with their implementation. But Ramda offers some nice ways to interoperate with them in a generic manner.

Fettling answered 16/7, 2021 at 13:15 Comment(6)
I guess to put it in yet another way - a Functor is broadly an interface. There is a set of behaviour expectations but for a moment we can leave them aside. A Functor must have a .map() method. Ramda's R.map() works with anything that has a .map() method. This means it works with any Functor. Very very simply the same sort of thing can be implemented as map = fn = x => x.map(fn) - you can run this against a plain array, or Either. Because both conform to the Functor specifications.Douglasdouglashome
@VLAZ: Yes, but it gets most interesting when these interfaces are combined. R.lift, for instance, should work on anything that has a map and an ap method. fp-ts goes much further down this route. I would like to see Ramda follow there too.Fettling
I would love to see Ramda do this, as well :)Douglasdouglashome
@VLAZ: I've been working quietly on a side-project -- which might eventually fold into a later version of Ramda, might become it's own thing, or might just fade away -- which combines Ramda's style of utility functions with typeclasses and a number of useful types, something akin to fp-ts but in pure JS and with a pluggable architecture. If you define a new typeclass of your own, you should be able to open up Array or Either to implement if for those. Perhaps one day it will see the light of day.Fettling
Thanks for letting me know. I'd be very interested in whatever shape it takes. Fingers crossed it does get out in some form.Douglasdouglashome
@VLAZ: It's slow going, but fun. I'm hoping it can take on the Expression Problem directly.Fettling

© 2022 - 2024 — McMap. All rights reserved.