问题
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!
- You have to call
z
in two "passes", i.e.z(...functions)(x)
- Although it should be easy to add, I didn't care about the case where no function "matches"
- 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 -
a function that that passes its argument to each of the two functions,
f
andg
, and returns the first truthy value -g
will not be evaluated iff
's result is truthy.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