Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OfType for Option<T> #810

Open
csillikd opened this issue Jan 16, 2025 · 5 comments
Open

OfType for Option<T> #810

csillikd opened this issue Jan 16, 2025 · 5 comments

Comments

@csillikd
Copy link

The OfType extension method would make the code more succinct, like a .Where(v => v is T).Select(v => (v as T)!)

@bash
Copy link
Member

bash commented Jan 16, 2025

We have DownCast<TResult>.From and UpCast<TResult>.From for this which makes your intentions more explicit (downcast can fail while upcast is infallible).

@bash
Copy link
Member

bash commented Jan 16, 2025

I think our main concern was to have upcasts type-checked which the UpCast class achieves :)
Original discussion with our considerations can be found here: #688

Maybe this is also a documentation issue? There's currently nothing pointing you from Option / Result / Either to the UpCast / DownCast classes.

What do you think @FreeApophis?

@FreeApophis
Copy link
Member

FreeApophis commented Jan 17, 2025

IEnumerable<T> implements Cast and OfType, however it is true that there is no functional difference for Option<T>

@bash
Copy link
Member

bash commented Jan 17, 2025

One of the issues with Cast and OfType as extension methods is that it introduces two generic parameters. Since C# doesn't have partial type inference this makes them inconvenient to use.
The enumerable extensions work around this by using the non-generic IEnumerable type instead. We could do something similar i.e. IOption. This is Idea 1. The downside of this is that the option has to be boxed :/

Idea 2:
One alternative that I've come up with is to add a parameter to help type inference:

public static class OptionExtensions
{
    public static Option<TResult> UpCast<TSource, TResult>(this Option<TSource> option, TypeMarker<TResult> marker)
        where TSource : TResult
        where TResult : notnull;
}

public readonly struct TypeMarker<TResult>;

// Usage
Option<string> option = ...;
option.UpCast(default(TypeMarker<object>));

We could make this more natural to call by introducing a factory function for the type marker, something like Cast.To<object>() or similar:

Option<string> option = ...;
option.UpCast(Cast.To<object>());

This would make the cast methods a lot more discoverable than the separate classes.

Idea 3:
Similar to the simplified monad constructors for Either (see #809) we could do casting as a two-step thing:

// Usage:
Option<string> option = ...;
option.Cast().Up<object>();

public static partial class OptionExtensions
{
    public static OptionCastBuilder<TSource> Cast<TSource>(this Option<TSource> option)
        where TSource : class
        => new(option);
}

public readonly struct OptionCastBuilder<TSource>(Option<TSource> option)
    where TSource : class
{
    public Option<TResult> Up<TResult>()
        // where TSource : TResult
        where TResult : class
        => option.Select(x => (TResult)(object)x);

    public Option<TResult> Down<TResult>()
        where TResult : class, TSource
        => option.SelectMany(x => Option.FromNullable(x as TResult));
}

The downside here is that there's no way for us to do the TSource : TResult constraint for upcasting. However... we could possibly add that back with an analyzer :)

@csillikd
Copy link
Author

I prefer idea 1. despite of boxing. It would be similar to IEnumerable, and the user code stays consistent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants