16

Consider the following code in C#.

public int Foo(int a)
{
    // ...
}

// in some other method

int? x = 0;

x = Foo(x);

The last line will return a compilation error cannot convert from 'int?' to 'int' which is fair enough. However, for example in Haskell there is Maybe which is a counterpart to Nullable in C#. Since Maybe is a Functor I would be able to apply Foo to x using fmap. Does C# have a similar mechanism?

Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
Bobby
  • 785
  • 5
  • 9
  • The easiest way is `if (x.hasValue) x = (int?) f(x.value)`... – user202729 Jan 28 '18 at 15:50
  • 1
    @user202729 - There is no reason for the `(int?)` return cast. – H H Jan 28 '18 at 15:58
  • I guess Haskell uses type inference there to make `Foo` have the type `a -> a`. But if this was an explicit `int -> int` (or `Just int -> Just int`?), I’m sure that Haskell also wouldn’t allow you to pass a `Maybe int` in that case. And that’s exactly what C# does here: The function cannot take a nullable int, so you have to pass it an actual int. – poke Jan 28 '18 at 16:07
  • `Nullable` does not have a method like `fmap`(or in the csharp world: `Select`). You can quite easily add an extension method for it, or even roll out your own option-type (and you can even make it so it can take any T, not just value-types). @poke haskell has a generic function `fmap` with the signature `Functor f => (a -> b) -> (f a -> f b)`, so you can use it to turn a function `Int -> Int` to a function `Maybe Int -> Maybe Int`. It's basically the same as `Select()` you know in LINQ, just with generalized types(works for any `Functor`) – Mor A. Jan 28 '18 at 16:09
  • So basically `Func fmap(Func func) where TIn : struct where TOut : struct => (x) => x.HasValue ? func(x.Value) : (TOut?)null;` – poke Jan 28 '18 at 16:17
  • 1
    Yes, that's the signature for `fmap` for the `Nullable` type(if it was an instance method), basically the same as Willem's answer. The signature is just like `Select`, except instead of `IEnumerable` we have `Nullable`. If you do use the name `Select` you can even use it as a LINQ query(and maybe add `Where` and `SelectMany`). [Here's a github repo I found where someone rolled out his own `Maybe` type](https://github.com/jb55/Data.Maybe.cs) – Mor A. Jan 28 '18 at 16:20

3 Answers3

15

We can implement such functionality ourselves:

public static class FuncUtils {

    public static Nullable<R> Fmap<T, R>(this Nullable<T> x, Func<T, R> f)
        where T : struct
        where R : struct {
        if(x != null) {
            return f(x.Value);
        } else {
            return null;
        }
    }

}

Then we can use it with:

int? x = 0;
x = x.Fmap(Foo);

It will thus call the function Foo if x is not null. It will wrap the result back in a Nullable<R>. In case x is null, it will return a Nullable<R> with null.

Or we can write a more equivalent function (like fmap in Haskell) where we have a function Fmap that takes as input a Func<T, R> and returns a Func<Nullable<T>, Nullable<R>> so that we can then use it for a certain x:

public static class FuncUtils {

    public static Func<Nullable<T>, Nullable<R>> Fmap<T, R>(Func<T, R> f)
        where T : struct
        where R : struct {
        return delegate (Nullable<T> x) {
            if(x != null) {
                return f(x.Value);
            } else {
                return null;
            }
        };
    }

}

We can then use it like:

var fmapf = FuncUtils.Fmap<int, int>(Foo);
fmapf(null);  // -> null
fmapf(12);    // -> Foo(12) as int?
Willem Van Onsem
  • 443,496
  • 30
  • 428
  • 555
  • Really? Can you call `x.Fmap(foo)` if `x` is null. Surely x dot anything will throw a null pointer exception if `x` is null? – Adam Jan 28 '18 at 20:32
  • 2
    @Adam: the above is with `Nullable` types (so if the type is `int?`, etc.). And furthermore no! Not every `null.foo(..)` will throw a null pointer exception, since you can introduce extension methods. – Willem Van Onsem Jan 28 '18 at 20:33
  • 2
    @Adam That's actually one of the really cool features of extension methods - externally, they look like instance methods, but they can be called on `null` objects without error! 'course it means that the extension method itself needs to check if the `this` parameter (which has a name instead of "`this`") is `null` or not. – Nat Jan 29 '18 at 01:27
  • Why use the `delegate` syntax instead of a lambda? Is there a functional difference? – jpmc26 Jan 29 '18 at 02:14
  • 2
    @Nat I don't consider that a "really cool feature" but an "unfortunate side effect". The introduction of extension methods changes some pretty fundamental properties of the language. Before extension methods, you could rely on the fact that after `x.Foo()`, `x` could be assumed non-null. – Derek Elkins left SE Jan 29 '18 at 02:59
  • @DerekElkins I tend to enjoy it because the type can assert its own `null`-handling behavior. For example, I enjoy being able to write `.ToString()`-like methods that return meaningful results on null values, e.g. `null.ToString()` might return `"null"`. Then numeric nullables can basically behave like `NaN`'s (or `0`'s, etc., if appropriate). Then for action-performing methods, e.g. `.Update()`, `null.Update()` can simply not perform an action. It's relieving for callers to not have to worry about handling the call-target's `null` behavior for the call target. – Nat Jan 29 '18 at 03:17
  • @jpmc26 probably for making the type explicit? One can argue that it reads better. Could be rewritten as `public static Func Fmap(Func f) where T : struct where R : struct => x => x != null ? (R?) f(x.Value) : null;` otherwise. – Andrew Savinykh Jan 29 '18 at 06:43
12

Functor

Not only can you turn Nullable<T> into a functor, but C# actually understands functors, enabling you to write something like this:

x = from x1 in x
    select Foo(x1);

If you prefer method call syntax, that's also possible:

x = x.Select(Foo);

In both cases, you need an extension method like this:

public static TResult? Select<T, TResult>(
    this T? source,
    Func<T, TResult> selector) where T : struct where TResult : struct
{
    if (!source.HasValue)
        return null;

    return new TResult?(selector(source.Value));
}

Monad

Not only does C# understand functors, but it understands monads as well. Add these SelectMany overloads as well:

public static TResult? SelectMany<T, TResult>(
    this T? source,
    Func<T, TResult?> selector)
    where T : struct
    where TResult : struct
{
    if (!source.HasValue)
        return null;

    return selector(source.Value);
}

public static TResult? SelectMany<T, U, TResult>(
    this T? source,
    Func<T, U?> k,
    Func<T, U, TResult> s)
    where T : struct
    where TResult : struct
    where U : struct
{
    return source
        .SelectMany(x => k(x)
            .SelectMany(y => new TResult?(s(x, y))));
}

This enables you to write queries like this:

var result = from x in (int?)6
             from y in (int?)7
             select x * y;

Here, result is an int? containing the number 42.

Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
  • I have a somewhat related question here - https://stackoverflow.com/questions/62895696/string-isnullorempty-monad. I'd appreciate any insights you might have. – Bondolin Jul 14 '20 at 13:37
4

If you have an extension method:

public int Foo(this int a)
{
    // ...
}

you can do:

// in some other method

int? x = 0;

x = x?.Foo();

The ?. operator will ensure Foo is called only if x is not null. If x is null, it is not called (a null of the return type is used instead).


Otherwise, the canonical way to write it is naturally:

x = x.HasValue ? Foo(x.Value) : (int?)null;

Of course you can create your own Maybe infrastructure if you will (Willem Van Onsem's answer).

Jeppe Stig Nielsen
  • 60,409
  • 11
  • 110
  • 181