Эти удивительные каррированные функции
Имея в языке только каррированные функции и левоассоциативный оператор вызова, вы можете с легкостью имитировать такие возможности как передача неограниченного количества параметров, при этом не вводя в язык какие-либо дополнительные механизмы.
К примеру, сколько аргументов у такой функции:
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
Это вообще-то совсем другой механизм, который основан на зависимости параметров - либо типов этих параметров, либо даже их значений, если речь идет о динамике. Он в чем-то мощнее. В динамическом языке с помощью него можно выражать функции, кол-во параметров которых вообще в компайл-тайме неизвестно.