Reputation: 139
I have some ugly data, that requires a lot of ugly null checks. My goal is to write a suite of functions to access/modify it in a point-free, declarative style, using the Maybe monad to keep null checks to a minimum. Ideally I would be able to use Ramda with the monads, but it's not working out so great.
This works:
const Maybe = require('maybe');
const R = require('ramda');
const curry = fn => (...args) => fn.bind(null, ...args);
const map = curry((fn, monad) => (monad.isNothing()) ? monad : Maybe(fn(monad.value())));
const pipe = (...fns) => acc => fns.reduce((m, f) => map(f)(m), acc);
const getOrElse = curry((opt, monad) => monad.isNothing() ? opt : monad.value());
const Either = (val, d) => val ? val : d;
const fullName = (person, alternative, index) => R.pipe(
map(R.prop('names')),
map(R.nth(Either(index, 0))),
map(R.prop('value')),
map(R.split('/')),
map(R.join('')),
getOrElse(Either(alternative, ''))
)(Maybe(person));
However, having to type out 'map()' a billion times doesn't seem very DRY, nor does it look very nice. I'd rather have a special pipe/compose function that wraps each function in a map().
Notice how I'm using R.pipe() instead of my custom pipe()? My custom implementation always throws an error, 'isNothing() is not a function,' upon executing the last function passed to it.
I'm not sure what went wrong here or if there is a better way of doing this, but any suggestions are appreciated!
Upvotes: 2
Views: 759
Reputation: 135197
first things first
Maybe
implementation (link) is pretty much junk - you might want to consider picking an implementation that doesn't require you to implement the Functor interface (like you did with map
) – I might suggest Data.Maybe from folktale. Or since you're clearly not afraid of implementing things on your own, make your own Maybe ^_^Your map
implementation is not suitably generic to work on any functor that implements the functor interface. Ie, yours only works with Maybe
, but map
should be generic enough to work with any mappable, if there is such a word.
No worries tho, Ramda includes map
in the box – just use that along with a Maybe
that implements the .map
method (eg Data.Maybe referenced above)
Your curry
implementation doesn't curry functions quite right. It only works for functions with an arity of 2 – curry should work for any function length.
// given, f
const f = (a,b,c) => a + b + c
// what yours does
curry (f) (1) (2) (3) // => Error: curry(...)(...)(...) is not a function
// because
curry (f) (1) (2) // => NaN
// what it should do
curry (f) (1) (2) (3) // => 6
There's really no reason for you to implement curry
on your own if you're already using Ramda, as it already includes curry
Your pipe
implementation is mixing concerns of function composition and mapping functors (via use of map
). I would recommend reserving pipe
specifically for function composition.
Again, not sure why you're using Ramda then reinventing a lot of it. Ramda already includes pipe
Another thing I noticed
// you're doing
R.pipe (a,b,c) (Maybe(x))
// but that's the same as
R.pipe (Maybe,a,b,c) (x)
Either
you made is probably not the Either functor/monad you're thinking of. See Data.Either
(from folktale) for a more complete implementationNot a single monad was observed – your question is about function composition with monads but you're only using functor interfaces in your code. Some of the confusion here might be coming from the fact that Maybe
implements Functor
and Monad
, so it can behave as both (and like any other interface it implements) ! The same is true for Either
, in this case.
You might want to see Kleisli category
for monadic function composition, though it's probably not relevant to you for this particular problem.
functional interfaces are governed by laws
Your question is born out of a lack of exposure/understanding of the functor laws – What these mean is if your data type adheres to these laws, only then can it can be said that your type is a functor. Under all other circumstances, you might be dealing with something like a functor, but not actually a functor.
functor laws
where
map :: Functor f => (a -> b) -> f a -> f b
,id
is the identity functiona -> a
, andf :: b -> c
andg :: a -> b
// identity map(id) == id // composition compose(map(f), map(g)) == map(compose(f, g))
What this says to us is that we can either compose multiple calls to map
with each function individually, or we can compose all the functions first, and then map
once. – Note on the left-hand side of the composition law how we call .map
twice to apply two functions, but on the right-hand side .map
was only called once. The result of each expression is identical.
monad laws
While we're at it, we can cover the monad laws too – again, if your data type obeys these laws, only then can it be called a monad.
where
mreturn :: Monad m => a -> m a
,mbind :: Monad m => m a -> (a -> m b) -> m b
// left identity mbind(mreturn(x), f) == f(x) // right identity mbind(m, mreturn) == m // associativity mbind(mbind(m, f), g) == mbind(m, x => mbind(f(x), g))
It's maybe even a little easier to see the laws using Kleisli composition function, composek
– now it's obvious that Monads truly obey the associativity law
monad laws defined using Kleisli composition
where
composek :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)
// kleisli left identity composek(mreturn, f) == f // kleisli right identity composek(f, mreturn) == f // kleisli associativity composek(composek(f, g), h) == composek(f, composek(g, h))
finding a solution
So what does all of this mean for you? In short, you're doing more work than you have to – especially implementing a lot of the things that already comes with your chosen library, Ramda. Now, there's nothing wrong with that (in fact, I'm a huge proponent of this if you audit many of my other answers on the site), but it can be the source of confusion if you get some of the implementations wrong.
Since you seem mostly hung up on the map
aspect, I will help you see a simple transformation. This takes advantage of the Functor composition law illustrated above:
Note, this uses R.pipe
which composes left-to-right instead of right-to-left like R.compose
. While I prefer right-to-left composition, the choice to use pipe
vs compose
is up to you – it's just a notation difference; either way, the laws are fulfilled.
// this
R.pipe(map(f), map(g), map(h), map(i)) (Maybe(x))
// is the same as
Maybe(x).map(R.pipe(f,g,h,i))
I'd like to help more, but I'm not 100% sure what your function is actually trying to do.
Maybe(person)
person.names
propertyperson.names
– is it an array or something? or the first letter of the name?.value
property?? We're you expecting a monad here? (look at .chain
compared to .map
in the Maybe
and Either
implementations I linked from folktale)/
''
That's my best guess at what's going on, but I can't picture your data here or make sense of the computation you're trying to do. If you provide more concrete data examples and expected output, I might be able to help you develop a more concrete answer.
remarks
I too was in your boat a couple of years ago; just getting into functional programming, I mean. I wondered how all the little pieces could fit together and actually produce a human-readable program.
The majority of benefits that functional programming provides can only be observed when functional techniques are applied to an entire system. At first, it will feel like you had to introduce tons of dependencies just to rewrite one function in a "functional way". But once you have those dependencies in play in more places in your program, you can start slashing complexity left and right. It's really cool to see, but it takes a while to get your program (and your head) there.
In hindsight, this might not be a great answer, but I hope this helped you in some capacity. It's a very interesting topic to me and I'm happy to assist in answering any other questions you have ^_^
Upvotes: 9