Каков наилучший подход к реализации монад в C # - PullRequest
2 голосов
/ 13 декабря 2010

Каковы наилучшие подходы к реализации монад в C #?Существуют ли конкретные стратегии реализации или каждая монада реализована иначе, чем другая?

1 Ответ

6 голосов
/ 14 декабря 2010

Чтобы ответить на вопрос, а не просто комментировать его, Linq, вероятно, является основным методом выполнения монадических преобразований в C #. Цепочка методов Linq - это не что иное, как ленивый, упорядоченный набор операций обработки списка.

Линк, конечно, не ракетостроение; больше, чем ваш средний курс программирования на уровне колледжа, но все же. Это серия методов расширения, каждый из которых создает перечисляемый заполнитель (монаду), содержащий логику, которую они должны выполнять, и ссылку на их источник данных (который может быть другой монадической инкапсуляцией). Вы можете, и многие это делают, добавить дополнительные расширения в базовую библиотеку Linq, чтобы заполнить пробелы в функциональности или выполнить пользовательские действия, отвечающие конкретным потребностям.

Вы также можете создавать свои собственные монадические цепочки методов, чтобы делать что угодно. Практически любая инфраструктура или библиотека, описанная как имеющая «свободный» интерфейс кодирования, является библиотекой на основе монад. Существуют быстрые юнит-тесты модульных тестов, текущие конфигурации ORM, даже расширения отображений пользовательского интерфейса в свободном домене.

Реализация монадической библиотеки в C # обычно выполняется с использованием статического класса и статических методов, которые используют один или несколько монадических типов, которые недоступны извне их предполагаемого использования. Например, вот основная монадическая библиотека, которая выполняет сложение и вычитание целых чисел:

public static class MonadicArithmetic
{
   public static Monad Take(int input) { return new Monad(input); }

   public class Monad
   {
      int theValue;

      internal Monad(int input) { theValue = input; }   

      public Monad Add(int input){ return new Monad(theValue + input); }
      public Monad Subtract(int input){ return new Monad(theValue - result); }
      public int Value { get { return theValue; } }
   }
}

...

//usage
var result = MonadicArithmetic.Take(1).Add(2).Subtract(1).Value; //2

Очевидно, что это очень просто, и все операции выполняются "с нетерпением". Если эти операции были сложными, это может быть неоптимально, поэтому вместо этого давайте выполним их «лениво»:

public static class MonadicArithmetic
{
   public static Monad Take(int input) { return new Monad(input); }

   public class Monad
   {
      //Change the "value keeper" into a Func that will return the value;
      Func<int> theValue;

      //the constructor now turns the input value into a lambda
      internal Monad(int input) { theValue = ()=>input; }
      //and another constructor is added for intra-class use that takes a lambda 
      private Monad(Func<int> input) { theValue = input; }   

      //And now the methods will create new lambdas that call the existing lambdas
      public Monad Add(int input){ return new Monad(()=>theValue() + input); }
      public Monad Subtract(int input){ return new Monad(()=>theValue() - input); }

      //Finally, our Value getter at the end will evaluate the lambda, unwrapping all the nested calls
      public int Value { get { return theValue(); } }
   }
}

То же использование, за исключением того, что никакие операции не будут выполняться, пока конкретное значение не будет запрошено с помощью кода:

//Each call just adds a shell to the nested lambdas
var operation = MonadicArithmetic.Take(1).Add(2).Subtract(1);

...

//HERE's the payoff; the result is not evaluated till the call to Value_get() behind the scenes of this assignment.
var result = operation.Value; 

Однако с этим есть проблема. Методы просто принимают входные значения и ссылаются на них в лямбде. Проблема в том, что область значений зависит от содержащего метода (то есть они не живут достаточно долго для оценки лямбды). Когда вызывается метод получения значения (), лямбда будет оценена, и на все эти переменные вне области будут ссылаться. Вместо этого мы должны сохранять ценности в чем-то, что будет жить, по крайней мере, до тех пор, пока живут лямбды. Монада является очевидным вариантом. Вот вероятное решение:

public static class MonadicArithmetic
{
   public static Monad Take(int input) { return new Monad(input); }

   public class Monad
   {
      //Our value keeper is now a pure function that requires no external closures
      Func<Func<int>, int, int> operation;
      //and we add two new private fields; 
      //a hook to a lambda that will give us the result of all previous operations,
      Func<int> source;
      //... and the value for the current operation.
      private int addend;

      //our constructor now takes the value, stores it, and creates a simple lambda
      internal Monad(int input) { addend = input; operation = ()=>addend; }
      //and our private constructor now builds a new Monad from scratch
      private Monad(Func<int> prevOp, Func<Func<int>, int, int> currOp, int input) 
      { 
          source = prevOp, 
          operation = currOp, 
          addend = input; 
      }  

      //The methods will create new Monads that take the current Monad's value getter,   
      //keeping the current Monad in memory.
      public Monad Add(int input)
      { 
         return new Monad(this.Result, (f,i)=>f()+i, input); 
      }

      public Monad Subtract(int input)
      { 
         return new Monad(this.Result, (f,i)=>f()-i, input); 
      }

      //And we change our property to a method, so it can also 
      //be used internally as a delegate
      public int Result() { return operation(source, addend); }
   }
}

//usage
var operations = MonadicArithmetic.Take(1).Add(3).Subtract(2); 
//There are now 3 Monads in memory, each holding a hook to the previous Monad, 
//the current addend, and a function to produce the result...

...

//so that here, all the necessary pieces are still available.
var result = operations.Result();  

Это базовый шаблон для монадической библиотеки. Статический метод, который запускает все это, может быть методом расширения, который используется в стиле Linq. Корень цепочки методов становится первым значением:

//using an "identity function" to convert to a monad
var operations = 1.AsMonad().Add(2).Subtract(3);

//performing the conversion implicitly from an overload of Add()
var operations = 1.Add(2).Subtract(3);

Linq для объектов особенно элегантен тем, что его библиотеки являются методами расширения, которые принимают IEnumerable и возвращают IEnumerable, поэтому процесс преобразования обрабатывается без какой-либо перегрузки или явных вызовов методов. Однако объекты IQueryable, скрывающие деревья переводимых выражений, являются новой идеей в .NET 3.5, и вам НЕОБХОДИМО явным образом преобразовывать коллекцию в IQueryable с помощью метода AsQueryable ().

...