Ммм, Карри

В недавнем комментарии меня спросили, почему программисты на Haskell иногда пишут лямбды C# в таком стиле:

Func<int, Func<int, int>> add = x=>y=>x+y;

и затем вызывают их как

sum = add(2)(3);

поскольку первый вызов, конечно, возвращает функцию, прибавляющую два, которую потом вызывают с тройкой. Зачем делать такое вместо более прямолинейного

Func<int, int, int> add = (x,y)=>x+y;

и вызова

sum = add(2,3);

???

Я собирался написать об этом короткую статью, когда вспомнил, что Вес уже сделал это лучше, чем я бы смог. Прочтите это перед тем, как читать дальше.

С возвращением. Я добавлю к этому пару интересных фактов.

Во-первых, операция переписывания функции от n аргументов в цепочку одноаргументных функций для получения «частичного применения» называется «карринг», в честь Хаскеля Карри, по имени которого названы языки программирования Haskell и Curry.

И во-вторых, есть малоизвестный факт про корявый, но изредка полезный способ «откаррировать» первый параметр существующей функции при помощи рефлексии.

Например, пусть у вас есть

public class C {
public static int M(T t, int x) { всякое }
}

В C#, для каррирования первого параметра вы можете просто написать

T t = чтонибудь;
Func<int, int> d = x=>C.M(t, x);

Что, конечно же, синтаксический сахар для создания скрытого вложенного класса с полем t, методом, который принимает int, всякой ерундой, и проче кучей сгенерированного нудного кода.

Если вам нужно откаррировать первый параметр метода на управляемом языке, в котором нет анонимных функций, или вы пишете компилятор, но вам не хочется порождать целую кучу вспомогательных методов, как это делает компилятор C#, вы также можете сделать это напрямую через рефлексию:

T t = чтонибудь;
MethodInfo methodInfo = typeof(C).GetMethod("M", BindingFlags.Static | BindingFlags.Public);
Func<int, int> d = (Func<int, int>) Delegate.CreateDelegate(typeof(Func<int, int>), t, methodInfo);

Это работает в .Net Framework 2.0 или новее, и, к сожалению, требует, чтобы T был ссылочным типом.

Обратите внимание, что здесь применён, собственно, тот же тип карринга, который работает, когда вы превращаете метод –расширение в делегат. Если статический метод C.M является расширением для типа T, то в C# вы можете сказать

Func<int, int> d = t.M;

И при создании делегата мы, собственно, откарриваем первый аргумент метода, используя похожий на рефлексию фокус. (Мы передаём управляемый адрес метода-расширения в конструктор вместо передачи MethodInfo в фабрику, но внутренняя суть проихсодящего та же самая). Если вы попробуете это с расширением для типа-значения, то получите ошибку. Sree только что опубликовал анализ того, почему это должен быть ссылочный тип.