Convert value to option within pipeline based off of predicate on pipeline data
What is the problem this feature would solve?
I have an Effect Pipeline that partway through wants to perform an operation, but only if the value at that point of the pipeline matches a condition. To use a dummy example:
pipe(
fetchValueFromDatabase(),
Effect.map(incrementValue)
// the next line only should execute if value is now > 10
Effect.map(saveValueInOtherDatabase)
)
Effect.when() seems to be made exactly for this, but the predicate is a LazyArg and thus cannot check anything on the value itself.
If I tried to map the new value into a boolean and then use Effect.whenEffect(), the value itself is then no longer available to pass into the function (as the pipeline now has something of type Effect.Effect<R, E, boolean>
What is the feature you are proposing to solve the problem?
A new function whenSelf + equivalent unlessSelf that act like when/whenEffect, but perform the predicate on self instead of independently:
<R, E, A>(self: Effect<R, E, A>, predicate: (a: A) => boolean): Effect<R, E, Option.Option<A>>
You could then use this in the pipeline without much trouble:
pipe(
Effect.succees(5),
Effect.whenSelf(isDivisibleByFive),
Effect.map(Option.map(Effect.log('buzz'))
)
Alternatively, a function Option.fromPredicate could do the same thing by acting exactly like Option.filter but on a non-optional value:
<A>(self: A, predicate: (a: A) => boolean): Option<A>
Which could then be used in a pipeline like:
pipe(
Effect.succees(3),
Option.fromPredicate(isDivisibleBy3)
Effect.map(Option.map(Effect.log('fizz'))
)
What alternatives have you considered?
I've extracted the check into a different function so that the value can be lifted out of a pipeline scope:
const validate = (foo: number) => Effect.succeed(foo).pipe(Effect.when(() => foo === 5))
pipe(
Effect.succeed(3),
Effect.map(validate),
Effect.map(Option.map(...))
)
I can do something similar by inlining everything:
pipe(
Effect.succeed(3),
Effect.flatMap((foo) => Effect.when(Effect.succeed(foo), () => foo === 5)),
Effect.map(Option.map(...))
)
These both work but feel (admitedly to my relatively amateur knowledge to this framework) rather clunky, considering the value we want to check is right there at that exact point of the pipeline.
Similarly, we can map it into an Option within the pipeline using Option.some, followed by Option.filter:
pipe(
Effect.succeed(3),
Effect.map(Option.some),
Effect.map(Option.filter((foo) => foo >1)),
...
But considering in this situation we only care about the the value if the predicate is true, it would be nice if you could go directly to this like you can with Option.fromNullable or Option.fromIterable
Lastly, although I have not tried it I have considered the Do Simulation to save the variable and the predicate result in the scope, and then map both into Effect.when. This feels like overkill for more simple situations like the ones above where variables aren't actually needed beyond the predicate check and following step in the pipeline
I see where you're coming from but it seems to me that there could be a slight misunderstanding on the semantics of when, the when function executes the effect when a specific predicate matches, for example:
Effect.sync(() => console.log("OK")).pipe(Effect.when(() => false))
Will not execute console.log("OK") because the predicate returns false.
A when function that acts on the result of the effect is meaningless because in order to have the result of the effect the effect needs to be executed.
What you want is a conditional continue, the best way I can think to represent such behaviour is:
import { Cause, Effect, Random } from "effect"
// Effect.Effect<never, never, Option<number>>
const program = Random.nextIntBetween(0, 100).pipe(
Effect.filterOrFail((n) => n > 10, () => new Cause.NoSuchElementException()),
Effect.map((n) => n + 1),
Effect.optionFromOptional
)
that I have to say I strictly prefer over the proposed method as it doesn't force you to work with a nested Option, this could be improved by adding an overload to filterOrFail that defaults to failing with NoSuchElementException so that the above could be wrote as:
import { Cause, Effect, Random } from "effect"
// Effect.Effect<never, never, Option<number>>
const program = Random.nextIntBetween(0, 100).pipe(
Effect.filterOrFail((n) => n > 10),
Effect.map((n) => n + 1),
Effect.optionFromOptional
)