Share via


Эти удивительные каррированные функции

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

К примеру, сколько аргументов у такой функции:

 out "Hello"

//Вывод:
//Hello

out "First" "Second" "Third"

//Вывод:
//First
//Second
//Third

Если вы читали мою предыдущую заметку, то уже знаете ответ - аргумент всего-лишь один. А вот как функция out может быть определена, к примеру, на Ela:

 open Con
let out x = writen x $ out

writen - это обычная функция вывода в консоль, такая же, как Console.WriteLine. Оператор ($) представляет собой так называемый sequencing operator. Ближайшим его аналогом в C# является точка с запятой. Оператор ($) исполняет сначала левое выражение, игнорирует вычисленное при этом значение, затем исполняет правое выражение и возвращает результат его вычисления. В итоге мы описали функцию, которая принимает один аргумент, выводит его на консоль, а потом возвращает саму себя, в результате чего работать с этой функцией можно так, как если бы она принимала бесконечное число аргументов.

Или возьмем для примера другой язык - F#. В стандартной библиотеке F# есть такая функция printfn. Вот как она работает:

 printfn "Hello, %s!" "world"

//Вывод:
//Hello, world!

printfn "%d+%d=%d" 2 2 (2+2)

//Вывод:
//2+2=4

Опять же, на первый взгляд кажется, что эта функция принимает неограниченное число параметров - так же, как и одна из версий Console.WriteLine, которая в качестве последнего параметра принимает массив аргументов с модификатором params. Но нет, printfn принимает всего лишь один аргумент.

Несмотря на то, что аргумент этот выглядит как строка, он в действительности имеет тип TextWriterFormat<'T>. Функция printfn типизируется в зависимости от значения этого аргумента. В первом случае в формате для вывода мы указали всего-лишь один аргумент - и в результате printfn возвращает функцию для одного аргумента, которую мы тут же и вызываем в нашем примере. Во втором случае нам требуется аж три аргумента - и printfn возвращает функцию для трех аргументов.

Более того, printfn контролирует типы еще на этапе компиляции. В первом случае мы указали, что нам требуется строка (%s) - и получили функцию, которая принимает строку. Во втором случае нам потребовались три числовых типа (%d) - и мы получили функцию, принимающую три целые числа. Попробуйте вызвать ее с параметрами другого типа - и получите ошибку времени компиляции. Удобно, правда?

Фактически у нас получается, что тип возвращаемого значения у функции зависит от типа ее аргумента.

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

 let fun (x::xs) = fun' x xs
                where fun' a [] = \y -> y + a
                      fun' a (x::xs) = \y -> fun' (y + a + x) xs

Полученная в результате вызова fun функция суммирует элементы списка с переданными в нее аргументами и вычисляет общую сумму:

 let f = fun [1,2,3]
f 1 2 3

//Вывод:
//12

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

Возьмем чуть более практичный пример. Скажем, у нас есть функция openConnection, которая принимает название используемого протокола и номер порта. При этом однако номер порта не требуется нам, если в качестве протокола используется HTTP (в таком случае всегда используется 80-й порт). Думаете, что для реализации такой функции нам пригодились бы необязательные параметры из C# 4.0? Мы можем прекрасно обойтись и без них:

 let openConnection p | p == "http" = create p 80
                     | else        = \port -> create p port
                     where create name port = {protocol=name,port=port}

А теперь вызовем эту функцию:

 openConnection "tcp" 244

//Вывод
//{protocol=tcp,port=244}

И для HTTP:

 openConnection "http"

//Вывод
//{protocol=http,port=80}

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

Comments

  • Anonymous
    April 21, 2011
    Ну понятно же, что это просто костыль. Какую-нибудь лисповскую + таким способом сделать уже невозможно.

  • Anonymous
    April 21, 2011
    Это вообще-то совсем другой механизм, который основан на зависимости параметров - либо типов этих параметров, либо даже их значений, если речь идет о динамике. Он в чем-то мощнее. В динамическом языке с помощью него можно выражать функции, кол-во параметров которых вообще в компайл-тайме неизвестно.