Article
· Sep 6, 2017 5m read

DeclarativeCOS — Declarative Programming in Caché

The DeclarativeCOS project is a heartfelt cry about programming in the COS language.

The purpose of the project is to draw attention of the public to improving the inner core of COS.

The idea of the project is the support of a laconic syntax for cycles and collections.

So what is this laconic something that I have come up with? Welcome to the examples below!

Examples

The key concept underlying the project is the declarative approach to writing code. You need to specify WHAT should be used and HOW.

I have personally always longed for a simple operator/command/magic spell in the COS terminal that would allow me to show a collection on the screen exactly the way I wanted to. Now we have two useful goodies to work with: zforeach and $zjoin!

>set words = ##class(%ListOfDataTypes).%New()
>do words.Insert("Hello")
>do words.Insert("World!")

>zforeach $zbind(words, "io:println")
Hello
World!

I should say a few words about the $zbind function. First of all, COS can be extended with custom commands and functions, which is described in detail in the corresponding documentation and this article on the developers’ community portal.

This function creates an instance of the Binder class. Its purpose is to bind a collection to a function that should be applied to each element of the collection. In this case, we use a standard function from DeclarativeCOS called «io:println», which runs a simple command for the given value of value:

>w value,!

The zforeach command works with an instance of the Binder class by traversing the collection and applying a function to each element.

$zjoin — creates a string from a collection by joining its elements and adding a delimiter between them.

Example: create a date based on a day, month and year using “ / ” as a delimiter.

>s numbers = ##class(%ListOfDataTypes).%New()
>d numbers.Insert("04")
>d numbers.Insert("03")
>d numbers.Inset("2017")

>w $zjoin(numbers, " / ")
04 / 03 / 2017

$zmap — creates a new collection from the elements of the original collection and applies the specified function to each of its elements.

Example: convert every number to hex.

>set numbers = ##class(%ListOfDataTypes).%New()
>do numbers.Insert($random(100))
>do numbers.Insert($random(100))
>do numbers.Insert($random(100))

>write "[" _ $zjoin(numbers, ", ") _ "]"
[82, 12, 27]

>set hexNumbers = $zmap(numbers, "examples:toHex")

>write "[" _ $zjoin(hexNumbers, ", ") _ “]”
[52, C, 1B]

$zfind — finds the first element of a collection for which the specified function returns $$$YES. Otherwise, returns a null string.

Example: find a prime number.

>set numbers = ##class(%ListOfDataTypes).%New()
>do numbers.Insert($random(100))
>do numbers.Insert($random(100))
>do numbers.Insert($random(100))

>set primeNumber = $zfind(numbers, "examples:isPrime")

>write "[" _ $zjoin(numbers, ", ") _ "]"
[69, 41, 68]

>write "Prime number: " _ $select(primeNumber="":"<not found>", 1:primeNumber)
Prime number: 41

$zfilter — creates a new collection on the basis of the original collection, but only using elements for which the specified function returns $$$YES. If there are no such elements, it returns an empty collection.

Example: select odd numbers.

>set numbers = ##class(%ListOfDataTypes).%New()
>do numbers.Insert($random(100))
>do numbers.Insert($random(100))
>do numbers.Insert($random(100))

>set filteredNumbers = $zfilter(numbers, "examples:isOdd")

>write "[" _ $zjoin(numbers, ", ") _ "]"
[22, 71, 31]

>write "[" _ $zjoin(filteredNumbers, ", ") _ "]"
[71, 31]

$zexists — checks whether the collection has at least one element for which the specified function returns $$$YES.

Example: check whether the collection contains even numbers.

>set numbers = ##class(%ListOfDataTypes).%New()
>do numbers.Insert($random(100))
>do numbers.Insert($random(100))
>do numbers.Insert($random(100))

>set hasEvenNumbers = $zexists(numbers, "examples:isEven")

>write "[" _ $zjoin(numbers, ", ") _ "]"
[51, 56, 53]

>write "Collection has" _ $case(hasEvenNumbers, 1:" ", 0:" no ") _ "even numbers"
Collection has even numbers

$zcount —count the number of elements in the collection for which the specified function returns $$$YES.

Example: count the number of palindromes.

>set numbers = ##class(%ListOfDataTypes).%New()
>do numbers.Insert($random(1000))
>do numbers.Insert($random(1000))
>do numbers.Insert($random(1000))

>set palindromicNumbersCount = $zcount(numbers, "examples:isPalindromic")

>write "[" _ $zjoin(numbers, ", ") _ "]"
[715, 202, 898]

>write "Count of palindromic numbers: " _ palindromicNumbersCount
Count of palindromic numbers: 2

Installation

To install DeclarativeCOS, all you need to do is to download two files from the project’s GitHub repository:

install.base.xml — just classes. No Z-functions.
install.advanced.xml — %ZLANG routines that add z-functions.

How to use it

  • Inherit the class from DeclarativeCOS.DeclarativeProvider.
  • Implement the class method.
  • Mark this method with the @Declarative annotation.
  • Use the z-functions of DeclarativeCOS.
  • Feel the bliss.

Detailed description

Step 1. Inherit the class from DeclarativeCOS.DeclarativeProvider.

Class MyPackage.IO extends DeclarativeProvider
{
}

Step 2. Implement the class method.

Class MyPackage.IO extends DeclarativeProvider
{

ClassMethod println(value As %String)
{
    w value,!
}

}

Step 3. Mark this method with the @Declarative annotation.

Class MyPackage.IO extends DeclarativeProvider
{

/// @Declarative("myIO:myPrintln")
ClassMethod println(value As %String)
{
    w value,!
}

}

Step 4. Use the z-functions of DeclarativeCOS.

>s words = ##class(%Library.ListOfDataTypes).%New()
>d words.Insert("Welcome")
>d words.Insert("to")
>d words.Insert("DeclarativeCOS!")

>zforeach $zbind(words, "myIO:println")

Step 5. Feel the bliss!

Welcome
to
DeclarativeCOS!

How it works

The DeclarativeCOS project uses the ^DeclarativeCOS global to save information about the methods marked with the @Declarative(declarativeName) annotation. 

Every such method is saved to the global in the following form:

set ^DeclarativeCOS(declarativeName) = $lb(className, classMethod)

For instance, for the io:println function:

set ^DeclarativeCOS("io:println") = $lb("DeclarativeCOS.IO", "println")

Each time we use the io:println function, the search is made in the global, then the $classmethod function calls the original method (DeclarativeCOS.IO # println) for the set value.

Conclusion

DeclarativeCOS is a contribution to the new Caché ObjectScript, the language that really helps developers to quickly write laconic, well-readable and reliable code. Feel free to dive into the ocean of criticism, support and opinions in the comments section under this post!)

Disclaimer: this article and my comments express my personal opinion only and have no relation to the official position of the InterSystems corporation.
 

Discussion (31)2
Log in or sign up to continue

Hello Maks, surely you can. We just need to rename it to something easier to remeber,  then set up README, CODE_OF_CONDUCT and LICENSE files.

This project was actually a response for another thread that you opened a while ago. So what motivated me on doing it was the community feedback (you as well) regarding your other thread.

https://community.intersystems.com/post/declarative-development-cach%C3%A9

Thank you, Herman Slagman!

Yeah, COS could support more modern concepts and technologies. But we should note that COS supports JSON natively now. It is good step to new bless future of COS.

How you think why ISC implements new features slowly?

I think it's inevitable. I think every language with long history should support legacy and any new feature should be well tested. so... Development team of certain language (even if they really want to implement new feature as soon as possible) have to implement new features slowly.

I think another moment is community state. Maybe something couldn't implement because the community  has no strong wish have it in language ecosystem.

Maks I fully support your position.
There are very few languages where you can run code written  40 yrs. ago with no modification !
That's what counts for customers.

You have other languages in parallel like MV or BASIC besides all the embedded things like &SQL(), &HTML(), &JS()..

But with same reasoning you may ask why is Java, JavaScript  or C, C# not changed?
Because you got GO, Angular, ..... to have attractive extensions.
COS has it's ZZ*, $ZZ* as extensions.  ( ZZ* ! )

Isn't this enough for thousands of developers that haven't asked for it nor have a need  for it?  

JavaScript is evolving day by day and really, really fast. Check what ECMAScript 6 and even 7 to see what it's becoming.

COS could evolve faster as well if there was some kind of COS API for parsing and rewriting the language itself. Something that could transpile features unknown to the current COS language. Just like BabelJS does.

However COS is proprietary, and just like Linux and Windows, open-source projects  evolve faster due to the community involvement. I don't mean to make it fully open, but provide the community a way to interact with it by providing with new features proposals.

> But with same reasoning you may ask why is Java, JavaScript  or C, C# not changed?

Java, C# and JavaScript have major new features every two to three years

I'm not talking about taking away old features, but adding new ones.

A major overhaul of Cache (Mumps) was when Objects, ProcedureBlocks and the $List were introduced, that didn't break anything.

Adding native JSON support was also very important.

Thank you Robert Cemper!

Yeah, COS really has ZZ extensions. Also, you have interesting point of view on Go and Angular smiley

Isn't this enough for thousands of developers that haven't asked for it nor have a need  for it?  

Years ago engineers was happy because they have simplest command like ADD, MOV, JMP.

And now we have a lot of abstraction layers which cover low-level commands.

And I think our programs became better.

 

Please, don't think that I am disagree with you. I just try to discover the root of... What developers really need?

Maybe. Maybe ZZ extensions is okay. Maybe. Not every cool thing or framework should be integrated into language. yes

You are right. We were happy writing a whole operating system in Assembly language (VAX/MACRO32) and we found it partially overdone compared to PDP-11/MACRO16. But we knew exactly what every bit in our box did. Not even C could give us that level of control.

I personally think a programming language reaches its limits when only a low percentage of developers still oversee it.
Therefore I think your idea to have a new layer on top of COS is much better as it doesn't impose any limitations that are required for backward compatibility.
So you are free to take with you what seems useful and leave behind old junk.

COS contains a real bad example that causes troubles over and over for beginners:
IF  / ELSE old style manipulating $T   vs.   IF { } ELSE { } new style not touching $T 
and some others

That must no happen again. With a new layer you leave this behind and have all freedom.

Maks, thanks for that project!

Do you want to add a sort of "For Each" for a global?

Suppose a have a global ^A(index)=value

And I need to go through all the indexes to do something with the value. 

Currently, I do the following in Objectscript to perform this:

s iter=$Order(^A(""))

while iter'="" {

set value=^A(iter)

/// do something with value

set iter=$O(^A(iter))

}

Would be great to change this to something like that:

zforeach(iter,^A) {

set value=^A(iter)

/// do something with value

}

Try it like this:

/// d ##class(isc.test.Utils).Test2()
ClassMethod Test2()
{
    set from = "^A"
    set to = "^B"
    kill @from, @to
    set @from@(1) = "A"
    set @from@(2) = "B"
    set @from@(3) = "C"
    
    do ..InvertList(from, to)
    
    zwrite @from,@to
}

ClassMethod InvertList(from, to) As %Status
{
    #define ForAll(%in,%gn) s gn%in=$na(%gn) s %in="" f { s %in=$o(@gn%in@(%in)) q:%in=""
    #define EndFor }

    $$$ForAll(key, @from)
        set @to@(@from@(key))=key
    $$$EndFor
}

For me it returns

^A(1)="A"
^A(2)="B"
^A(3)="C"
^B("A")=1
^B("B")=2
^B("C")=3

I was looking at @David Underhill's code for Metrics project and decided that for one level global traversing his code style is the most readable:

    s database="" for {
        s database=$o(databases(database))
        q:database=""

...

}

And in general:

s iter="" for {

    s iter=$o(^array(iter)) q:iter=""

 // do something

}

So, use for instead of while for global traversing and this is readable and you never miss the next set is in while example.