Аппликативность диагонального функтора
Oct. 13th, 2015 07:02 pmЗанимался тут исследовательским/экспериментальным кодом, а там получилось, что одни и те же действия делаются с двумя похожими наборами данных (горизонтальные и вертикальные координаты), большую часть времени независимо. И захотелось вместо того, чтобы писать одинаковый код и следить, как бы не передать куда иксы вместо игреков, просто лифтануть имеющиеся функции, чтобы они работали сразу с парами значений.
На хаскеле для этого достаточно описать, каким образом диагональный функтор (гомогенные туплы) является аппликативным. Что-то вроде:
(вот только на самом деле не хочется определять свой тип, было бы здорово обычные туплы (а,а) там использовать)
Но у меня тот проектик был не на хаскеле, а на D. Там подход чуть иной. Сперва как оно выглядит в использовании:
А вот как это реализовано:
По шагам:
tmap получает некоторую функцию f в качестве compile-time-known параметра, и набор рантайм-аргументов args, типы которых вычисляются как Double!(Parameters!f). Parameters - это типовая ф-я из стандартной библиотеки, она возвращает список типов аргументов переданной ей ф-ии. К этому списку мы применяем типовую функцию Double, которая просто маппит список, преобразуя каждое значение функцией Dbl, которая всякий Т превращает в Tuple!(T,T), где Tuple - контейнер из стандартной библиотеки. В результате, например, если исходная функция f принимала аргументы типов (string, bool, int[]), то tmap!f будет принимать аргументы типов (Tuple!(string, string), Tuple!(bool, bool), Tuple!(int[], int[])).
Возвращает tmap, понятное дело, тоже тупл, полученный применением исходной ф-ии к соответствующим элементам входных туплов. Для этого ей надо разделить набор входных туплов на два набора - левые и правые половины оных. Для такой проекции из набора туплов в набор их частей описана ф-я tproject. В принципе, ее наверняка можно реализовать более чистым образом, но я использовал тупую кодогенерацию. Если args это набор туплов, то args[0] это первый тупл, а args[0][0] это первое значение из первого тупла. Т.е. чтобы взять все первые значения нам нужен tuple(args[0][0], args[1][0], args[2][0]...). Такая строчка генерится в компайл-тайме в первой строке tproject и тут же вставляется во вторую строчку, так получается нужное значение.
А, насчет синтаксиса. args.tproject!0 это (благодаря Universal Function Call Syntax) то же самое, что tproject!(0)(args), т.е. передаем компайл-тайм аргумент 0 (это будет pos), и рантайм-аргумент args, а второй компайл-тайм аргумент Ts - это список типов args, он выводится/извлекается автоматически. Три точки говорят, что это не одно значение, а набор значений (несколько аргументов). Чтобы развернуть библиотечную структуру Tuple в набор передаваемых значений, делается .expand.
Вот и все, дешево и сердито. А как такое делается на вашем языке программирования?
На хаскеле для этого достаточно описать, каким образом диагональный функтор (гомогенные туплы) является аппликативным. Что-то вроде:
data Pair a = P a a deriving (Functor, Show) instance Applicative Pair where pure x = P x x P fa fb <*> P xa xb = P (fa xa) (fb xb)
(вот только на самом деле не хочется определять свой тип, было бы здорово обычные туплы (а,а) там использовать)
Но у меня тот проектик был не на хаскеле, а на D. Там подход чуть иной. Сперва как оно выглядит в использовании:
unittest { int f1(string s) { return s.length; } int f2(int x, int y) { return x + y; } bool f3(int a, string b, bool c) { return c && (a <= b.length); } auto r1 = tmap!f1( tuple("aa", "bbb") ); writeln(r1); // (2,3) auto r2 = tmap!f2( tuple(10,20), r1 ); writeln(r2); // (12, 23) auto r3 = tmap!f3( r2, tuple("one", "five"), tuple(true, false)); writeln(r3); // (false, false) }
А вот как это реализовано:
alias Dbl(T) = Tuple!(T,T); alias Double(Ts...) = staticMap!(Dbl, Ts); auto tproject(int pos, Ts...)(Ts args) { enum projStr = iota(args.length).map!(i => format("args[%s][%s]", i, pos)).join(", "); return mixin("tuple(" ~ projStr ~ ")"); } auto tmap(alias f)(Double!(Parameters!f) args) { return tuple(f(args.tproject!0.expand), f(args.tproject!1.expand)); }
По шагам:
tmap получает некоторую функцию f в качестве compile-time-known параметра, и набор рантайм-аргументов args, типы которых вычисляются как Double!(Parameters!f). Parameters - это типовая ф-я из стандартной библиотеки, она возвращает список типов аргументов переданной ей ф-ии. К этому списку мы применяем типовую функцию Double, которая просто маппит список, преобразуя каждое значение функцией Dbl, которая всякий Т превращает в Tuple!(T,T), где Tuple - контейнер из стандартной библиотеки. В результате, например, если исходная функция f принимала аргументы типов (string, bool, int[]), то tmap!f будет принимать аргументы типов (Tuple!(string, string), Tuple!(bool, bool), Tuple!(int[], int[])).
Возвращает tmap, понятное дело, тоже тупл, полученный применением исходной ф-ии к соответствующим элементам входных туплов. Для этого ей надо разделить набор входных туплов на два набора - левые и правые половины оных. Для такой проекции из набора туплов в набор их частей описана ф-я tproject. В принципе, ее наверняка можно реализовать более чистым образом, но я использовал тупую кодогенерацию. Если args это набор туплов, то args[0] это первый тупл, а args[0][0] это первое значение из первого тупла. Т.е. чтобы взять все первые значения нам нужен tuple(args[0][0], args[1][0], args[2][0]...). Такая строчка генерится в компайл-тайме в первой строке tproject и тут же вставляется во вторую строчку, так получается нужное значение.
А, насчет синтаксиса. args.tproject!0 это (благодаря Universal Function Call Syntax) то же самое, что tproject!(0)(args), т.е. передаем компайл-тайм аргумент 0 (это будет pos), и рантайм-аргумент args, а второй компайл-тайм аргумент Ts - это список типов args, он выводится/извлекается автоматически. Три точки говорят, что это не одно значение, а набор значений (несколько аргументов). Чтобы развернуть библиотечную структуру Tuple в набор передаваемых значений, делается .expand.
Вот и все, дешево и сердито. А как такое делается на вашем языке программирования?