5

I have a need for a variadic version of R.either. After doing some searching around the web, I have not found a solution. R.anyPass would work but it returns a Boolean instead of the original value. Is there already a solution that I have overlooked? If not, what would be the most optimal way to write a variadic either utility function?

An example:

const test = variadicEither(R.multiply(0), R.add(-1), R.add(1), R.add(2))
test(1) // => 2 
webstermath
  • 555
  • 1
  • 5
  • 14
  • 2
    It's not quite clear what output you want if there is no match. but would this do: `const varEither = (...fns) => (x, res = null, fn = fns.find(fn => res = fn(x))) => res` ? It should run each function once until it hits a truthy result, returning that, or return the result of the last function. Is that what you want? – Scott Sauyet Jul 01 '19 at 19:55
  • The above does satisfy all requirements. I suggest submitting it as an answer so it can be considered for the best implementation. – webstermath Jul 02 '19 at 16:03
  • added it. But I personally prefer the updated answer from customcommander, at least if you're already using Ramda in your code. – Scott Sauyet Jul 02 '19 at 16:13
  • As a side concern, which name would be most appropriate for a variadic or array accepting either? Some that come to mind: eitherV, varEither, eitherAny, anyEither. Thoughts? – webstermath Jul 03 '19 at 18:52
  • 2
    You know what they say, right: The two hardest problems in software are cache invalidation, naming things, and off-by-one errors. – Scott Sauyet Jul 03 '19 at 19:50
  • @webstermath I would really like for your question to be updated to show an example use of such a function and what you expect the output to be. – Mulan Jul 04 '19 at 14:09
  • Should Ramda be petitioned to include an official variadic either in its library? – webstermath Jul 04 '19 at 18:07

5 Answers5

4

You could use a combination of reduce + reduced:

const z = (...fns) => x => reduce((res, fn) => res ? reduced(res) : fn(x), false, fns);

console.log(
  z(always(0), always(10), always(2))(11),
  z(always(0), always(''), always(15), always(2))(11),
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>
<script>const {reduce, reduced, always} = R;</script>

(previous attempt)

I would do something like this:

const z = unapply(curry((fns, x) => find(applyTo(x), fns)(x)));

console.log(

  z(always(0), always(15), always(2))(10),
  z(always(0), always(''), always(NaN), always(30), always(2))(10),

);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>
<script>const {unapply, curry, find, applyTo, always} = R;</script>

There are three main caveats to this though!

  1. You have to call z in two "passes", i.e. z(...functions)(x)
  2. Although it should be easy to add, I didn't care about the case where no function "matches"
  3. Perhaps not a big deal but worth noting: a matching predicate will be executed twice
customcommander
  • 17,580
  • 5
  • 58
  • 84
  • Thanks, for the solution. However, I do need a solution that calls each function at most once. Also if there are only two functions passed in or none of the functions return a truthy response, the solution above gives the error: `find(...) is not a function` – webstermath Jul 01 '19 at 19:02
  • @webstermath Yep I'm aware that's the case #2 in my original attempt. Please see updated answer. – customcommander Jul 01 '19 at 20:54
  • Thanks, your new solution does satisfy all requirements and I do like the implementation. However, as there are now several solutions that satisfy all requirements of a variadic either, I am going to wait for some more up votes to help me decide which one to accept. – webstermath Jul 02 '19 at 16:01
  • 1
    nice application of transducer techniques – Mulan Jul 04 '19 at 16:16
3

Without Ramda ...

I'd probably write this using simple recursion -

const always = x =>
  _ => x

const identity = x =>
  x

const veither = (f = identity, ...more) => (...args) =>
  more.length === 0
    ? f (...args)
    : f (...args) || veither (...more) (...args)

const test =
  veither
    ( always (0)
    , always (false)
    , always ("")
    , always (1)
    , always (true)
    )

console .log (test ())
// 1

But there's more to it ...

R.either has to be one the more eccentric functions in the Ramda library. If you read the documentation closely, R.either has two (2) behaviour variants: it can return -

  1. a function that that passes its argument to each of the two functions, f and g, and returns the first truthy value - g will not be evaluated if f's result is truthy.

  2. Or, an applicative functor

The signature for R.either says -

either : (*… → Boolean) → (*… → Boolean) → (*… → Boolean)

But that's definitely fudging it a bit. For our two cases above, the following two signatures are much closer -

// variant 1
either : (*… → a) → (*… → b) → (*… → a|b)

// variant 2
either : Apply f => f a → f b → f (a|b)

Let's confirm these two variants with simple tests -

const { always, either } =
  R

const { Just, Nothing } =
  folktale.maybe

// variant 1 returns a function
const test =
  either
    ( always (0)
    , always (true)
    )

console.log(test()) // => true

// variant 2 returns an applicative functor
const result =
  either
    ( Just (false)
    , Just (1)
    )

console.log(result) // => Just { 1 }
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/folktale/2.0.1/folktale.min.js"></script>

Double down ...

Now let's make a super-powered veither that offers the same dual capability as R.either -

const vor = (a, ...more) =>
  a || vor (...more)

const veither = (f, ...more) =>
  f instanceof Function
    // variant 1
    ? (...args) =>
        f (...args) || veither (...more) (...args)
    // variant 2
    : liftN (more.length + 1, vor) (f, ...more)

It works just like R.either except now it accepts two or more arguments. Behaviour of each variant is upheld -

// variant 1 returns a function
const test =
  veither
    ( always (false)
    , always (0)
    , always ("fn")
    , always (2)
    )

test () // => "fn"

// variant 2 returns an applicative functor
veither
  ( Just (0)
  , Just (false)
  , Just ("")
  , Just ("ap")
  , Just (2)
  )
  // => Just { "ap" }

You can view the source for R.either and compare it with veither above. Uncurried and restyled, you can see its many similarities here -

// either : (*… → a) → (*… → b) → (*… → a|b)

// either : Apply f => f a -> f b -> f (a|b)

const either = (f, g) =>
  isFunction (f)
    // variant 1
    ? (...args) =>
        f (...args) || g (...args)
    // variant 2
    : lift (or) (f, g)

Expand the snippet below to verify the results in your own browser -

const { always, either, liftN } =
  R

const { Just, Nothing } =
  folktale.maybe

const vor = (a, ...more) =>
  a || vor (...more)

const veither = (f, ...more) =>
  f instanceof Function
    ? (...args) =>
        f (...args) || veither (...more) (...args)
    : liftN (more.length + 1, vor) (f, ...more)

// variant 1 returns a function
const test =
  veither
    ( always (false)
    , always (0)
    , always ("fn")
    , always (2)
    )

console .log (test ()) // "fn"

// variant 2 returns an applicative functor
const result =
  veither
    ( Just (0)
    , Just (false)
    , Just ("")
    , Just ("ap")
    , Just (2)
    )

console .log (result) // Just { "ap" }
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/folktale/2.0.1/folktale.min.js"></script>

With hindsight and foresight ...

With one little trick, we can skip all the ceremony of reasoning about our own veither. In this implementation, we simply make a recurring call to R.either -

const veither = (f, ...more) =>
  more.length === 0
    ? R.either (f, f) // ^_^
    : R.either (f, veither (...more))

I show you this because it works nicely and preserves the behaviour of both variants, but it should be avoided because it builds a much more complex tree of computations. Nevertheless, expand the snippet below to verify it works -

const { always, either } =
  R

const { Just, Nothing } =
  folktale.maybe

const veither = (f, ...more) =>
  more.length === 0
    ? either (f, f)
    : either (f, veither (...more))

// variant 1 returns a function
const test =
  veither
    ( always (false)
    , always (0)
    , always ("fn")
    , always (2)
    )

console .log (test ()) // "fn"

// variant 2 returns an applicative functor
const result =
  veither
    ( Just (0)
    , Just (false)
    , Just ("")
    , Just ("ap")
    , Just (2)
    )

console .log (result) // Just { "ap" }
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/folktale/2.0.1/folktale.min.js"></script>
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • Excellent and informative answer! Really felt I learned a lot not just about 'either' but Ramda and functional programming in general. This does seem like a great solution and possibly the solution. I just want to see if others agree. Furthermore, it looks like your vote is for the name 'veither', no? – webstermath Jul 04 '19 at 18:05
  • @webstermath, I'm really happy to help. Ramda has many other variadic functions so I'm not sure why `R.either` in particular is limited to just two arguments. This function is particularly difficult to give a name to though because the behaviour is very complex and it does different things depending on the inputs. A better design would probably separate the two variants and give each of them a unique name, imo. – Mulan Jul 04 '19 at 18:19
  • On second thought, I don't know why the applicative behaviour is here at all. Having a library function as a shortcut for `R.lift(R.or)` is basically saving you nothing at the cost of tremendous complexity. If you have functions, use `R.either(f,g)(x)` – if you have applicative functors, you would instead use `R.lift(R.or)(f,g)`. I don't use Ramda in practice, so I don't know how many people rely on the dual behaviour of `R.either`. Based on the other answers here, I would be surprised if others are even aware of the applicative variant. – Mulan Jul 04 '19 at 18:32
  • 3
    I honestly don't recall why `either` got the behavior it did. There was a fair bit of early churn around the functions like `or`/`either`, `and`/`both`. I honestly doubt that it's used much in this case. But many of our other functions, which operate on various types do make sense. We supply implementations of `map`, for instance, for arrays, objects, and functions, and delegate to object methods for other Functors. This makes sense to me. `either`, not so much. I would probably reach for `lift(or)` before remembering that `either` has that behavior. – Scott Sauyet Jul 05 '19 at 13:54
2

Update:

I personally prefer the previous version, it is much more cleaner, but you could eventually ramdify it even more (you cannot write it entirely point-free due to the recursion):

const either = (...fns) => R.converge(R.either, [
  R.head,
  R.pipe(
    R.tail,
    R.ifElse(R.isEmpty, R.identity, R.apply(either)),
  ),
])(fns);

const result = either(R.always(null), R.always(0), R.always('truthy'))();

console.log(`result is "${result}"`);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>

Update:

as per @customcommander's suggestion, recursion may be nested in the right branch to have a much cleaner script...

const either = (...fns) => (...values) => {
  const [left = R.identity, ...rest] = fns;
  
  return R.either(
    left, 
    rest.length ? either(...rest) : R.identity,
  )(...values);
}

const result = either(R.always(null), R.always(0), R.always('truthy'))();

console.log(`result is "${result}"`);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>

You could possibly call R.either recursively...

const either = (...fns) => (...values) => {
  const [left = R.identity, right = R.identity, ...rest] = fns;
  
  return R.either(left, right)(...values) || (
    rest.length ? either(...rest)(...values) : null
  );
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
Hitmands
  • 13,491
  • 4
  • 34
  • 69
  • 1
    That’s interesting. I wonder if you could nest your recursion in the right branch of an outer `R.either`. e.g. `R.either(R.either(...), either(...))` – customcommander Jul 03 '19 at 09:00
  • definitely doable, it also makes the script easier to read and maintain. Great suggestion! – Hitmands Jul 03 '19 at 13:29
  • 1
    Thanks @Hitmands. This is a great solution. It is now even harder for me to choose which solution to accept. I am looking for up votes and comments to argue for which solution is most consistent with functional/Ramda principles before I choose. – webstermath Jul 03 '19 at 18:49
  • well, then I hope you don't mind if I complicate your choice even more by adding another solution :) – Hitmands Jul 04 '19 at 07:30
1

The 1st function findTruthyFn is used to find a truty function or take the last function if none of them return a truthy result.

The 2nd function fn gets a list of functions, and the value, uses findTruthyFn to find the function, and apply it to the value to get the result.

const { either, pipe, applyTo, flip, find, always, last, converge, call, identity } = R

const findTruthyFn = fns => either(
  pipe(applyTo, flip(find)(fns)), 
  always(last(fns))
)

const fn = fns => converge(call, [findTruthyFn(fns), identity])

const check = fn([x => x + 1, x => x - 1])

console.log(check(1))
console.log(check(-1))
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>

If you want to limit the number of calls to of matching functions to one, you can memoize the functions before testing them:

const { either, pipe, applyTo, flip, find, always, last, converge, call, identity, map, memoizeWith } = R

const findTruthyFn = fns => either(
  pipe(applyTo, flip(find)(map(memoizeWith(identity), fns))), 
  always(last(fns))
)

const fn = fns => converge(call, [findTruthyFn(fns), identity])

const check = fn([
  x => console.log('1st called') || x + 1, 
  x => console.log('2nd called') || x - 1
])

console.log(check(1))
console.log(check(-1))
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
Ori Drori
  • 183,571
  • 29
  • 224
  • 209
  • Thanks for the solution. I tested it and it does work. However, it calls the matching predicate twice and I need for each function to be called at most once. – webstermath Jul 01 '19 at 19:08
  • 1
    We can memoize the functions to limit the number of calls to one. See update. – Ori Drori Jul 01 '19 at 19:09
1

This (non-Ramda) version is quite simple, and it seems to do what's needed:

const varEither = (...fns) => (x, res = null, fn = fns.find(fn => res = fn(x))) => res

If you need to supply multiple parameters to the resulting function, it wouldn't be much harder:

const varEither = (...fns) => (...xs) => {
  let res = null;
  fns .find (fn => res = fn (...xs) )
  return res;
}

But I've got to say calling fns.find for its side-effects does seem quite dirty, which might make me choose customcommander's updated version instead of this.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103