В поисках аналога функций первого порядка в СУБД Caché

Пост написан в дополнение к статье Декларативная разработка на Caché.

 
[2, 3, 5, 7, 11, 13, 17].forEach(function(i) {
  console.log(i);
});

Как делать такое в Caché с помощью COS?
Под катом несколько упражнений на заданную тему.

Чтобы увидеть, как средствами COS – одного из серверных языков, встроенных в Caché – можно добиться такого же лаконичного, наглядного и гибкого кода, был разработан собственный класс test.ForEach для работы с коллекциями-списками.

Исходный код класса test.ForEach

/// Класс-итератор для коллекций.

Class test.ForEach Extends %RegisteredObject 
Final ]

{

/// Коллекция-список <s>почти</s> любого типа.

Property 
collection As %Collection.AbstractList InternalPrivateReadOnlyTransient ];

/// Инициализация свойства <property>collection</property>.

/// <br><br> Допустимые аргументы <var>val</var>:

/// <br> <li>объект класса-наследника от %Collection.AbstractList;

/// <br> <li>список простых элементов в формате $List;

/// <br> <li>список простых элементов в формате строки. В этом случае <var>sep</var> – разделитель элементов в строке;

Method 
%OnNew(valsep ","As %Status PrivateServerOnly = 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(scsc

  

    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 $$$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)

Исходный код класса test.myclass

Class test.myclass Extends %RegisteredObject

{

/// Строковое поле.

Property 
field;

/// Инициализация свойства <property>field</property>.

Method 
%OnNew(fieldAs %Status InternalPrivateServerOnly = 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(elemArgs…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(elemArgs…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

}

}

Исходный код программы prog.mac

    #include %systemInclude

    

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: данная статья опубликована с разрешения автора, пожелавшего остаться неизвестным.

Спасибо за внимание!

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *