Может ли 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
. Имеет ли С# аналогичный механизм?
Ответы
Ответ 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
, если хотите (ответ Виллем Ван Онсем).