Подтвердить что ты не робот

Может ли Nullable использоваться как функтор в С#?

Рассмотрим следующий код в С#.

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

// in some other method

int? x = 0;

x = Foo(x);

Последняя строка вернет ошибку компиляции cannot convert from 'int?' to 'int', которая достаточно справедлива. Однако, например, в Haskell существует Maybe, который является эквивалентом Nullable в С#. Поскольку Maybe является Functor, я мог бы применить Foo к x с помощью fmap. Имеет ли С# аналогичный механизм?

4b9b3361

Ответ 1

Мы можем реализовать такую ​​функциональность самостоятельно:

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

Таким образом, он вызовет функцию Foo, если x не null. Он вернет результат в Nullable<R>. Если x - null, он вернет a Nullable<R> с помощью null.

Или мы можем написать более эквивалентную функцию (например, fmap в Haskell), где у нас есть функция fmap, которая берет на входе a Func<T, R> и возвращает 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?

Ответ 2

Functor

Вы можете не только превратить Nullable<T> в функтор, но С# на самом деле понимает функторы, позволяя вам написать что-то вроде этого:

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

Монада

Не только С# понимает функторы, но и понимает монады. Добавьте эти перегрузки 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 является int?, содержащим число 42.

Ответ 3

Если у вас есть метод расширения:

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

вы можете сделать:

// in some other method

int? x = 0;

x = x?.Foo();

Оператор ?. гарантирует, что Foo вызывается только в том случае, если x не является нулевым. Если x равно null, он не вызывается (вместо этого используется нуль типа возвращаемого значения).


В противном случае канонический способ его записи, естественно:

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

Конечно, вы можете создать свою собственную инфраструктуру Maybe, если хотите (ответ Виллем Ван Онсем).