vkhorikov/CSharpFunctionalExtensions

Behavior with Nullable Interface while implicit maybe-generation

LaszloLueck opened this issue · 7 comments

Currently if facing in an issue / behavior with the implicit Maybe-generation.
As for an example:

    public interface ITest
    {
        
    }

        private Maybe<ITest> CallMethod()
        {
            ITest? test = null;
            return test;
        }

The return test resulted in an error cannot convert expression ITest? to return type Maybe<ITest>.
Is there not an implicit conversion for nullable interfaces?
The above described problem is an easy example. In my code i receive (from Quartz.net) e.g. a Task<ITrigger?> and, also for async operations, the implicit conversion does not work.

Regards
Laszlo

What is working atm for me is to manual resolve the nullable.
As for an example:

public interface ITest
    {
        
    }

        private Maybe<ITest> CallMethod()
        {
            ITest? test = null;
            return test == null ? Maybe<ITest>.None : Maybe<ITest>.From(test);
        }

In my opinion the main problem is, that for the current implementation of the implicit conversion, the Type ITest? is another thing as ITest so the conversion does not work.

I have written two static extension functions

        public static Maybe<TOut> MaybeValue<TIn, TOut>([AllowNull] this TIn? value) where TIn : TOut 
        {
            return value == null ? Maybe<TOut>.None : Maybe<TOut>.From(value);
        }

        public static TOut ResolveNullable<TIn, TOut>([AllowNull] this TIn? nullable, TOut alternative, Func<TIn, TOut, TOut> action) where TIn : class
        {
            return nullable
                .MaybeValue<TIn, TIn>()
                .Match(d => action.Invoke(d, alternative), () => alternative);
        }

The first one resolves the nullable in a specific Maybe (as described in my posts above).
The second resolves a nullable object and let you decide what is the best result when null.
It´s not optimal (the first need in any case the input and return type, there is no autoresolve) and the second could be a little bit complecated but here are a view examples of how to use:

define an interface:
public interface ITest{}

Somewhere in your code;

ITest? x = null;
var foo = x.MaybeValue<ITest, ITest>();

foo is of type Maybe<ITest>

And for the second one (which used the MaybeValue method:

//ok, that is simple, x is null
StringValue? x = null;

//also cool, y is a StringValue object, but the property Value is null
StringBuilder? y = new StringValue();

//Now you can use:
var foo = y.ResolveNullable(string.Empty, (value, alternative) => value.Value ?? alternative)

The result is
a) alternative (string.Empty) when y is null
b) alternative (string.Empty) when y.Value is null
c) property value when a and b not null

if the nullable object is e.g. a DateTimeValue? (here the value is not a nullable) you can pass the inner alternative away. e.g.

var foo = d.Date.ResolveNullable(new DateTime(1970, 1, 1), (value, _) => value.Value)

The ResolveNullable is (for me) better readable syntactic sugar for StringValue?.Value??alternative

OK, that was easy...
The signature for MaybeValue has changed to:

        public static Maybe<TOut> MaybeValue<TOut>([AllowNull] this TOut? value) 
        {
            return value == null ? Maybe<TOut>.None : Maybe<TOut>.From(value);
        }

Because the In-Type is the same as the out-type but as nullable one.
So the using of that in ResolveNullable is also a little bit different:

        public static TOut ResolveNullable<TIn, TOut>([AllowNull] this TIn? nullable, TOut alternative, Func<TIn, TOut, TOut> action) where TIn : class
        {
            return nullable
                .MaybeValue()
                .Match(d => action.Invoke(d, alternative), () => alternative);
        }

And the usage as example from above:

define an interface:
public interface ITest{}

Somewhere in your code;

ITest? x = null;
var foo = x.MaybeValue();

foo is of type Maybe<ITest>

This is not related to nullable/non-nullable reference types (? reference types behave the same as non-? reference types behind the scenes; ? is just for compiler analysis). The issue is with interfaces -- the implicit conversion doesn't work for them.

In your example, instead of this:

return test == null ? Maybe<ITest>.None : Maybe<ITest>.From(test);

just use:

return Maybe.From(test);

or

return Maybe.From<ITest>(test);

Hi Vladimir,
thanks for your response.

But this, with a nullable interface want work!
return Maybe.From(test);

And this
return Maybe<ITest>.From(test);
also did not working. The result ist a Maybe<ITest?> e.g. you have a nullable maybe (and thats definitely not what you want).
As you can see below (a screenshot of my ide):
image

ITest is a public interface ITest{};

You can put the null value ITest in your Maybe.From with the result of a nullable Maybe.
If you resolve this back to a concrete value, you receive a Nullable ITest.

My goal is, to eleminate the nullables, also from an (possible nullable) interface, that was the reason of my question.
For that i´ve build some helper methods to avoid the usage of ? or ?? as often as necessary.

Here are the both helper methods as i used it in my project (one with none Func<> and one with none Action<>).

        public static TOut ResolveNullable<TIn, TOut>([AllowNull] this TIn? nullable, [DisallowNull] TOut alternative, Func<TIn, TOut, TOut> action)
        {
            return nullable is not null ? action.Invoke(nullable, alternative) : alternative;
        }
        
        public static TOut ResolveNullable<TIn, TOut>([AllowNull] this TIn? nullable, [DisallowNull] TOut alternative, Func<TIn, TOut, TOut> some, Func<TOut, TOut> none)
        {
            return nullable is not null ? some.Invoke(nullable, alternative) : none.Invoke(alternative);
        }      

And here is how i use this:

        public ITest Foo()
        {
            ITest? nullable = null;
            ITest alternative = new Test();
            return nullable.ResolveNullable(alternative, (maybeValue, _) => maybeValue);
        }

Regards
Laszlo

Sorry for late replies.

You can have it both ways:

Maybe<ITest> maybe = Maybe.From(test);

or

Maybe<ITest?> maybe = Maybe.From(test);

The compiler will agree with either option.

Your suggestion is to add extensions for T?, in addition to Maybe<T>? If so, I don't think it's a good idea, from the point of keeping the library focused on the one way of doing things. T? is an alternative to Maybe<T>. It makes sense to support the conversion from one to the other, but not re-implement the existing extension methods on top of both.

Hi Vladimir,
thanks for your response.
I think you´re right. It is not the best idea to wrap an extension like maybe over a low level nullable handling like in c# 8.0 (which is exactly designed for).
As i coded in Scala, there was the best practice to handle nulls as None<T> or not nulls as Some<T> to avoid nulls from your code.

I leave my function extensions to remove the nullables in my project. This is primarily syntactic sugar for me, to avoid the handling with ?? and ?.

Thanks for your patience,

Regards,
Laszlo