В поисках аналога функций первого порядка в СУБД 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 ]
{
/// Коллекция-список почти любого типа.
Property collection As %Collection.AbstractList Internal,  Private,  ReadOnly,  Transient ];
/// Инициализация свойства collection.
/// 

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

  • объект класса-наследника от %Collection.AbstractList;
    /// 
     
  • список простых элементов в формате $List;
    /// 
     
  • список простых элементов в формате строки. В этом случае sep — разделитель элементов в строке;
    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
    }
    /// Основной метод-обработчик.
    /// 

    /// 
    Аргументы:
    /// 

    /// 
    func:
    1. имя метода экземпляра класса
    2. имя метода класса (любого)
    3. код в формате команды xecute
    4. некоторые сокращённые команды

    /// Примеры вызова:
    /// 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)
    /// 

    /// 
    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)=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
    }
    /// d ##class (test.ForEach).Test ()
    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;
    /// Инициализация свойства field.
    Method %OnNew (fieldAs %Status Internal,  Private,  ServerOnly = 1 ]
    {
        set field=field
        quit $$$OK
    }
    /// Заполнение field первым входным аргументом.
    Method SetField (Args…As %Status
    {
        set field=Args(1,2)
        quit $$$OK
    }
    /// Вывод field и первого входного аргумента.
    Method PrintLn (Args…As %Status
    {
        write Args(1,2), $$$quote(…field),!
        quit $$$OK
    }
    /// Конкатенация field с разделителем (метод экземпляра класса).
    /// 
    Если первый входной аргумент совпадает с field,  генерируем ошибку (для демонстрационных целей!
    )
    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)
    }
    /// Сумма elem (метод класса).
    /// 
    Если первый входной аргумент совпадает с elem (он же field),  генерируем ошибку (для демонстрационных целей!
    )
    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)
    }
    /// Вывод всех аргументов.
    /// 

     elem = элемент коллекции
    /// 
     Args(1) = кол-во переданных аргументов кроме первого,  т.е. elem
    /// 
     Args(1,1) = аргумент 1 (у нас это входной/выходной аргумент
    )
    /// 
     Args(1,2) = аргумент 2
    /// 
     …
    /// 
     Args(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
    }
    }


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

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

    © Habrahabr.ru