1

I'm trying to write a general monoidal pattern in C#, starting with a homogeneous function combining two non-null values, and returning either value if the other is null, or the combined value if neither is, or null. So I have this:

public static Func<TIn?, TIn?,TIn?> Drop2<TIn>(Func<TIn, TIn, TIn> f)
{
    return (lhs, rhs) =>
    {
        if (lhs == null && rhs == null)
        {
            return default;
        }

        if (lhs == null && rhs != null)
        {
            return rhs;
        }

        if (rhs == null && lhs != null)
        {
            return lhs;
        }

        return f(lhs, rhs);
    };
}

This looks fine and it even compiles, but when I try to use it, two odd things happen.

    Func<int, int, int> sum = (lhs, rhs) => lhs + rhs;

    var sumNonNull = DropNullable.Drop2(sum);

The Intellisense for sumNonNull shows as Func<int, int, int>?, not the expected Func<int?, int?, int?>, and I can't pass in null as either argument for sumNonNull (can't convert from int? to int).

Should this work? What am I missing?

Thanks in advance for any help

user118165
  • 67
  • 4
  • 2
    You should probably start by declaring `sum` a `Func` instead of a `Func` :) – Mathias R. Jessen Nov 07 '22 at 11:15
  • 1
    Note, that `Func sum = (lhs, rhs) => lhs + rhs;` doesn't allow you to use `null` (`int` can't be `null`), so `var sumNonNull = DropNullable.Drop2(sum);` is useless. – Dmitry Bychenko Nov 07 '22 at 11:20
  • 1
    Note also that the "lifting" of operators to nullable value types is built into the language, and with different semantics (`5 + (int?) null == null` despite there being no explicit `operator+` for `int?`), so anything along these lines you write yourself has quite some potential for adding confusion. For production work you're better off using the `??` operator where appropriate (or, of course, an actual functional language like F#). – Jeroen Mostert Nov 07 '22 at 11:20
  • 1
    See [this](https://stackoverflow.com/a/72911969/2501279) and [this](https://stackoverflow.com/a/73635064/2501279) answers, they will provide some context about what is happening here. – Guru Stron Nov 07 '22 at 12:38

1 Answers1

0

I think you want something like this:

    public static Func<TIn?, TIn?, TIn?> Drop2<TIn>(Func<TIn?, TIn?, TIn?> f)
    {
        return (lhs, rhs) =>
              lhs is null ? rhs
            : rhs is null ? lhs
            : f(lhs, rhs);
    }

note TIn? in both input argument and the result (both of them can be null). Then you can use it as follow:

// note int? - sumInt accepts nulls (int? is a short for Nullable<int>)
Func<int?, int?, int?> sumInt = (a, b) => a + b;

Console.WriteLine(Drop2(sumInt)(null, 123));
Console.WriteLine(Drop2(sumInt)(456, null));
Console.WriteLine(Drop2(sumInt)(456, 123));
Console.WriteLine(Drop2(sumInt)(null, null));

// strings can be null
Func<string, string, string> sumStr = (a, b) => a + b;

Console.WriteLine(Drop2(sumStr)(null, "123"));
Console.WriteLine(Drop2(sumStr)("456", null));
Console.WriteLine(Drop2(sumStr)("456", "123"));
Console.WriteLine(Drop2(sumStr)(null, null));
Dmitry Bychenko
  • 180,369
  • 20
  • 160
  • 215
  • Thanks Dmitry, this works, as Jeroen points out, constraining the type of Tin to a struct allows the returned function to be well defined and also solves the problem. – user118165 Nov 07 '22 at 11:40
  • @user118165: yes, you add the `struct` constraint, but in this case you'll not be able to use `sumStr` since `string` is not `struct` – Dmitry Bychenko Nov 07 '22 at 11:57