Nullable 可以用作 C# 中的函子吗?

Can Nullable be used as a functor in C#?

考虑以下 C# 代码。

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

// in some other method

int? x = 0;

x = Foo(x);

最后一行将 return 编译错误 cannot convert from 'int?' to 'int' 这很公平。但是,例如在 Haskell 中有 Maybe,它对应于 C# 中的 Nullable。由于 MaybeFunctor,因此我可以使用 fmapFoo 应用于 x。 C#有没有类似的机制?

我们可以自己实现这样的功能:

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;
        }
    }

}

然后我们可以使用它:

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

如果x不是null,它将因此调用函数Foo。它会将结果包装回 Nullable<R>。如果 xnull,它将 return 一个 Nullable<R>null

或者我们可以编写一个更等效的函数(如 Haskell 中的 fmap),其中我们有一个函数 Fmap,它以 Func<T, R> 和 returns a Func<Nullable<T>, Nullable<R>> 这样我们就可以将它用于某个 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;
            }
        };
    }

}

然后我们可以像这样使用它:

var fmapf = FuncUtils.Fmap<int, int>(Foo);
fmapf(null);  // -> null
fmapf(12);    // -> Foo(12) as int?

如果你有扩展方法:

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

你可以做到:

// in some other method

int? x = 0;

x = x?.Foo();

?. 运算符将确保仅当 x 不为空时才调用 Foo。如果 x 为 null,则不调用它(而是使用 return 类型的 null)。


否则,规范的写法自然是:

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

当然,如果愿意,您可以创建自己的 Maybe 基础设施(Willem Van Onsem 的回答)。

函子

您不仅可以将 Nullable<T> 变成仿函数,而且 C# 实际上理解仿函数 ,使您能够编写如下内容:

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

如果您更喜欢方法调用语法,也可以:

x = x.Select(Foo);

在这两种情况下,您都需要这样的扩展方法:

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));
}

单子

C# 不仅理解函子,而且理解 monad。还要添加这些 SelectMany 重载:

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))));
}

这使您能够像这样编写查询:

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

这里,result 是一个包含数字 42int?