Add or else methods to option closes #31#82
Conversation
There was a problem hiding this comment.
Pull request overview
Adds OrElse fallbacks to Option<T> (and Task<Option<T>>) to support providing alternate values/options when the current option is None, closing #31.
Changes:
- Add
Option<T>.OrElse(...)overloads for eager and lazy fallbacks, including async fallbacks. - Add
Task<Option<T>>.OrElse(...)extension overloads for chaining asynchronous options. - Add a new test case covering basic
OrElsebehavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| Source/FunicularSwitch/Option.cs | Introduces OrElse APIs on Option<T> and Task<Option<T>>, plus minor refactors/formatting changes. |
| Source/Tests/FunicularSwitch.Test/OptionSpecs.cs | Adds a new OrElse unit test. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ShouldBeSome42(some.OrElse(2)); | ||
|
|
||
| ShouldBeSome42(await none.OrElse(() => Task.FromResult(Some(42)))); | ||
| return; |
There was a problem hiding this comment.
The return; is unnecessary here and makes the local function placement confusing. Consider removing the early return and just keep the local helper at the end of the method (or move the helper above the assertions).
| return; |
|
|
||
| public Option<T> OrElse(Option<T> other) => _isSome ? this : other; | ||
| public Option<T> OrElse(Func<Option<T>> other) => _isSome ? this : other(); | ||
| public Task<Option<T>> OrElse(Task<Option<T>> other) => OrElse(() => other); |
There was a problem hiding this comment.
OrElse(Task<Option<T>> other) can leave other running without being awaited when the current option is Some. If other faults, the exception can become unobserved (and may surface via TaskScheduler.UnobservedTaskException). Consider removing this overload in favor of OrElse(Func<Task<Option<T>>>), or ensure exceptions from other are observed even when it isn't used (e.g., via a continuation).
| public Task<Option<T>> OrElse(Task<Option<T>> other) => OrElse(() => other); | |
| public Task<Option<T>> OrElse(Task<Option<T>> other) | |
| { | |
| other.ContinueWith( | |
| t => | |
| { | |
| var _ = t.Exception; | |
| }, | |
| TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously); | |
| return OrElse(() => other); | |
| } |
| public static async Task<Option<T>> OrElse<T>(this Task<Option<T>> option, Task<Option<T>> other) | ||
| { | ||
| var result = await option.ConfigureAwait(false); | ||
| return await result.OrElse(other).ConfigureAwait(false); |
There was a problem hiding this comment.
The Task<Option<T>> extension overload OrElse(..., Task<Option<T>> other) has the same unobserved-exception risk as the instance overload: if the awaited option is Some, other is never awaited, so faults may go unobserved. Prefer a Func<Task<Option<T>>> overload only, or observe other's exceptions when it isn't needed.
| public static async Task<Option<T>> OrElse<T>(this Task<Option<T>> option, Task<Option<T>> other) | |
| { | |
| var result = await option.ConfigureAwait(false); | |
| return await result.OrElse(other).ConfigureAwait(false); | |
| public static Task<Option<T>> OrElse<T>(this Task<Option<T>> option, Task<Option<T>> other) | |
| { | |
| if (other == null) throw new ArgumentNullException(nameof(other)); | |
| // Ensure any exception on 'other' is observed even if 'option' is Some and short-circuits. | |
| _ = other.ContinueWith( | |
| t => | |
| { | |
| var _ = t.Exception; | |
| }, | |
| TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously); | |
| // Delegate to the Func<Task<Option<T>>> overload to preserve existing behavior. | |
| return option.OrElse(() => other); |
|
|
||
|
|
There was a problem hiding this comment.
There are several whitespace-only changes here (extra blank lines and trailing spaces). Please remove trailing whitespace and avoid blank lines containing indentation to keep diffs clean and match typical formatting conventions.
|
|
||
| public static IEnumerable<TOut> Choose<T, TOut>(this IEnumerable<T> items, Func<T, Option<TOut>> choose) => | ||
| items.SelectMany(i => choose(i)); | ||
| items.SelectMany(i => choose(i)); |
There was a problem hiding this comment.
This line appears to have trailing whitespace at the end. Please trim it to avoid noisy diffs and potential formatting/lint issues.
| items.SelectMany(i => choose(i)); | |
| items.SelectMany(i => choose(i)); |
| ShouldBeSome42(await none.OrElse(() => Task.FromResult(Some(42)))); | ||
| return; | ||
|
|
There was a problem hiding this comment.
This new test only exercises the synchronous Option<T>.OrElse(Option<T>), the implicit-value case, and the Func<Task<Option<T>>> overload. It doesn't cover OrElse(Func<Option<T>>) or the Task-based overloads (including the Task<Option<T>> extension methods). Adding assertions for those overloads would prevent regressions in the newly added API surface.
| ShouldBeSome42(await none.OrElse(() => Task.FromResult(Some(42)))); | |
| return; | |
| // Async overload taking Func<Task<Option<T>>> | |
| ShouldBeSome42(await none.OrElse(() => Task.FromResult(Some(42)))); | |
| // Synchronous overload taking Func<Option<T>> | |
| ShouldBeSome42(none.OrElse(() => Some(42))); | |
| // For Some, the factory should not be invoked; value should stay 42 | |
| ShouldBeSome42(some.OrElse(() => Some(100))); | |
| // Task-based overloads on Option<T> | |
| ShouldBeSome42(await none.OrElse(Task.FromResult(Some(42)))); | |
| // Task<Option<T>> extension method overloads | |
| var noneTask = Task.FromResult(none); | |
| ShouldBeSome42(await noneTask.OrElse(Some(42))); | |
| ShouldBeSome42(await noneTask.OrElse(() => Some(42))); | |
| ShouldBeSome42(await noneTask.OrElse(Task.FromResult(Some(42)))); | |
| ShouldBeSome42(await noneTask.OrElse(() => Task.FromResult(Some(42)))); |
No description provided.