Настраиваемые методы расширения с возможностью обнуления и SelectMany

#c# #linq #monads

#c# #linq #монады

Вопрос:

Существуют методы расширения Nullable<T> , подобные приведенным ниже.

 using System;
using System.Runtime.CompilerServices;

namespace DoNotationish
{
    public static class NullableExtensions
    {
        public static U? Select<T, U>(this T? nullableValue, Func<T, U> f)
            where T : struct
            where U : struct
        {
            if (!nullableValue.HasValue) return null;
            return f(nullableValue.Value);
        }

        public static V? SelectMany<T, U, V>(this T? nullableValue, Func<T, U?> bind, Func<T, U, V> f)
            where T : struct
            where U : struct
            where V : struct
        {
            if (!nullableValue.HasValue) return null;
            T value = nullableValue.Value;
            U? bindValue = bind(value);
            if (!bindValue.HasValue) return null;
            return f(value, bindValue.Value);
        }
    }
}
 

Это позволяет Nullable<T> использовать в синтаксисе запроса.
Следующие тесты будут пройдены.

         [Test]
        public void Test1()
        {
            int? nv1 = 5;
            int? nv2 = 3;
            var q = from v1 in nv1
                    from v2 in nv2
                    select v1   v2;
            Assert.AreEqual(8, q);
        }

        [Test]
        public void Test2()
        {
            int? nv1 = null;
            int? nv2 = 3;
            var q = from v1 in nv1
                    from v2 in nv2
                    select v1   v2;
            Assert.IsNull(q);
        }
 

Однако, если вы попытаетесь связать 3 или более, он будет рассматриваться как анонимный тип и не будет компилироваться.

         [Test]
        public void Test3()
        {
            int? nv1 = 5;
            int? nv2 = 3;
            int? nv3 = 8;
            var q = from v1 in nv1
                    from v2 in nv2  // Error CS0453: anonymous type is not struct
                    from v3 in nv3
                    select v1   v2   v3;
            Assert.AreEqual(16, q);
        }
 

Вы можете обойти эту проблему, указав вручную использовать ValueTuple , как показано ниже, но это некрасиво.

         [Test]
        public void Test3_()
        {
            int? nv1 = 5;
            int? nv2 = 3;
            int? nv3 = 8;
            var q = from v1 in nv1
                    from v2 in nv2
                    select (v1, v2) into temp      // ugly
                    from v3 in nv3
                    select temp.v1   temp.v2   v3; // ugly
            Assert.AreEqual(16, q);
        }
 

Эти упрощенные примеры могут быть решены просто с помощью оператора: var q = nv1 nv2 nv3;

Однако вам было бы удобнее работать с пользовательскими структурами, если бы вы могли свободно их писать. Есть ли какой-нибудь хороший способ?

Ответ №1:

Подумайте о том, как компилятор превратит выражение запроса в SelectMany вызовы. Это превратило бы его во что-то вроде:

 var q =
    nv1.SelectMany(x => 
       nv2.SelectMany(x => nv3, (v2, v3) => new { v2, v3 }), 
       (v1, v2v3) => v1   v2v3.v2   v2v3.v3);
 

Обратите внимание, что V из второго SelectMany вызова выводится анонимный класс, который является ссылочным типом и не соответствует ограничению : struct .

Обратите внимание, что он специально использует анонимный класс, а не a ValueTuple ( (v2, v3) => (v2, v3) ) . Это указано в спецификации языка:

Выражение запроса со вторым предложением from, за которым следует что-то другое, кроме предложения select:

 from x1 in e1
from x2 in e2
...
 

преобразуется в

 from * in ( e1 ) . SelectMany( x1 => e2 , ( x1 , x2 ) => new { x1 , x2 } )
...
 

Так что, к сожалению, вы ничего не можете с этим поделать. Вы можете попробовать разветвить компилятор Roslyn, чтобы заставить его вместо этого компилироваться для создания a ValueTuple , но технически это уже не «C #».

OTOH, эта идея может сработать, если вы напишете свой собственный Nullable<T> тип, а не ограничиваете T тип значения, но я не уверен, что оно того стоит.

Комментарии:

1. Когда вы написали «если вы напишете свой собственный тип с нулевым значением <T>», меня осенила та же идея. Так что да, у меня был этот «собственный Nullable<T> тип», поэтому я только что применил его к этой проблеме. : ^

Ответ №2:

Давайте посмотрим на этот запрос

 from a in source1
from b in source2
from c in source3
from d in source4
// etc
select selector // how is it possible that a, b, c, d available in selector?
 

Такие запросы будут скомпилированы как цепочка вызовов SelectMany

 SelectMany(IEnumerable<TSource> source, 
           Func<TSource, IEnumerable<TCollection>> collectionSelector,
           Func<TSource, TCollection, TResult> resultSelector)
 

Как вы можете видеть, он может принимать только два аргумента в селекторе результатов — один из исходного типа коллекции и один из второго типа коллекции, возвращаемый селектором. Таким образом, единственный способ передать более двух аргументов по цепочке (чтобы все аргументы в конечном итоге попали в последний селектор результатов) — это создать анонимные типы. И вот как это выглядит:

 source1
  .SelectMany(a => source2, (a, b) => new { a, b })
  .SelectMany(x1 => source3, (x1, c) => new { x1, c })
  .SelectMany(x2 => source4, (x2, d) => selector(x2.x1.a, x2.x1.b, x2.c, d));
 

Опять же, селектор результатов ограничен двумя входными аргументами. Итак, для вашего прохождения Test1 и Test2 анонимный тип не создается, потому что оба аргумента могут быть переданы селектору результатов. Но для Test3 требуется три аргумента для селектора результатов, и для этого создается промежуточный анонимный тип.


Вы не можете заставить свой метод расширения принимать как обнуляемые структуры, так и сгенерированные анонимные типы (которые являются ссылочными типами). Я бы посоветовал вам создать привязку и сопоставление методов расширения для конкретного домена. Пара этих методов будет соответствовать предметной области функционального программирования гораздо больше, чем from v1 in nv1 запросы:

 public static U? Bind<T, U>(this T? maybeValue, Func<T, U?> binder)
    where T : struct
    where U : struct
        => maybeValue.HasValue ? binder(maybeValue.Value) : (U?)null;

public static U? Map<T, U>(this T? maybeValue, Func<T, U> mapper)
    where T : struct
    where U : struct
        => maybeValue.HasValue ? mapper(maybeValue.Value) : (U?)null;
 

И использование

 nv1.Bind(v1 => nv2.Bind(v2 => nv3.Map(v3 => v1   v2   v3)))
   .Map(x => x * 2) // eg