Open Exchange App DeclarativeCOS — Declarative Programming in Caché

ObjectScript, 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.
 

  • + 6
  • 0
  • 1240
  • 31

Comments

Rubens Silva! I glad to see your project! Your implementation supports base things of Functional Programming like Monoid, Functor and etc.

Good work!

Did you try to use it in some real project?

No, it's a proof-of-concept, my purpose was to discover a way to express the minimal features needed to develop in a functional fashion.

You can use it with real projects and it will work as it should, the only real issue might be regarding the performance in a long term, since this library uses %ConstructClone to keep the monoids pure.

Do you plan to support and improve your proof-of-concept project?

I would like to. But I need some clue about how to isolate monoids from the global scope, this would push the development a lot further. I mean... it would be wonderful if subroutines could be passed as parameters, you know. But this is out of my reach yet.

For now what I could do is to add more methods to handle different type of datas.

Would you mind if I join your project?) I think it would be good experience to me) Maybe we can start to make something interesting)

 

About current issues. Yes, I understand you.

 

Why you decided to make your proof-of-concept project? :-)

Hello Maks.

Not that I'm against using e-mail, however I think it's more resourceful if we keep the discussion opened for whoever has interest on participating. We could use the community forum as a start point and as the project expands into something solid we migrate to a github issue.

While I find your initiative very praiseworthy, it's a shame ISC doesn't do anything anymore at COS.

There could be a lot of modernization/expansion on the language itself (your declarative efforts), but also on the object level (Design by Contract to name one) but also on the level of protocols (GRPC, MQTT, GraphQL)

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.

Thank you for BabelJS. Never heard about it. cool

So, you know, I thought about creating new language which could be translated into COS.

The language have to support all modern concepts like FP, OOP, Generics (Java generics) and etc.

What do you think about it?

I think this should be the way to go: "New language translated  to COS"
And I'm sure it would be better than the ugly attempt to map COS into HTML as done in CSP winksad  
yes

wink thank you! I think it would be interesting! Thank you for your support! cool

I just want to say that these features have been around for a very long time in some languages, one of them developed in Caché's neighborhood (Scheme).

> 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.

[@Robert.Cemper], would you mind to take participation in new lang discussion?

[@robert.cemper] Please, write me on my email atygaev.mi@gmail.com

I will add you to private google doc discuss.

hm... Maybe I missed something but I didn't understand where "it sounds like another New Language project"

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

}

Enjoy.

#define ForAll(%in,%gn) s gn%in=$na(%gn) s %in="" f { s %in=$o(@gn%in@(%in)) q:%in=""
#define EndFor  }

set glvn = "^MyGlobal"
$$$ForAll(value, @glvn)
    write value,!
$$$EndFor

Says 

#1038: Private variable not allowed : 'gniter=$na(from)' : Offset:21 [zInvertList+1^Example.Globals.1]

for

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(iter,from)

set value=@from@(iter)

set @to@(value)=iter

$$$EndFor

}

"For Each" for a global?

This is For each for a global, not local variable.

Global name is in the var already. Here is the code I needed it for:

ClassMethod InvertList(from, to) As %Status

{



set iter=$o(@from@(""))

while iter'="" {

 set value=@from@(iter)

 set @to@(value)=iter

 set iter=$O(@from@(iter))

 }

}

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.