0

The Problem:

I'm learning functional programming

Just kidding, but also...

I have a helper function that composes a function with itself over and over again until some condition is met. Like f(f(f(f(f(f(f(x))))))) or compose(f,f,f,f,f,f,f)(x), except it keeps going unless told to stop.

The way I've implemented it, it doesn't really feel like composition (and perhaps that's the wrong word to use here regardless)

This is my current solution:

const selfComposeWhile = curry(
  (pred, fn, init) => {
    let prevVal = null;
    let nextVal = init;
    while(prevVal == null || pred(prevVal, nextVal)){
      prevVal = nextVal;
      nextVal = fn(nextVal);
    }
    return nextVal;
  }
);

and here it is in use:

const incOrDec = ifElse(gt(30), inc, dec);
console.log(
  selfComposeWhile(lt, incOrDec, 0)
); // -> 29

I don't want to use recursion as JavaScript doesn't have proper tail recursion and the namesake of this site (Stack Overflow) is a real concern for how I use this.

There's nothing wrong with it as is, but I've been trying to learn functional programming techniques by applying them to a dummy problem and this is one of the few places my code stands out as decidedly imperative.

I also have

useWith(selfComposeWhile, [pipe(nthArg(1), always)]);

That takes a predicate that is only concerned with the nextVal, which seems like the more general case of this.

The Question:

Can anybody think of a more functional (sans recursion) way to write selfComposeWhile and its cousin?

Mrk Sef
  • 7,557
  • 1
  • 9
  • 21
  • Note the similarities with [`R.until`](https://ramdajs.com/docs/#until). It won't solve your problem, as the predicate it takes is unary. But the implementation is similar. As Ori Drori says, `unfold` is likely your best bet among Ramda functions, but I have to say, I wish we had chosen a different API for `unfold`, such as [this one](https://stackoverflow.com/a/49543489/1243641) from user @Thankyou. – Scott Sauyet Feb 09 '21 at 21:07
  • @ScottSauyet I'm noticing that a lot of the names/naming conventions make sense with a little bit of thought, but I wouldn't have thought to search the Docs/Google with them. Part of the disadvantage of not coming from a background in math, perhaps. – Mrk Sef Feb 09 '21 at 21:32
  • I founded the library and have been involved in almost every naming decision, but I still have some that escape me, or whose name doesn't always make sense. `fold/unfold` make sense once you hear them, and `until` is pretty well named. But coming up with them, or understanding `assoc` or `juxt` takes quite a bit more effort. I don't know of any solutions. – Scott Sauyet Feb 09 '21 at 22:23

2 Answers2

2

R.unfold does mostly what you, it accepts a seed value (init), and transforms it, and on each iteration it returns the current value, and the new seed value. On each iteration you need to decide to continue or stop using a predicate.

The main difference between your function, and R.unfold is that the last one produces an array, and this is easily solvable with R.last:

const { curry, pipe, unfold, last } = R

const selfComposeWhile = curry(
  (pred, fn, init) => pipe(
    unfold(n => pred(n, fn(n)) && [n, fn(n)]),
    last
  )(init)
)

const { ifElse, gt, inc, dec, lt } = R

const incOrDec = ifElse(gt(30), inc, dec)

console.log(selfComposeWhile(lt, incOrDec, 0)) // -> 29
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js" integrity="sha512-rZHvUXcc1zWKsxm7rJ8lVQuIr1oOmm7cShlvpV0gWf0RvbcJN6x96al/Rp2L2BI4a4ZkT2/YfVe/8YvB2UHzQw==" crossorigin="anonymous"></script>
Ori Drori
  • 183,571
  • 29
  • 224
  • 209
  • 1
    Wow. Perfect. I knew it felt like something I couldn't be alone in wanting, glad to see Ramda basically has it built-in. :) – Mrk Sef Feb 09 '21 at 20:48
  • Ah, this runs f(n) twice per unfolding. Not too hard to fix that though. Thanks again :) – Mrk Sef Feb 10 '21 at 04:34
0

The solution I've settled on for now.

I've taken selfComposeWhile and named it unfoldUntil. I'm not sure that's the best name as it doesn't return a list. It is basically R.until where the predicate can access the previous and the next value.

To bring them a bit more in alignment, I've changed my while behavior into until behavior (R.complement the predicate).


unfoldUntil

If it were typed:

unfoldUntil: <T>(
  pred: (p:T, n:T) => boolean, 
  fn:   (a:T) => T, 
  init: T
) => T

Implemented

const unfoldUntil = curry( 
  (pred, fn, init) => pipe(
    unfold(n =>
      isNil(n) ? 
      false : 
      call((next = fn(n)) => 
        (pred(n, next) ? 
        [next, null] : 
        [next, next])
      )
    ),
    last
  )(init)
);

Notes: This will never pass null/undefined into the transformation function (fn). You can use a transformation that returns null as a stopping condition and be returned the previous value. Otherwise, you'll be returned the first value of next that causes the predicate to return true.

Mrk Sef
  • 7,557
  • 1
  • 9
  • 21