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

С# Linq против Currying

Я немного играю с функциональным программированием и различными концепциями. Все это очень интересно. Несколько раз я читал о Карринге и о том, какое преимущество у него есть.

Но я не понимаю этого. Следующий источник демонстрирует использование концепции карри и решения с linq. На самом деле, я не вижу никаких оснований использовать концепцию каррирования.

Итак, в чем преимущество использования currying?

static bool IsPrime(int value)
{
    int max = (value / 2) + 1;
    for (int i = 2; i < max; i++)
    {
        if ((value % i) == 0)
        {
            return false;
        }
    }
    return true;
}

static readonly Func<IEnumerable<int>, IEnumerable<int>> GetPrimes = 
        HigherOrder.GetFilter<int>().Curry()(IsPrime);

static void Main(string[] args)
{
    int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };

    Console.Write("Primes:");
    //Curry
    foreach (int n in GetPrimes(numbers))
    {
        Console.Write(" {0}", n);
    }
    Console.WriteLine();

    //Linq
    foreach (int n in numbers.Where(p => IsPrime(p)))
    {
        Console.Write(" {0}", n);
    }

    Console.ReadLine();
}

Вот метод фильтра более высокого уровня:

public static Func<Func<TSource, bool>, IEnumerable<TSource>, IEnumerable<TSource>> GetFilter<TSource>()
{
    return Filter<TSource>;
}
4b9b3361

Ответ 1

В чем преимущество использования каррирования?

Прежде всего, проясните некоторые термины. Люди используют "currying" для обозначения как переформулировки метода двух параметров в методах одного параметра, который возвращает метод одного параметра и частичное применение метода двух параметров для создания метода одного параметра. Ясно, что эти две задачи тесно связаны и, следовательно, путаница. Говоря формально, следует ограничить "каррирование" ссылкой на первое определение, но, если говорить неформально, либо обычное использование.

Итак, если у вас есть метод:

static int Add(int x, int y) { return x + y; }

вы можете называть его следующим образом:

int result = Add(2, 3); // 5

Вы можете использовать метод Add:

static Func<int, int> MakeAdder(int x) { return y => Add(x, y); }

и теперь:

Func<int, int> addTwo = MakeAdder(2);
int result = addTwo(3); // 5

Частичное приложение иногда также называется "currying", если говорить неформально, потому что оно очевидно связано:

Func<int, int> addTwo = y=>Add(2,y);
int result = addTwo(3);

Вы можете сделать машину, которая выполняет этот процесс для вас:

static Func<B, R> PartiallyApply<A, B, R>(Func<A, B, R> f, A a)
{
    return (B b)=>f(a, b);
}
...
Func<int, int> addTwo = PartiallyApply<int, int, int>(Add, 2);
int result = addTwo(3); // 5

Итак, теперь мы подошли к вашему вопросу:

В чем преимущество использования каррирования?

Преимущество любого из методов заключается в том, что он дает вам больше гибкости при работе с методами.

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

static double ApproximateDistance(Point p1, Point p2) { ... }

Но когда вы на самом деле строите алгоритм, то, что вы часто хотите знать, - это расстояние между текущим местоположением и фиксированной конечной точкой. Для чего нужен алгоритм Func<Point, double> - каково расстояние от местоположения до фиксированной конечной точки? У вас есть Func<Point, Point, double>. Как вы собираетесь превратить то, что у вас есть, в то, что вам нужно? С частичным применением; вы частично применяете фиксированную конечную точку в качестве первого аргумента метода приближенного расстояния, и вы получаете функцию, которая соответствует потребностям алгоритма поиска пути:

Func<Point, double> distanceFinder = PartiallyApply<Point, Point, double>(ApproximateDistance, givenEndPoint);

Если вначале был применен метод ApproximateDistance:

static Func<Point, double> MakeApproximateDistanceFinder(Point p1) { ... }

Тогда вам не нужно будет делать частичное приложение самостоятельно; вы просто вызываете MakeApproximateDistanceFinder с фиксированной конечной точкой, и вы можете сделать это.

Func<Point, double> distanceFinder = MakeApproximateDistanceFinder(givenEndPoint);

Ответ 2

Комментарий @Eric Lippert на В чем преимущество Currying в С#? (достижение частичной функции) указывает на это сообщение в блоге:

Работа с каррированием и частичной функцией

Где я нашел это лучшее объяснение, которое работает для меня:

С теоретической точки зрения, это интересно, потому что это (каррирование) упрощает лямбда-исчисление включает только те функции, которые имеют не более один аргумент. С практической точки зрения, это позволяет программисту генерировать семейства функций из базовой функции, фиксируя первых k аргументов. Это похоже на закрепление чего-то на стене который требует двух контактов. Перед тем, как быть закрепленным, объект свободен для перемещаться в любом месте на поверхности; однако, когда при первом вводе то движение ограничено. Наконец, когда второй штырь ставится тогда свобода передвижения больше не существует. Аналогичным образом, когда программист представляет собой функцию двух аргументов и применяет ее к первый аргумент, тогда функциональность ограничена одним измерением. Наконец, когда он применяет новую функцию ко второму аргументу, тогда вычисляется конкретное значение.

Принимая это далее, я вижу, что функциональное программирование по сути вводит "программирование потока данных, а не поток управления", это сродни использованию SQL-кода вместо С#. С этим определением я вижу, почему LINQ и почему у него много приложений за пределами чистых Linq2Objects - таких как события в Rx.

Ответ 3

Преимущество использования currying в основном можно найти на функциональных языках, которые построены, чтобы извлечь выгоду из currying, и имеют удобный синтаксис для этой концепции. С# не является таким языком, и реализации currying в С# обычно трудно поддаются, как и выражение HigherOrder.GetFilter<int>().Curry()(IsPrime).