12

I am learning about pointfree functions and am trying to implement this recursive null remover in that style.

Works, but is not pointfree:

function removeNulls(obj) {
  return R.ifElse(
    R.either(R.is(Array), R.is(Object)),
    R.pipe(
      R.filter(R.pipe(R.isNil, R.not)),
      R.map(removeNulls)
    ),
    R.identity
  )(obj)
}

module.exports = removeNulls

The following is my non-functioning attempt at it:

const removeNulls = R.ifElse(
  R.either(R.is(Array), R.is(Object)),
  R.pipe(
    R.filter(R.pipe(R.isNil, R.not)),
    // throws `ReferenceError: removeNulls is not defined`
    R.map(removeNulls)
  ),
  R.identity
)
Archimedes Trajano
  • 35,625
  • 19
  • 175
  • 265
Henry Marshall
  • 850
  • 2
  • 8
  • 21
  • 1
    You cannot, since JavaScript does not provide a way to lazily declare arguments. You might have luck using the `Y` combinator, but that'll get really ugly. – Bergi Dec 05 '16 at 23:17
  • @Bergi Thanks for the tip, I'll be leaving it as is. I'd love to learn more about using a Y combinator, if only for future use. It appears that [ramda lacks a `Y` combinator](https://gist.github.com/Avaq/1f0636ec5c8d6aed2e45), but I don't know where to go from there. It is a bit hard to google for... – Henry Marshall Dec 05 '16 at 23:31
  • @Bergi is correct, you can't do it with a const... however you can do it if you separate the action from applying it: const filterNotNull = filter(pipe(isNil, not)) const recurseAction = action => ifElse( either(is(Array), is(Object)), pipe( action, map(action) ), identity ) – Nigel Benns Dec 05 '16 at 23:39
  • @NigelBenns Your `recurseAction` isn't pointfree, and also it's not recursive - it should be `map(recurseAction(action))` – Bergi Dec 05 '16 at 23:45
  • It's easy enough to implement a version of the Y-combinator in JS. But it is not for anything practical. – Scott Sauyet Dec 06 '16 at 00:49
  • So many people pushing for point-free before understanding basic functional concepts. Ow, my aching heart. – Mulan Dec 06 '16 at 17:53

2 Answers2

23

Luckily JavaScript has resources to deal with its lack of laziness. So, it's completely possible to declare a recursive pointfree solution by using lambda functions in the following way: a => f(a). Just replace R.map(removeNull) with R.map(a => removeNull(a)).

const removeNulls = R.ifElse(
    R.either(R.is(Array), R.is(Object)),
    R.pipe(
        R.filter(R.pipe(R.isNil, R.not)),
        R.map(a => removeNulls(a))
    ),
    R.identity
)

In your case, I'll recommend you to use R.reject which is the oposite to R.filter. Since you are negating the predicate, R.filter(R.pipe(R.isNil, R.not)) is equal to R.reject(R.isNil)

const removeNulls = R.ifElse(
    R.either(R.is(Array), R.is(Object)),
    R.pipe(
        R.reject(R.isNil),
        R.map(a => removeNulls(a))
    ),
    R.identity
)

Finally, this function has the following structure ifElse(predicate, whenTrue, identity) which is equal to when(predicate, whenTrue)

const removeNulls = R.when(
    R.either(R.is(Array), R.is(Object)),
    R.pipe(
        R.reject(R.isNil),
        R.map(a => removeNulls(a))
    )
)

Simplified version, regarding the comment from Declan Whelan, since arrays are Object

const removeNulls = R.when(
    R.is(Object),
    R.pipe(
        R.reject(R.isNil),
        R.map(a => removeNulls(a))
    )
)
yosbel
  • 339
  • 1
  • 4
  • A minor simplification is possible because an Array is an Object: const removeNulls = R.when( R.is(Object), R.pipe( R.reject(R.isNil), R.map(a => removeNulls(a)) ) ) – Declan Whelan Sep 05 '18 at 00:53
  • but this isn't pointfree though, OPs main requirement has not been meant. Good optimisations though. :) – jlouzado Feb 18 '20 at 07:42
  • @jlouzado yes it is – yosbel Feb 19 '20 at 02:46
  • @yosbel the `a` in the map is a point. If you just use `R.map(removeNulls)` it won't work. – jlouzado Feb 20 '20 at 03:46
  • @jlouzado so it is a limitation of the language, the answer is as point-free as the language allows. What is your proposal? – yosbel Feb 25 '20 at 02:04
  • @yosbel the definition of point-free is not language specific. since js doesn't support it natively, a more complete answer would a y-combinator based approach as mentioned in one of the [comments](https://stackoverflow.com/questions/40985044/pointfree-recursion-in-js-with-ramda/41000959?noredirect=1#comment69180334_40985044). I agree that your answer is as pointfree as the language allows. – jlouzado Feb 28 '20 at 10:34
3

Javascript's lack of laziness is a double killer here: you can't call it when its const because you're in the same scope and its trying to resolve resolveNulls in its definition.

Also, you can't just map(recurseAction(action)) because the definition itself will blow the stack, so you need to wrap it in another scope to do so:

const {ifElse, always, tap, apply, either, is, isNil, not, pipe, filter, map, identity} = require('ramda')

const filterNotNull = filter(pipe(isNil, not))
const log = tap(console.log)

const recurseAction =
  action =>
    ifElse(
      either(is(Array), is(Object)),
      pipe(
        action,
        map(a => recurseAction(action)(a))
      ),
      identity
    )

const removeNulls = recurseAction(filterNotNull)

const a = {
  a: null,
  b: "blah",
  c: 2,
  d: undefined,
  e: {
    meow: null,
    blah: undefined,
    jim: 'bob'
  }
}

const b = removeNulls(a)
console.log(b)
Nigel Benns
  • 1,236
  • 11
  • 14