[2, 3, 5, 7, 11, 13, 17].forEach(function(i) {
console.log(i);
});
Как делать такое в Cache с помощью COS?
Под катом несколько упражнений на заданную тему.
Чтобы увидеть, как средствами COS — одного из серверных языков, встроенных в Cache — можно добиться такого же лаконичного, наглядного и гибкого кода, был разработан собственный класс test.ForEach для работы с коллекциями-списками.
Class test.ForEach Extends %RegisteredObject [ Final ]
{
/// Коллекция-список <s>почти</s> любого типа.
Property collection As %Collection.AbstractList [ Internal, Private, ReadOnly, Transient ];
/// Инициализация свойства <property>collection</property>.
/// <br><br> Допустимые аргументы <var>val</var>:
/// <br> <li>объект класса-наследника от %Collection.AbstractList;
/// <br> <li>список простых элементов в формате $List;
/// <br> <li>список простых элементов в формате строки. В этом случае <var>sep</var> - разделитель элементов в строке;
Method %OnNew(val, sep = ",") As %Status [ Private, ServerOnly = 1 ]
{
if $IsObject(val) {
quit:'val.%Extends("%Collection.AbstractList") $$$ERROR($$$OrefInvalid,val)
set i%collection=val
}else{
set i%collection=##class(%ListOfDataTypes).%New()
do ..collection.InsertList($select($listvalid(val):val,1:$listfromstring(val,sep)))
}
quit $$$OK
}
/// Основной метод-обработчик.
/// <br>
/// <br>Аргументы:
/// <br>
/// <br><var>func</var>:<ol><li>имя метода экземпляра класса</li><li>имя метода класса (любого)</li><li>код в формате команды <link href=/DocBook.UI.Page.cls?KEY=RCOS_cxecute>xecute</link></li><li>некоторые сокращённые команды</li></ol>
/// Примеры вызова:<example>
/// s obj=##class(test.ForEach).%New("2,3,5")
/// ; для каждого элемента коллекции будет вызван соответствующий метод класса с передачей аргументов.
/// ; Первый аргумент выходной/входной, остальные - входные, но это лишь способ соглашения.
/// ; При желании можно поменять их местами, сделать несколько выходных и т.д.
/// d obj.Do("className:methodName",.result,param1,param2,paramN)
/// ; сумма элементов (имеет смысл лишь для коллекции чисел)
/// d obj.Do("+",.result)
/// ; произведение (имеет смысл лишь для коллекции чисел)
/// d obj.Do("*",.result)
/// ; конкатенация с разделителем (имеет смысл лишь для коллекции простых типов)
/// d obj.Do("_",.result,separator)
/// ; минимум (имеет смысл лишь для коллекции простых типов)
/// d obj.Do("min",.result)
/// ; максимум (имеет смысл лишь для коллекции простых типов)
/// d obj.Do("max",.result)
/// ; среднее (имеет смысл лишь для коллекции чисел)
/// d obj.Do("avg",.result)
/// ; любой код, где el=элемент коллекции, args=переданные аргументы
/// d obj.Do($lb("s args(1,1)=args(1,1)+el"),.result) ; эквивалент "+"
/// ; вызов подпрограммы sub^prog с передачей аргументов
/// d obj.Do($lb("d sub^prog(el,args...)"),.result,param1,param2,paramN)
/// </example>
///
Method Do(func = "+", Args...) As %Status
{
#define ReturnOnError(%expr) s sc=%expr ret:$$$ISERR(sc) sc
quit:'..collection.Count() $$$OK
if func="+" {
set func=$listbuild("s args(1,1)=args(1,1)+el")
}elseif func="*" {
set func=$listbuild("s args(1,1)=args(1,1)*el")
}elseif func="_" {
set func=$listbuild("s args(1,1)=args(1,1)_args(1,2)_el")
}elseif func="min" {
set func=$listbuild("s:el<args(1,1) args(1,1)=el"),Args(1)=999999999999999
}elseif func="max" {
set func=$listbuild("s:el>args(1,1) args(1,1)=el"),Args(1)=-999999999999999
}elseif func="avg" {
set func=$listbuild("s args(1,1)=el/args(1,2)+args(1,1)"),Args=2,Args(2)=..collection.Count() kill Args(1)
}
if $listvalid(func) {
set cmd=$list(func)
$$$ReturnOnError(##class(%Routine).CheckSyntax(" "_cmd))
set cmd="(el,args...){"_cmd_"}"
set key = ""
for {
set el = ..collection.GetNext(.key)
quit:key=""
xecute (cmd,el,.Args)
}
}else{
if func[":" {
set className=$piece(func,":",1)
set methodName=$piece(func,":",2)
quit:'##class(%Dictionary.MethodDefinition).IDKEYExists(className,methodName) $$$ERROR($$$MethodDoesNotExist,func)
quit:'$$$defMemberKeyGet(className,"m",methodName,23) $$$ERROR($$$GeneralError,$$$FormatText("Метод %1 не является методом класса %2",methodName,className))
set key = ""
for {
set el = ..collection.GetNext(.key)
quit:key=""
$$$ReturnOnError($classmethod(className,methodName,el,.Args))
}
}else{
set methodName=func
set key = ""
for {
set el = ..collection.GetNext(.key)
quit:key=""
set className=$classname(el)
return:'##class(%Dictionary.MethodDefinition).IDKEYExists(className,methodName) $$$ERROR($$$MethodDoesNotExist,className_":"_methodName)
return:$$$defMemberKeyGet(className,"m",methodName,23) $$$ERROR($$$GeneralError,$$$FormatText("Метод %1 не является методом экземпляра класса %2",methodName,className))
$$$ReturnOnError($method(el,methodName,.Args))
}
}
}
quit $$$OK
}
/// <example>d ##class(test.ForEach).Test()</example>
ClassMethod Test() [ Internal ]
{
set old=$system.Process.Undefined(2)
try{
;============================== КОЛЛЛЕКЦИЯ ПРОСТЫХ ТИПОВ ДАННЫХ =============================
set t=##class(test.ForEach).%New("2,3,5")
;s t=##class(test.ForEach).%New("2,3,5,asd")
;s t=##class(test.ForEach).%New(##class(test.ForEach).%New()) ; раскомментируйте, чтобы увидеть ошибку
if '$IsObject(t) $$$ThrowStatus(%objlasterror)
write !,"==========",!,"test.myclass:Dump",!!!
$$$ThrowOnError(t.Do("test.myclass:Dump"))
; или $$$ThrowOnError(t.Do("test.myclass:Dump",.result))
write !,"==========",!,"test.myclass:Dump(.r=""result"",""p1"",""p2"")",!!!
set r="result" $$$ThrowOnError(t.Do("test.myclass:Dump",.r,"p1","p2"))
write !,"==========",!,"test.myclass:Sum(.r)",!!!
$$$ThrowOnError(t.Do("test.myclass:Sum",.r)) write "Результат = ",r,!
;$$$ThrowOnError(t.Do("test.myclass:Sum",.r,5)) ; раскомментируйте, чтобы увидеть ошибку
write !,"==========",!,"+10",!! set r=10
$$$ThrowOnError(t.Do(,.r)) write "Результат = ",r,!
write !,"==========",!,"+",!! kill r
$$$ThrowOnError(t.Do(,.r)) write "Результат = ",r,!
write !,"==========",!,"*",!! set r=1
$$$ThrowOnError(t.Do("*",.r)) write "Результат = ",r,!
write !,"==========",!,"_ + разделитель=""^""",!! kill r
$$$ThrowOnError(t.Do("_",.r,"^")) write "Результат = ",r,!
write !,"==========",!,"min (входной аргумент не учитывается)",!!!
set r="asd" $$$ThrowOnError(t.Do("min",.r)) write "Результат = ",r,!
write !,"==========",!,"max (входной аргумент не учитывается)",!!!
set r="asd" $$$ThrowOnError(t.Do("max",.r)) write "Результат = ",r,!
write !,"==========",!,"avg (входной аргумент не учитывается)",!!!
set r="asd" $$$ThrowOnError(t.Do("avg",.r)) write "Результат = ",r,!
write !,"==========",!,"s args(1,1)=args(1,1)+el",!!!
kill r $$$ThrowOnError(t.Do($listbuild("s args(1,1)=args(1,1)+el"),.r)) write r,!
write !,"==========",!,"d sub^prog(el,args...) [.r=""r"",""p1"",""p2""]",!!!
set r="r" $$$ThrowOnError(t.Do($listbuild("d sub^prog(el,args...)"),.r,"p1","p2"))
;============================== КОЛЛЛЕКЦИЯ СЛОЖНЫХ ТИПОВ ДАННЫХ =============================
set list=##class(%ListOfObjects).%New()
for i="f1","f2","f3" do list.Insert(##class(test.myclass).%New(i))
;f i="f1","f2","f3",7 d list.Insert(##class(test.myclass).%New(i))
set t=##class(test.ForEach).%New(list)
if '$IsObject(t) $$$ThrowStatus(%objlasterror)
write !,"++++++++++",!,"test.myclass:Dump",!!!
$$$ThrowOnError(t.Do("test.myclass:Dump"))
write !,"++++++++++",!,"PrintLn",!!!
$$$ThrowOnError(t.Do("PrintLn"))
write !,"++++++++++",!,"PrintLn(,""Элемент = "")",!!!
$$$ThrowOnError(t.Do("PrintLn",,"Элемент = "))
write !,"++++++++++",!,"Concat(.r)",!! kill r
$$$ThrowOnError(t.Do("Concat",.r)) write "Результат = ",r,!
;$$$ThrowOnError(t.Do("Concat",.r,"f3")) w "Результат = ",r,! ; раскомментируйте, чтобы увидеть ошибку
write !,"++++++++++",!,"SetField(,""blablabla"") + PrintLn(,""Элемент = "")",!!!
$$$ThrowOnError(t.Do("SetField",,"blablabla")) $$$ThrowOnError(t.Do("PrintLn",,"Элемент = "))
write !,"++++++++++",!,"d el.PrintLn(.args)",!!!
$$$ThrowOnError(t.Do($listbuild("d el.PrintLn(.args)")))
write !,"++++++++++",!,"w ""field="",el.field,!",!!!
$$$ThrowOnError(t.Do($listbuild("w ""field="",el.field,!")))
}catch(ex){
#dim ex As %Exception.AbstractException
write ex.DisplayString()
}
do $system.Process.Undefined(old)
}
}
В коде класса применялись некоторые возможности COS:
- Args... (передача произвольного числа аргументов в метод/процедуру/программу);
- XECUTE или $XECUTE (выполнение произвольных команд COS);
- $COMPILE (компиляция/проверка синтаксиса кода);
- $CLASSMETHOD (вызов произвольного метода класса с передачей произвольного числа аргументов);
- $METHOD (вызов произвольного метода экземпляра класса с передачей произвольного числа аргументов);
- Библиотека встроенных классов.
Внимание! Все приведенные ниже примеры предполагают, что Undefined=2.
Этот режим можно установить в терминале.
> set old=$system.Process.Undefined(2)
Выполнить тесты и не забыть потом вернуть на место
> do $system.Process.Undefined(old)
{
/// Строковое поле.
Property field;
/// Инициализация свойства <property>field</property>.
Method %OnNew(field) As %Status [ Internal, Private, ServerOnly = 1 ]
{
set ..field=field
quit $$$OK
}
/// Заполнение <property>field</property> первым <u>входным</u> аргументом.
Method SetField(Args...) As %Status
{
set ..field=Args(1,2)
quit $$$OK
}
/// Вывод <property>field</property> и первого <u>входного</u> аргумента.
Method PrintLn(Args...) As %Status
{
write Args(1,2),$$$quote(..field),!
quit $$$OK
}
/// Конкатенация <property>field</property> с разделителем (<span style="color:green;">метод <b>экземпляра</b> класса</span>).
/// <br>Если первый входной аргумент совпадает с <var>field</var>, генерируем ошибку (<span style="color:red;">для демонстрационных целей!</span>)
Method Concat(Args...) As %Status
{
set Args(1,1)=Args(1,1)_Args(1,2)_..field
quit $select(..field=Args(1,2):$$$ERROR($$$GeneralError,$$$FormatText("Возникла ошибка на элементе: %1",..field)),1:$$$OK)
}
/// Сумма <var>elem</var> (<span style="color:green;">метод класса</span>).
/// <br>Если первый <u>входной</u> аргумент совпадает с <var>elem</var> (он же <property>field</property>), генерируем ошибку (<span style="color:red;">для демонстрационных целей!</span>)
ClassMethod Sum(elem, Args...) As %Status
{
set Args(1,1)=Args(1,1)+elem
quit $select(elem=Args(1,2):$$$ERROR($$$GeneralError,$$$FormatText("Возникла ошибка на элементе: %1",elem)),1:$$$OK)
}
/// Вывод всех аргументов.
/// <br><br> <var>elem</var> = элемент коллекции
/// <br> <var>Args</var>(1) = кол-во переданных аргументов кроме первого, т.е. <var>elem</var>
/// <br> <var>Args</var>(1,1) = аргумент 1 (<span style="color:red;">у нас это входной/выходной аргумент</span>)
/// <br> <var>Args</var>(1,2) = аргумент 2
/// <br> …
/// <br> <var>Args</var>(1,n) = аргумент n
ClassMethod Dump(elem, Args...) As %Status
{
set params=""
for i=2:1:Args(1) set params=params_$listbuild(Args(1,i))
if '$IsObject(elem) {
set el=elem
}elseif elem.%Extends("test.myclass"){
set el=elem.field
}else{
set el=elem.%ClassName($$$YES)
}
write "Элемент = ",$$$quote(el),", Выходной аргумент = ",$$$quote(Args(1,1)),", Дополнительные аргументы = ",$$$quote($listtostring(params)),!
quit $$$OK
}
}
sub(el,args...) public {
write "--------",!,"el = ",$$$quote(el),!
zwrite args
write !
}
Поехали!
Инициализация объекта ForEach
Инициализация происходит с помощью метода test.ForEach.%New(val,sep)
Первый параметр val принимает коллекцию литералов, либо список, либо коллекцию объектов.
Второй параметр sep — разделитель коллекции литералов.
1. Инициализация коллекции литералов
set tmp=##class(test.ForEach).%New("2,3,5")
или
set tmp=##class(test.ForEach).%New($listbuild(2,3,5))
2. Инициализация коллекции литералов через произвольный разделитель
Например через разделитель ";"
set tmp=##class(test.ForEach).%New("2;zxc;5;asd,ert",";")
или
set tmp=##class(test.ForEach).%New($listbuild(2,"zxc",5,"asd,ert"))
3. Инициализация списка объектов
set list=##class(%ListOfObjects).%New()
for i="f1","f2","f3",7 do list.Insert(##class(test.myclass).%New(i))
set tmp=##class(test.ForEach).%New(list)
Внимание! Класс test.ForEach в методе %New ожидает коллекцию-наследника от %Collection.AbstractList
Примеры использования
В классе test.myclass реализованы несколько методов, которые мы будем вызывать для каждого из элементов коллекции.
Например Dump — выводит информацию об элементе и переданных параметрах.
Sum — суммирует аргументы, выводит результат.
Примеры с коллекцией чисел
Инициализируем коллекцию:
set tmp=##class(test.ForEach).%New("2,3,5")
Выполним в терминале:
>do tmp.Do("test.myclass:Dump")
Элемент = 2, Выходной аргумент = "", Дополнительные аргументы = ""
Элемент = 3, Выходной аргумент = "", Дополнительные аргументы = ""
Элемент = 5, Выходной аргумент = "", Дополнительные аргументы = ""
>set r="result" do tmp.Do("test.myclass:Dump",.r,"p1","p2")
Элемент = 2, Выходной аргумент = "result", Дополнительные аргументы = "p1,p2"
Элемент = 3, Выходной аргумент = "result", Дополнительные аргументы = "p1,p2"
Элемент = 5, Выходной аргумент = "result", Дополнительные аргументы = "p1,p2"
>kill r do tmp.Do("test.myclass:Sum",.r) write r
10
>kill r do $system.OBJ.DisplayError(tmp.Do("test.myclass:Sum",.r,5))
ОШИБКА #5001: Возникла ошибка на элементе: 5
>do $system.OBJ.DisplayError(tmp.Do("PrintLn"))
ОШИБКА #5654: Метод '2:PrintLn' не существует
>do $system.OBJ.DisplayError(tmp.Do("test.myclass:PrintLn"))
ОШИБКА #5001: Метод PrintLn не является методом класса test.myclass
>set r=10 do tmp.Do(,.r) write r
20 (=10 +2+3+5)
>kill r do tmp.Do(,.r) write r
10 (=2+3+5)
>set r=-10 do tmp.Do("+",.r) write r
0 (=-10 +2+3+5)
>set r=1 do tmp.Do("*",.r) write r
30 (=2*3*5)
>kill r do tmp.Do("_",.r,"^") write r
^2^3^5 (склейка с разделителем)
>do tmp.Do("min",.r) write r
2 (минимум)
>do tmp.Do("max",.r) write r
5 (максимум)
>do tmp.Do("avg",.r) write r
3.333333333333333334 (=(2+3+5)/3)
>kill r do tmp.Do($listbuild("set args(1,1)=args(1,1)+el"),.r) write r
10 (=2+3+5)
>set r="r" do tmp.Do($listbuild("do sub^prog(el,args...)"),.r,"p1","p2")
--------
el = 2
args=1
args(1)=3
args(1,1)="r"
args(1,2)="p1"
args(1,3)="p2"
--------
el = 3
args=1
args(1)=3
args(1,1)="r"
args(1,2)="p1"
args(1,3)="p2"
--------
el = 5
args=1
args(1)=3
args(1,1)="r"
args(1,2)="p1"
args(1,3)="p2"
>set r="r" do tmp.Do($listbuild("do1 sub^prog(el,args...)"),.r,"p1","p2")
ОШИБКА #5745: Ошибка компиляции!
Примеры использования для коллекции объектов
Инициализация:
set list=##class(%ListOfObjects).%New()
for i="f1","f2","f3" do list.Insert(##class(test.myclass).%New(i))
set tmp=##class(test.ForEach).%New(list)
Проверка в терминале:
>do tmp.Do("test.myclass:Dump")
Элемент = "f1", Выходной аргумент = "", Дополнительные аргументы = ""
Элемент = "f2", Выходной аргумент = "", Дополнительные аргументы = ""
Элемент = "f3", Выходной аргумент = "", Дополнительные аргументы = ""
>do tmp.Do("PrintLn")
"f1"
"f2"
"f3"
>do tmp.Do("PrintLn",,"Элемент = ")
Элемент = "f1"
Элемент = "f2"
Элемент = "f3"
>kill r do tmp.Do("Concat",.r,"**") write r
**f1**f2**f3
>kill r do $system.OBJ.DisplayError(tmp.Do("Concat",.r,"f3"))
ОШИБКА #5001: Возникла ошибка на элементе: f3
>do $system.OBJ.DisplayError(tmp.Do("PrintLn1"))
ОШИБКА #5654: Метод 'test.myclass:PrintLn1' не существует
>do $system.OBJ.DisplayError(tmp.Do("Sum",.r))
ОШИБКА #5001: Метод Sum не является методом экземпляра класса test.myclass
>do tmp.Do("SetField",,"blablabla"), tmp.Do("PrintLn",,"Элемент = ")
Элемент = "blablabla"
Элемент = "blablabla"
Элемент = "blablabla"
>do tmp.Do($listbuild("do el.PrintLn(.args)"))
"blablabla"
"blablabla"
"blablabla"
>do tmp.Do($listbuild("write ""field="",el.field,!"))
field=blablabla
field=blablabla
field=blablabla
Без внимания остались другие типы коллекций, например: массивы, глобалы, таблицы, потоки. Но зато теперь вы знаете «как это работает»…
Исходники классов и примеров.
Disclaimer: данная статья опубликована с разрешения автора, пожелавшего остаться неизвестным.
Спасибо за внимание!
Комментарии (6)
tsafin
19.08.2015 17:09+1Много вопросов возникает к реализации метода Do («Почему она делает столько всего из одного места через такой странный интерфейс?»). Если бы я делал агрегатные функции на коллекциях, я бы выделил отдельные, нормально доступные функции Min, Max, Sum, Product, Average. Это предоставило бы более высокоуровневый интерфейс со строгой типизацией (насколько возможно). Вместо такого перегруженного интерфейса, без проверок и странным способом вернуть значение.
(Более того, такие агрегатные функции могли бы возвращать объект такой же коллекции, что позволяло бы применять их каскадным образом.)dolphin278
19.08.2015 20:44+1Можно было бы просто реализовать
reduce()
, как базовый примитив, из него можно сделатьmap()
, и остальные агрегаты.
dolphin278
19.08.2015 20:47+1Мерили сравнительную скорость работы такого итератора с обычным циклом?
Маленький вброс — сделать класс-итератор, который бы внедрял тело итерации макроподстановкой при компиляции и сравнить скорость работы по сравнению с вызовом внешнего метода, например. По идее, тогда можно сравняться с обычным циклом.
jxcoder
Хорошо, что данную тему поднимают! Спасибо!