Is there a Variadic Version of either (R.either)?

五迷三道 提交于 2021-02-07 15:09:58

问题


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 

回答1:


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



回答2:


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>



回答3:


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>



回答4:


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>



回答5:


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.



来源:https://stackoverflow.com/questions/56840515/is-there-a-variadic-version-of-either-r-either

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!