Language Extensions

This is a posting about a particular feature of Caché which I find useful but is probably not well known or used. I am referring to the feature of Language Extensions.

This feature allows you to extend the commands, special variables and functions available in Caché Object Script with commands, special variables and functions of your own. This functionality also applies to other languages the Caché supports at the server, including Caché Basic and Multivalue Basic.  


Why would I need or want to add new commands ?

There are a number of use cases. Typically, I like to use this to save time during development and debugging, by wrapping up frequently used function calls, or a series of commands, or just a long function call,  into a single abbreviated custom command.

Let's look at an example.  Developers debugging at the command line regularly need to retrieve the error text of the last error raised, represented by the status object variable (%objlasterror), that would be defined in their partition.  This is how you do it:

 

> Do ##class(%SYSTEM.Status).DisplayError(%objlasterror)

In the above call, we are invoking the DisplayError method from the %SYSTEM.Status class, passing in %objlasterror.  This is a long string to type frequently, inviting typos causing delay.  What we can do is introduce our own site’s short command, that would do the same thing.  Our new command could be designed to optionally take any status object variable as an argument, and left off, %objlasterror is assumed to be the argument.

Here is the new command which I have called ‘ZOE’, in action:
 

SAMPLES>set p=##class(Sample.Person).%New()
SAMPLES>do p.%Save()    ; (save will fail, and define %objlasterror).
SAMPLES>zoe
ERROR #7209: Datatype value '' does not match PATTERN '3N1"-"2N1"-"4N'

 

SAMPLES>set tSC= ##class(%SYSTEM.Status).Error(5001,”Sample Status Text”)
SAMPLES>zoe tSC
ERROR #5001: Sample Status Text

So how are custom commands, special variable and functions implemented ?

Extensions are implemented by creating a Caché Objectscript Routine specifically called %ZLANGCnn, %ZLANGVnn or %ZLANGFnn, that define the logic behind new commands, special variables and functions respectively.

The nn in the above routine names, denote the languages in which these extensions are to be made available. Common nn values include:

00 - Caché Object Script
09 - Caché Basic
11 - Multivalued Basic

(See documentation link at the bottom of this post for the full list)

I have built the routine %ZLANGC00 to define ZOE command above, and saved this in the %SYS namespace. I can add as many language extension commands as a I like inside of the same routine (and that routine can of course invoke other code).

Here is a sample %ZLANGC00 routine I defined for this post:

%ZLANGC00 ; Language Extensions (Commands) - SP
          ;
          Quit    ; quit nicely if this routine is actually invoked directly.
         
          ; Command to display error text of status object supplied, OR of %objlasterror
          ; if no argument defined.
ZOERROR(%err) do ZOE(.%err)  ; note the '.' so it works if %err is undefined.
ZOE(%err) ;
          if $get(%err)="",$get(%objlasterror)'="" {
               set %err=%objlasterror
          }
          if $get(%err)'="" do $System.OBJ.DisplayError(%err)
          quit

ZOLIST    ; Command to quickly output the list of defined objects in my partition
ZOL       ;
          do $System.OBJ.ShowObjects()
          quit

 


 

Notice also that in the above example, that I have also introduce a long-handed alias (ZOERROR) for the ZOE command which will drop through and do exactly the same thing.  There is also a second, argument-less command ZOL (ZOLIST) for dumping out the currently defined object variables. Note that when implementations of custom code do not take arguments (eg ZOLIST, ZOL) code can just drop through from one line label (ZOLIST) to the next. However, if arguments are required (eg ZOERROR and ZOE), then the long-handed function needs to explicitly call the short-handed function.

 

Similarly, you can create your own custom Functions.  Here is the implementation of a function to return the r , instead of commands.  Consider for example, a function to return Year Quarter given a $Horolog date.

%ZLANGF00 ; Language Extensions (Functions) - SP
          ;
          Quit     ; quit nicely if this routine is actually invoked directly.

          ; Function to return the quarter (Q1, Q2, etc..) of a given date supplied in the
          ; internal date format ($HOROLOG).
          ;
ZYEARQ(hdate)    ;
          do ZYQ(.hdate)
ZYQ(hdate)       ;
         ;
         quit:+$get(hdate)'?5n ""   ; quit if argument passed in, isn't in the format
    
         set date=$zdate(hdate,1)   ; returned AS MM/DD/YY
         set month=+date            ; convert date string to numeric, returns month alone.
    
         set quarter=$normalize((month+1)/3,0)
         quit "Q"_quarter

 

Note: From a best practice stand-point, functions like the example above are probably more intended for use by application code (rather than an administrator or developer working at the command-line prompt) and I would ideally prefer to implement the above as a method in a Utility-type class of an application.

Usage:

SAMPLES>WRITE $ZYQ($horolog)
“Q2”


Finally, you can define your own environment’s special variables. Special variables are variables maintained by the system.

You can retrieve, and in some cases, set, their value.  A common read-only special variable is, for example, $ZVERSION which returns a string denoting the currently installed version.    A special variable you can set is $NAMESPACE, which has the effect of switching namespaces for you automatically.

If you implement a special variable, that records a value, you are responsible for storing that value somewhere.

The following is an implementation of a special variable that returns the number of seconds elapsed since midnight

%ZLANGV00    ; Custom extended Special variables - SP
        ;
        Quit     ; quit nicely if this routine is actually invoked directly.
            
        ; Special variable representing seconds since midnight
ZHTIME() quit $$ZHT()
ZHT()   quit $piece($horolog,",",2)

 

Usage:

SAMPLES>Write !,"Good ",$select($zht>43200:"Afternoon.",1:"Morning.")

 

Conclusion and final thoughts:

Here are a couple of things to bare in mind if you intent to adopt this functionality.

I believe it is worth coming up with a handful of useful commands and socialise your proposal with your development team for approval and review, then finally, implementing as required.  This is because once you implement %ZLANG* routines in your environment, they are available to all, and in all namespaces.

I do believe that a small set is warranted, don’t go overboard, and definitely only if there is no native alternative.

Keep commands short, with the long version (if you implement it) no more than 6-8 characters.

Language extensions are inherently slower calling native commands, functions or variables. Whilst you can invoke your new features in your application, as part of your application, it might be worth designating their use to command line debugging or administration, choosing instead to use the native commands within the application code.

When you use your extended command, functions or variables, they behave just like native ones, which means they are case insensitive, and, obey other structures like post conditional expressions. Eg   write:X>1 $zht

Be careful not to define customer extensions that overlap existing native commands, functions or special variables, as, your version will not work.

Be careful when implementing your custom code, that it does not modify state that you would expect to remain the same, after your code runs - for example, special variables like $ZREFERENCE (the current global reference) or $TEST (truth value resulting from the last command using the timeout option) should not change if you execute your custom command.

Finally - the documentation on this topic can be found here:

http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GSTU_customize#GSTU_customize_zlang

Steve

  • + 10
  • 1
  • 782
  • 5

Comments

Great article, I find this feature very useful too, here's my %ZLANGC00:

 ; %ZLANGC00
 ; custom commands for ObjectScript
 ; http://localhost:57772/csp/docbook/DocBook.UI.Page.cls?KEY=GSTU_customize
  Quit    

/// Execute Query and display the results
/// Call like this:
/// zsql "SELECT TOP 10 Name FROM Sample.Person"
ZSQL(Query)
    #Dim ResultSet As %SQL.StatementResult
    Set ResultSet = ##class(%SQL.Statement).%ExecDirect(, Query)
    Do ResultSet.%Display()
    Quit

/// Move user to a namespace of his choice
/// Call like this:
/// zm "s"
ZM(Namespace)
    Do MoveToNamespace^%ZSTART(Namespace)
    Quit
    
/// Move to Samples namespace and set a as a Samples.Person object
/// Set b as %ZEN.proxyObject
/// Set c as %Object
ZO(move = 1) Public
    ZN:move "SAMPLES"
    If ##class(%Dictionary.CompiledClass).%ExistsId("Sample.Person") {
        Set p = ##class(Sample.Person).%OpenId(1)
    }
    Set a = ##class(%ZEN.proxyObject).%New()
    Set b = {}
    Quit

and related %ZSTART:

%ZSTART() {
    Quit    
}

/// This entry point is invoked on user login
/// offering user to choose a namespace
LOGIN() Public {
    Set Timeout = 3
    Write "Namespace <" _ $Namespace _ ">: "  
    Read Namespace:Timeout // Get value of a Namespace variable
    Quit:Namespace=""
    Do MoveToNamespace(Namespace)
}    

/// Does actual moving to a chosen namespace
/// This is an entry point, for cases where
/// Namespace value is already aquired
MoveToNamespace(Namespace = "") Public {
    Set Timeout = 3
    #Dim List As %ListOfDataTypes
    Set List = $$GetNamespaceList(Namespace)
    Set Count = List.Count()
    
    If Count = 1 {
        Set Choice = 1
    } ElseIf Count > 1 {
        Do DisplayList(List)

        // If there is less then 10 results, then we need only 1 digit
        // Otherwise we need 2 digits
        // It is assumed that no more then 99 results would be returned
        Read "Select number <1>: ", Choice#$Select(Count < 10:1, 1:2):Timeout

        // If the user entered nothing or not a valid number
        // we select first namespace in a list to go to
        Set:((Choice = "") || ('$IsValidNum(Choice, 0, 1, Count))) Choice = 1
    } Else {
        // No namespaces found
        Quit
    }
    
    Zn List.GetAt(Choice)
}

/// Get all availible namespaces that satisfy
/// "Name %STARTSWITH Namespace" condition
/// as %ListOfDataTypes
GetNamespaceList(Namespace = "") {
    New $Namespace
    Set $Namespace = "%SYS"

    #Dim List As %ListOfDataTypes
    #Dim ResultSet As %SQL.StatementResult
    Set List = ##class(%ListOfDataTypes).%New()

    Set UserCondition = "%UPPER(Name) %STARTSWITH %UPPER(?)"  // Or [ if you wish
    Set Condition="(" _ UserCondition _ ") AND (SectionHeader='Namespaces') AND (%UPPER(Name)!='%ALL')"
    Set SQL = "SELECT Name FROM Config.Namespaces WHERE " _ Condition

    Set ResultSet = ##class(%SQL.Statement).%ExecDirect(, SQL, Namespace)
    While ResultSet.%Next() {
        Do List.Insert(ResultSet.%Get("Name"))
    }

    Quit List
}

/// Display %ListOfDataTypes in a format:
/// 1    item
/// 2    item
/// ...
DisplayList(List) {
    #Dim List As %ListOfDataTypes
    Write !
    For i = 1:1:List.Count() {
        Write i, $C(9), List.GetAt(i), !
    }
}

Github repo.

Yes, such language extension is very useful, but there is one more feature less known, and unfortunately completely undocumented. Its structured system variables, some of them could me familiar it is: ^$JOB,  ^$LOCK, ^$GLOBAL and ^$ROUTINE. More information in documentation here. But not all knows that it is possible to make your own such global. You should create routine with name SSVN"global_name" in %SYS namespace, for example SSVNZA for ^$ZA global

and for example with this code below, you SET, GET, KILL any values in this global, and this global will contain individual data for every session, and it is possible to use $order or even $query for  such global

ROUTINE SSVNZA
#define global $na(^CacheTemp.ZA($select($isobject($get(%session)):%session.SessionId,1:$job)))

fullName(glob) {
    set global=$$$global
    for i=4:1:glob set global=$na(@global@(glob(i)))
    quit global
}
set(glob, value) public {
    set global=$$fullName(.glob)
    set @global=value
}
get(glob) public {
    set global=$$fullName(.glob)
    quit @global
}
kill(glob, zkill=0) public {
    set global=$$fullName(.glob)
    if zkill {
        zkill @global
    } else {
        kill @global
    }
}
order(glob, dir) public {
    set global=$$fullName(.glob)
    quit $order(@global, dir) 
}
query(glob, dir) public {
    set global=$$fullName(.glob)
    set result=$query(@global, dir)
    quit:result="" ""
    set newGlobal=$name(^$ZA)
    for i=$ql($$$global)+1:1:$ql(result) {
        set newGlobal=$name(@newGlobal@($qs(result,i)))
    }
    quit newGlobal 
}

But as I said before, as it is completely undocumented feature a bit difficult to get complete compatibility with ordinary globals. But it still could be useful, in other cases, like it used by InterSystems.

Hi.

I have made a correction to the post, and associated sample code, to indicate the correct way that long-handed versions of custom commands, functions and variables need to developed.  Code that is implemented as a function with arguments needs to explicitly invoke the short-hand logic, or the functionality will not get invoked when using the long-handed command.

Thanks

Steve

enlightened The article is considered as InterSystems Data Platform Best Practice.