cheesenthusiast
cheesenthusiast

Reputation: 139

Function Composition With Monads...not working

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

Answers (1)

Mulan
Mulan

Reputation: 135197

first things first

  1. that 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 ^_^

  1. 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)


  1. 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


  1. 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)
    

  1. That Either you made is probably not the Either functor/monad you're thinking of. See Data.Either (from folktale) for a more complete implementation

  1. Not 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 function a -> a, and f :: b -> c and g :: 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.

  1. starting with Maybe(person)
  2. read person.names property
  3. get the first index of person.names – is it an array or something? or the first letter of the name?
  4. read the .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)
  5. split the value on /
  6. join the values with ''
  7. if we have a value, return it, otherwise return some alternative

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

Related Questions