Недавно потребовалось заиметь простой и легко расширяемый способ сопоставления по образцу.
Было решено сделать конструирование сопоставляющей функции в функциональном стиле, иными словами нечто вроде такого:
// сопоставляющая функция. Её можно инициализировать, чтобы в случае отстутствия шаблона выкидывалось исключение
// к примеру так: matcher = function() { throw new Error("no applicable pattern found") }
var matcher
matcher = combine(PATTERN1, CALLBACK1(OBJ, .. OPTIONAL_ARGS){...}, matcher)
matcher = combine(PATTERN2, CALLBACK2(OBJ, .. OPTIONAL_ARGS){...}, matcher)
// промежуточные результаты matcher можно так же сохранять и использовать в сопоставлении
matcher = combine(PATTERN3, CALLBACK3(OBJ, .. OPTIONAL_ARGS){...}, matcher)
...
// сопоставление
matcher(OBJ, ... OPTIONAL_ARGS)
Реализация (простая как 3 рубля. Расширение сопоставителя — простое добавление):
// namespace
var pm = {}
/**
* Matcher functions constructors are used in pm.combine method.
* Each key in this object corresponds to the certain pattern member.
*/
pm._matcherConstructors = {
instanceOf: function (matcher, instanceTarget) {
return function (obj) {
if (obj instanceof instanceTarget) {
return matcher.apply(this, arguments)
}
return false
}
},
type: function (matcher, typeId) {
return function (obj) {
if (typeof(obj) === typeId) {
return matcher.apply(this, arguments)
}
return false
}
},
scheme: function (matcher, scheme) {
return function (obj) {
if (typeof(obj) !== "object") {
return false
}
for (var i in scheme) {
if (i in obj) {
var target = obj[i]
var source = scheme[i]
var sourceType = typeof(source)
if (sourceType === "string") {
if (source === "any" || source == typeof(target)) {
continue
}
return false
}
if (source !== target) {
return false
}
}
else {
return false
}
}
return matcher.apply(this, arguments)
}
}
}
/**
* Creates pattern matching function that accepts the pattern given.
* The latter combined patterns takes priority over the previously declared ones.
* @param pattern Pattern to match the target object.
* @param callback User-defined callback to accept target object as well as the accompanying arguments.
* @param prevMatcher Previous matcher function created by combine method or null or undefined.
* @returns Matcher function to be used as follows: matcher.call(objectToBeMatched, optionalArguments...).
*/
pm.combine = function(pattern, callback, prevMatcher) {
var matcher = function() {
callback.apply(this, arguments)
return true
}
// join visitor function according to the pattern given
for (var i in pattern) {
if (!(i in pm._matcherConstructors)) {
throw new Error("unexpected pattern tag: " + i)
}
matcher = pm._matcherConstructors[i](matcher, pattern[i])
}
// if prev matcher either undefined or null - create new function
if (prevMatcher == null) {
return matcher
}
else {
return function() {
if (matcher.apply(this, arguments)) {
return true
}
return prevMatcher.apply(this, arguments)
}
}
}
/**
* Helper function that initializes matcher for all the types of objects with
* the callback that throws an error.
*/
pm.unknownObjectMatcher = function() {
throw new Error("unknown object matched")
}
Здравствуйте, c-smile, Вы писали: CS>Здравствуйте, A13x, Вы писали:
A>>Буду рад любым комментариям CS>А зачем это всё? Можешь привести типовую задачу где это нужно?
В смысле, зачем нужен паттерн-матчинг вообще, или зачем он нужен конкретно в js?
Здравствуйте, c-smile, Вы писали:
CS>Здравствуйте, A13x, Вы писали:
A>>Буду рад любым комментариям
CS>А зачем это всё? Можешь привести типовую задачу где это нужно?
В моем конкретном случае pattern matching понадобился для реализации паттерна "посетитель".
Конкретно — в функцию приходят разнородные объекты, в зависимости от типа объекта (строка, { expr: STRING, val: ANY }, { expr: STRING, val: ANY, source: ANY }, { snippet: ANY }, и т.п., здесь заглавными буквами описан тип) требовалось выполнять разные действия.
Изначально очень не хотелось громоздить кучу if с typeof-ами и in-ами, т.к. это снижало наглядность кода, хотелось более общего и расширяемого решения. В результате пришел к такому вот pattern matching'у.
BTW, упомянутые объекты не могли содержать функции, поэтому реализовать паттерн "посетитель" в чистом виде не удавалось, плюс к тому нужно было различать некоторые объекты имеющие одинаковый тип, но разное содержимое.
Здравствуйте, A13x, Вы писали:
A>Изначально очень не хотелось громоздить кучу if с typeof-ами и in-ами, т.к. это снижало наглядность кода, хотелось более общего и расширяемого решения. В результате пришел к такому вот pattern matching'у.
Интересно, а если бы у вас был бы внешний компилятор PM, например встроеный в "Closure Compiler" (http://code.google.com/closure/compiler/) — вы бы им пользовались?
Здравствуйте, z00n, Вы писали:
Z>Здравствуйте, A13x, Вы писали:
A>>Изначально очень не хотелось громоздить кучу if с typeof-ами и in-ами, т.к. это снижало наглядность кода, хотелось более общего и расширяемого решения. В результате пришел к такому вот pattern matching'у. Z>Интересно, а если бы у вас был бы внешний компилятор PM, например встроеный в "Closure Compiler" (http://code.google.com/closure/compiler/) — вы бы им пользовались?
Я думал об этом, но показалось, что будет как то неудобно — хотелось бы после редактирования кода сразу смотреть на результат без промежуточной компиляции.
Впрочем, думаю, в итоге можно прийти к компромиссу между отладочным режимом и оптимизированной сборкой в closure compiler — выделить задачу pattern matching-а в отдельную функцию, в которой использовать ту или иную реализацию в зависимости от константы USE_COMPILED_PM_ENGINE. Соответственно при сборке closure compiler'ом выставлять эту константу/дефайн в true и использовать "скомпилированную" функцию матчинга.
Но пока такой необходимости нет.
Кстати, а разве компилятор PM встроен в closure compiler? Я что-то не припоминаю такого.
Здравствуйте, A13x, Вы писали:
A>Здравствуйте, z00n, Вы писали:
Z>>Здравствуйте, A13x, Вы писали:
A>>>Изначально очень не хотелось громоздить кучу if с typeof-ами и in-ами, т.к. это снижало наглядность кода, хотелось более общего и расширяемого решения. В результате пришел к такому вот pattern matching'у. Z>>Интересно, а если бы у вас был бы внешний компилятор PM, например встроеный в "Closure Compiler" (http://code.google.com/closure/compiler/) — вы бы им пользовались?
A>Я думал об этом, но показалось, что будет как то неудобно — хотелось бы после редактирования кода сразу смотреть на результат без промежуточной компиляции.
Можно компиляцию повесить на кнопку Save (я примерно так и делал, только на отдельную комбинацию кнопок в емаксе).
A>Впрочем, думаю, в итоге можно прийти к компромиссу между отладочным режимом и оптимизированной сборкой в closure compiler — выделить задачу pattern matching-а в отдельную функцию, в которой использовать ту или иную реализацию в зависимости от константы USE_COMPILED_PM_ENGINE. Соответственно при сборке closure compiler'ом выставлять эту константу/дефайн в true и использовать "скомпилированную" функцию матчинга. A>Но пока такой необходимости нет.
A>Кстати, а разве компилятор PM встроен в closure compiler? Я что-то не припоминаю такого.
Я у меня есть такой компилятор для Lua, который работает под JVM. Его можно было-бы переписать для Javascript использовав Closure compiler как фронт-энд — но я что-то не думаю, что есть спрос — потому и спросил
Здравствуйте, z00n, Вы писали:
Z>Можно компиляцию повесить на кнопку Save (я примерно так и делал, только на отдельную комбинацию кнопок в емаксе).
Ну, во-первых, я не использую кнопку Save (в IntelliJIdea просто нет такой кнопки), во-вторых один из проектов в котором я принимаю участие содержит около 200 яваскрипт файлов. Учитывая довольно медленную компиляцию closure в advanced режиме получим довольно внушительную задержку.
Z>Я у меня есть такой компилятор для Lua, который работает под JVM. Его можно было-бы переписать для Javascript использовав Closure compiler как фронт-энд — но я что-то не думаю, что есть спрос — потому и спросил
Не думаю, что для этой задачи стоило бы использовать closure compiler.
Скорее можно было бы держать описание шаблонов (patterns) и название операций в отдельном JSON файлике по которому бы внешний тул генерировал js файл с функцией сопоставления (match).
Здравствуйте, A13x, Вы писали:
A>Здравствуйте, z00n, Вы писали:
Z>>Можно компиляцию повесить на кнопку Save (я примерно так и делал, только на отдельную комбинацию кнопок в емаксе).
A>Ну, во-первых, я не использую кнопку Save (в IntelliJIdea просто нет такой кнопки), во-вторых один из проектов в котором я принимаю участие содержит около 200 яваскрипт файлов. Учитывая довольно медленную компиляцию closure в advanced режиме получим довольно внушительную задержку.
А зачем их перекомпилировать каждый раз? Я встроил это компилятор и даже не требовалось использовать внешнюю систему сборки.
Z>>Я у меня есть такой компилятор для Lua, который работает под JVM. Его можно было-бы переписать для Javascript использовав Closure compiler как фронт-энд — но я что-то не думаю, что есть спрос — потому и спросил
A>Не думаю, что для этой задачи стоило бы использовать closure compiler.
Только как фронтэнд — я думаю там обычный рукописный парсер — врядли он сам по себе медленный.
A>Скорее можно было бы держать описание шаблонов (patterns) и название операций в отдельном JSON файлике по которому бы внешний тул генерировал js файл с функцией сопоставления (match).
По моему это ужасно неудобно Идеальным вариантом было бы поддержание яваскриптом PM изначально — разве нет? Вы просто создаете такой язык: JS + PM, даете файлам другое расширение, перед использованием компилируете в читаемый, отформатированный яваскрипт, в котором все случае использования PM заменены на дерево if-ов.
Здравствуйте, kochetkov.vladimir, Вы писали:
KV>Здравствуйте, c-smile, Вы писали: CS>>Здравствуйте, A13x, Вы писали:
A>>>Буду рад любым комментариям CS>>А зачем это всё? Можешь привести типовую задачу где это нужно?
KV>В смысле, зачем нужен паттерн-матчинг вообще, или зачем он нужен конкретно в js?
Еще биндинг переменных нужен.
CS>вопрос состоит в том что стоит ли овчинка генерализации (на уровне библиотеки или языка) или нет (это я уже про tiscript)?
Стоит, но вопрос что разбирать в JS в функциональных языках вся мощь ПМ держится на разборе алгебраических типов данных.
Хотя если посмотреть на динамический Эрланг то наверно можно многое позаимствовать.
Здравствуйте, c-smile, Вы писали:
CS>вопрос состоит в том что стоит ли овчинка генерализации (на уровне библиотеки или языка) или нет (это я уже про tiscript)?
ПМ это не просто набор условий, это прежде всего средства для деконструкции выражений. Т.е. очень важен биндинг. В языках, где есть ПМ, зачастую присутствуют и структуры данных вроде алгебраических типов, анализировать которые можно *только* через ПМ. Или те же связные списки, которые можно разбирать "рекурсивно". Например, функция вычисления длины:
x::xs — образец, в котором x это первый элемент списка, а xs — хвост, т.е. все остальные элементы, кроме этого. В ФЯ очень много функций, включая такие ФВП как fold и проч. построены по такой схеме.
Без специальный структур данных ценность ПМ несколько падает, но в принципе он все равно может быть полезен. Например, есть паттерн:
[|x,y,z|]
Этот паттерн проверяет, что длина массива равна трем и связывает все элементы массива с переменными x, y и z. Лаконично и понятно, в отличие от императивных проверок.
Здравствуйте, Воронков Василий, Вы писали:
ВВ>Здравствуйте, c-smile, Вы писали:
CS>>вопрос состоит в том что стоит ли овчинка генерализации (на уровне библиотеки или языка) или нет (это я уже про tiscript)?
ВВ>ПМ это не просто набор условий, это прежде всего средства для деконструкции выражений. Т.е. очень важен биндинг. В языках, где есть ПМ, зачастую присутствуют и структуры данных вроде алгебраических типов, анализировать которые можно *только* через ПМ. Или те же связные списки, которые можно разбирать "рекурсивно". Например, функция вычисления длины:
ВВ>
ВВ>x::xs — образец, в котором x это первый элемент списка, а xs — хвост, т.е. все остальные элементы, кроме этого. В ФЯ очень много функций, включая такие ФВП как fold и проч. построены по такой схеме.
ВВ>Без специальный структур данных ценность ПМ несколько падает, но в принципе он все равно может быть полезен. Например, есть паттерн:
ВВ>[|x,y,z|]
ВВ>Этот паттерн проверяет, что длина массива равна трем и связывает все элементы массива с переменными x, y и z. Лаконично и понятно, в отличие от императивных проверок.
Это уже более интересно.
На самом деле я ищу приемлемую форму вот этого: http://goessner.net/articles/jsont/
По идее это должно быть выражаемо именно через ПМ но что-то никак каменный цветок не выходит...
Здравствуйте, c-smile, Вы писали:
CS>Ну как бы car/cdr и без ПМ можно использовать.
Все, что можно делать с помощью ПМ-а, можно в принципе и без ПМ-а, если только в языке нет каких-либо специальных ограничений.
Просто ПМ декларативен и часто позволяет сильно "ужать" код. Паттерны же могут быть и вложенными. x::xs — это простейший пример. Можно и так:
x:(e1, e2)::xs
Тут мы уже хотим, чтобы второй элемент был кортежем и связываем его элементы с переменными.
ВВ>>Без специальный структур данных ценность ПМ несколько падает, но в принципе он все равно может быть полезен. Например, есть паттерн: ВВ>>[|x,y,z|] ВВ>>Этот паттерн проверяет, что длина массива равна трем и связывает все элементы массива с переменными x, y и z. Лаконично и понятно, в отличие от императивных проверок. CS>Это уже более интересно.
CS>На самом деле я ищу приемлемую форму вот этого: http://goessner.net/articles/jsont/ CS>По идее это должно быть выражаемо именно через ПМ но что-то никак каменный цветок не выходит...
JSONT — это типа XSLT для JSON? Может, и стоит тогда посмотреть на ПМ в XSLT? Ведь задачи ровно те же самые. Нечто подобное XPath вполне прокатило бы.
Кстати, для трансформации JSON-а я сейчас XSLT и использую, вполне удобно получается.
Здравствуйте, c-smile, Вы писали:
CS>На самом деле я ищу приемлемую форму вот этого: http://goessner.net/articles/jsont/ CS>По идее это должно быть выражаемо именно через ПМ но что-то никак каменный цветок не выходит...
C PM было бы как-то так:
// псевдокд JS:
var transform_link = function ( {link:{uri:x1,title:x2}} ) {
return make_link_helper(x1,x2);
};
Hyperlua это (и многое другое) умеет :
-- реальный Hyperlua код:
local fun transform_link
| {link = {uri = x1, title = x2}} -> make_link_helper(x1,x2)
end
-- Lua код после компиляции (`#x' это length(x), `~=' значит "неравно") :
local function transform_link(_u0)
if type(_u0)=='table' and #_u0==0 and _u0.link~=nil and
type(_u0.link)=='table' and #_u0.link==0 and
_u0.link.uri~=nil and _u0.link.title~=nil
then
return make_link_helper(_u0.link.uri, _u0.link.title)
else error'pattern-match error'
end
end
ВВ>>[|x,y,z|]
ВВ>>Этот паттерн проверяет, что длина массива равна трем и связывает все элементы массива с переменными x, y и z. Лаконично и понятно, в отличие от императивных проверок.
CS>Это уже более интересно.
CS>На самом деле я ищу приемлемую форму вот этого: http://goessner.net/articles/jsont/ CS>По идее это должно быть выражаемо именно через ПМ но что-то никак каменный цветок не выходит...
Не знаю, насколько это будет полезно, но обработка подобного в терминах Erlang'а была бы, как показано ниже. Самое главное, что вне зависимости от структуры, подход всегда одинаковый. Указываем на входе функции, параметр какого типа нам нужен, и какие именно значения из этого параметра нам нужны — и вперед
(сорри за Erlang, код не самый красивый, нет некоторых проверок)
%% [] -> список/массив
%% {} -> кортеж (тупл)
%% с маленькой буквы — атомы, http://www.trapexit.org/Atom
%% с большой буквы — переменные
%% последняя строка в функции является ее возвращаемым значением
%% _ — это placeholder на случай, если значение нам не нужно
parse_data(Data) ->
self(Data).
self(Data) ->
%% строк в Erlang'е нет, есть списки
["<svg>", elements(Data) ,"</svg>"].
%% Head содержит первый элемент
%% Tail — остаток списка
elements([Head|Tail]) ->
[element(Head), elements(Tail)].
%% принимаем только структуру вида
%% {line, {p1, {X1, Y2}},
%% {p2, {X2, Y2}}}
%% при этом значения координат сразу закидываем
%% в соответствующие переменные (X1, X2, Y1, Y2)
element({line, {p1, {X1, Y2}},
{p2, {X2, Y2}}}) ->
["<line x1=\"", X1, "\" y1=\"", Y1, "\" x2=\"", X2, "\" y1=\"", Y2, "\" />"];
%% принимаем только структуру вида
%% {circle, {center, {X, Y}},
%% {radius, R}}
%% при этом значения координат сразу закидываем
%% в соответствующие переменные (X, Y, R)
element({circle, {center, {X, Y}},
{radius, R}}) ->
["<circle x=\"", X, "\" y=\"", Y, "\" r=\"", R, "\" />"];
%% любые другие элементы просто пропускаем
element(_) ->
"".
Такой код с легкостью пробежится по списку такого типа: