2

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!

  • Could you provide an example of what the data inside `person` look like? – atomrc Jun 13 '17 at 08:36
  • What have you tested for composition? You should be able to do `R.compose(R.props(..), R.nth(..), R.split(...)` and then `R.map(composedFuncs, Maybe(person))` – atomrc Jun 13 '17 at 08:43
  • 1
    Hint: in modern browsers `.bind(thisArg)` is incredibly fast. at the same time, `.bind(thisArg, ...funcArgs)` is awfully slow. Closures are a better (faster) alternative in this case: `const curry = fn => (...args) => (...brgs) => fn.apply(null, args.concat(brgs));` – Thomas Jun 13 '17 at 08:48
  • That's the point where I dislike FP. When I have to twist and bend, just to fit the paradigm. I like a declarative style, because used right it doubles as commenting the code, but not if I have to use it for every single tiny step/operation/operator. I'd write your code as `const fullName = (person, alternative = "", index=0) => string( fetch(person, ["names", index, "value"]) ) .split("/").join("") || alternative;` where my two utilities `string` and `fetch` don't contain more code than all your utils. But this doesn't really fit the topic, or does it? – Thomas Jun 13 '17 at 09:21
  • @atomrc The data looks like { names: [{value: 'Bob / Johnson', sources : { ... } }, {...etc.} ] }. None of the Ramda functions can access the Maybe value by themselves, so I'm trying to separate map somehow and use a custom compose/pipe function. – cheesenthusiast Jun 13 '17 at 09:26
  • @Thomas Thanks for reminding me of default arguments! I'm basically just trying to refactor old code that was kind of ugly and contained a ton of null checks, in order to add more features later without the headache. You're correct, I replaced it with another headache, but the word 'monad' sounds cool. – cheesenthusiast Jun 13 '17 at 09:38
  • You might want to look at [`R.pipeK`](http://ramdajs.com/docs/#pipeK). – Scott Sauyet Jun 13 '17 at 10:21

1 Answers1

9

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 ^_^

Mulan
  • 129,518
  • 31
  • 228
  • 259
  • 1
    Thanks for the super in-depth answer! Funny how you managed to narrow down the source of my confusion, even though I was not aware enough of the terminology/practice to formulate it correctly in the first place. – cheesenthusiast Jun 13 '17 at 10:57
  • it's more impressive to me that you saw a pattern and recognized that it could be improved, even if you couldn't immediately see how. It's always fun to learn that there's an elegant strategy/technique to handle a particular problem ^_^ – Mulan Jun 13 '17 at 11:20
  • Well it's impressive when people know what they're talking about, too :). Do you have any recommendations for further reading/other learning materials? Also, any opinion on Clojure for learning FP? Thanks again for your great answer. – cheesenthusiast Jun 13 '17 at 11:29
  • JavaScript has first-class functions making it great to learn many functional concepts. If JavaScript is already comfortable for you, I would check out [Professor Frisby's Mostly Adequate Guide to Functional Programming](https://drboolean.gitbooks.io/mostly-adequate-guide/content/) – Or, if you want to explore some of the basics in a very learner-friendly language, I would recommend [Structure and Interpretation of Computer Programs (SICP)](https://github.com/sarabander/sicp) – Mulan Jun 13 '17 at 12:02
  • ... and SICP's [accompanying video lectures](https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-001-structure-and-interpretation-of-computer-programs-spring-2005/video-lectures/) from Sussman/Abelson – wrt Clojure, it's a very nice language from what I've seen, but I've avoided spending too much time with it because I particularly loathe all things Java (including JVM). – Mulan Jun 13 '17 at 12:04
  • 1
    (In hindsight) - No, this is a great answer, clearing up all sorts of issues, some I thought about when I saw this earlier but was on a phone, and some I hadn't even noticed. Two corrections: "`// but that's the same as R.pipe (Maybe,a,b,c) (x)`" (The `Maybe` was at the wrong end for `pipe`.) And " and `f` and `g` are functions (of type `(b -> c)` and `(a -> b)`, respectively.)" (`(a -> b)` does not compose with itself.) – Scott Sauyet Jun 13 '17 at 14:15
  • @ScottSauyet heaps thanks for the technical audit! I did indeed cross my wires between `compose` and `pipe` wrt composition order. Good catch on the function types too – I always appreciate your comments ^_^ – Mulan Jun 13 '17 at 21:18