Modules, Functions and Services

Module Structure

Inq applications comprise a number of parsing units, or modules, each of which is reached by a URL. An Inq module has the following high-level structure:

  <module> =
  [ package <package-specification>; ]
  [ import <package> as <identifier> ; ]

  ( ( [<function-definition>]
    | [<service-definition>]
    | [<typedef-definition>]
    | [<expression> ...]
    | #include <url>
    )
   ...
  )

  <package-specification> = <identifier>[.<identifier> ...];

  <function-definition> =
  [local] function <identifier> ( [<argument-declaration>[, <argument-declaration> ...] )
    <statement>

  <service-definition> =
  [syncgui] service <identifier> ( [<argument-declaration>[, <argument-declaration> ...] )
    <statement>

  <typedef-definition> =
  typedef { <typedef body> }

Referencing Defined Entities

A function, service or typedef is referenced by its name, optionally qualified by a package specification. For example, a parse module that resides in package bar may contain the following function references:

call funcA();     // Calls funcA defined in package "bar"
  .
  .
call foo:funcB()  // Calls funcB defined in package "foo"

Inq Expressions and Scope

Every executable statement in Inq returns a value, even such things as variable declarations and block statements. Constructs such as the following are legal:

string someCcy = "GBP";
any var = {
            if (someCcy == "GBP")
              100;
            else
              1;
          };

The value of a block statement (contained within { ... } braces) is the last expression that was actually executed and this trivial example always returns 100. Inq uses the term expression to mean any executable element, including all its built-in functions.

The term "scope" when applied to a node path either means the current stack frame or one of the permanent node spaces of

$root the root of this process's node space
$this the current context node (a descendent of $root)
$process the running process (used by Inq but can also be used by script)
$catalog a global node space (used by Inq but can also be used by script)

Local variables are those passed to services and functions as arguments or declared on the stack. Local declarations are placed on the stack and remain there regardless of changes in the block nesting level:

>inq -show
string s = "outside";
outside
// enter nested block
{
  string s = "inside";
  int i = 3;
}
3
// The original s was replaced and i is still visible
s;
inside
i;
3
^Z
Inq done

The node space at $this represents the next level of visibility. Nodes below $this remain indefinitely but the exact node that $this is may be different on each service invocation. Put another way, an application may set up a number of sub-structures, each representing a separate section of the application. The node $this is therefore the definition of the current working set of long lived data (whether similar or different to other sub-structures). This is not so much to do with module (i.e. source code) structure as it is run-time structures, however Inq is a run-time language, so it is worth stating here.

The node space at $catalog is global to all processes in a server and available in the client. It is typically used to hold global, read-only data such as an internationalised set of string constants.

Service and Function Definitions

We have seen in earlier examples that Inq executes expressions as it parses them. A service or function definition declares arguments and parses the function body so that script can be pre-parsed into the Inq run-time. A service can only be invoked by one Inq process on another. This may be between:

  • a Client process and its peer User process;
  • a User process and its peer Client process;
  • any two server-side processes.

A function can be called only within an Inq process, or run as a handler for a dispatched event.

Function Definitions

A function definition takes the form

[local] function <identifier> ([<argument-declaration>[, <argument-declaration> ...])
  <statement>

<argument-declaration> = ( <data-type> <identifier> [ = <expression> ]
                         | any <identifier>
                         | <type-reference> [<identifier>]
                         )

<type-reference> = [<package specification>:]<identifier>[.<identifier>]

Function Arguments

Function arguments can either be of known data types or based on a reference to an application type definition. The use of a type reference protects the function definition from the direct use of data types. We look at this in detail in the section on the typedef construct.

Inq function arguments are named. When a call statement passes values to a function it identifies the argument by its name. The order the arguments appear in the call statement is not significant. The example function.inq illustrates this. Note, we drop the -show argument:

>inq -in examples/function.inq
William Blake wrote On Another's Sorrow
Carl Barât and Peter Doherty wrote Seven Deadly Sins
William Blake wrote Jerusalem
Inq done

It is common for applications to define an informal naming convention for their variables leading to their repeated use in source code. If a stack variable has the same name as a function argument then the argument's name can be omitted in call statements:

string work = "Thick as Thieves";
string writer = "Paul Weller";
call whoWroteWhat(work, writer);

The Call Stack

A new stack frame is established each time a function is called and any arguments or defaults initialised there. The previous stack frame cannot be accessed within the called function although it can, if desired, be passed as an explicit argument.

If a function incurs an error while executing, the resulting exception carries the Inq stack trace. Each entry identifies the URL from which the source was originally parsed, the function name and line number at which the exception occurred. When running the parser interactively the bottom-most stack frame shows the string <parser>. An example is shown in the next section.

A Function's Return Value

Inq functions always return a value, so this is not part of the definition. The return value of a function is the value of the last expression within the function body actually executed. Here is an example in returnvalue.inq that illustrates this:

>inq -in examples/returnvalue.inq
There are 30 days in January
February (most often) has 28
file:/C:/inqwell/doc/src/documentation/content/xdocs/primer/examples/returnvalue.inq
com.inqwell.any.AnyException: Bad month number 13
        at com.inqwell.any.Throw.exec(Throw.java:92)
        at com.inqwell.any.EvalExpr.evalFunc(EvalExpr.java:109)
        at com.inqwell.any.EvalExpr.evalFunc(EvalExpr.java:140)
        at com.inqwell.any.Choose.exec(Choose.java:68)
        at com.inqwell.any.EvalExpr.evalFunc(EvalExpr.java:109)
        at com.inqwell.any.EvalExpr.evalFunc(EvalExpr.java:140)
        at com.inqwell.any.Sequence.exec(Sequence.java:46)
        at com.inqwell.any.Exec.exec(Exec.java:32)
        at com.inqwell.any.Call.call(Call.java:92)
        at com.inqwell.any.Call.exec(Call.java:179)
        at com.inqwell.any.OperatorVisitor$RankVisitor.visitFunc(OperatorVisitor.java:380)
        at com.inqwell.any.AbstractFunc.accept(AbstractFunc.java:82)
        at com.inqwell.any.OperatorVisitor.rank(OperatorVisitor.java:104)
        at com.inqwell.any.OperatorVisitor.samePrecision(OperatorVisitor.java:153)
        at com.inqwell.any.OperatorVisitor.doOperation(OperatorVisitor.java:39)
        at com.inqwell.any.EvalExpr.exec(EvalExpr.java:215)
        at com.inqwell.any.EvalExpr.evalFunc(EvalExpr.java:109)
        at com.inqwell.any.EvalExpr.evalFunc(EvalExpr.java:140)
        at com.inqwell.any.WriteStream.exec(WriteStream.java:50)
        at com.inqwell.any.EvalExpr.evalFunc(EvalExpr.java:109)
        at com.inqwell.any.EvalExpr.evalFunc(EvalExpr.java:140)
        at com.inqwell.any.parser.Inq.main(Inq.java:512)
file:/C:/inqwell/doc/src/documentation/content/xdocs/primer/examples/returnvalue.inq <parser>(22)
file:/C:/inqwell/doc/src/documentation/content/xdocs/primer/examples/returnvalue.inq :daysInMonth(9)

Inq done
Note
We have declared a function and called it from the parser at the point the exception is thrown. We now have two entries on the Inq stack trace, which reads the other way up to the Java one

The Inq language does have a return statement, however the preferred coding style is not to use it for normal code flow. It should be viewed as an abortive and exceptional way to leave a function. The syntax of the return statement is

return([<expression>]);

If no expression is present the return value is null.

We could have written daysInMonth like this:

function daysInMonth(int month)
{
  if (month < 1 || month > 12)
    return("Oh dear");

  switch
  {
    when (contains(set s = (4, 6, 9, 11), month)) 30;
    when (month == 2) 28;
    when (contains(set s = (1, 3, 5, 7, 8, 10, 12), month)) 30;
  }
}

As with previous examples, it doesn't matter that the return type is not the same for all paths of execution (or return expression). A call statement is just another expression and its value (the function's return) can be anything that is acceptable to the context in which it is used.

Function Scope

A function has global or module scope. If a function definition uses the local specifier then that function is only callable from within the same source module. Otherwise the function occupies the global name space.

If there is a local and global function of the same name then this ambiguity can be resolved by using the explicit global qualifier in the call:

call global:foo(bar = a.b.c);

Service Definitions

A service definition takes the same form as a function apart from the service keyword. A service is invoked when a process receives a service request at its input channel, sent by another (possibly remote) process. Commonly, the client process sends service requests to its associated user process in the connected server, however its also possible to invoke services in detached or child processes within the server.

Note
Services and functions do not occupy the same name space, so it is possible to have a global function and a service with the same name

The send Function

The send function is used to make service requests. Here are some examples:

send filterAccounts(filter, listPath);

send calcEntry(@channel=someProcess.ichannel,
               entry,
               invoker,
               fwd,
               nfm,
               accountEntry,
               forceReval);
  

Both these examples use the short-hand form of argument naming and, as with functions, the order in which the arguments appear is not important. By default send posts the service request to the executing process's output channel. In the case of client/server, this means the invocation appears at the user process's input channel.

If the sending process has a reference to the desired target then that process's input channel can be specified by the named argument @channel as in the second example, above.

The syncgui Modifier

In the client, service invocations are received from the server and executed on the client process thread. Occasionally, this can cause problems with the point at which graphics events generated during the invocation are processed.

The process and graphics threads do not run together. While a service invocation is running, any graphics events are queued until the request has completed and the process thread returns to wait at its input channel. On the other hand, graphics events occuring on the graphics thread will be processed at the time they occur.

This will be an issue if the code flow assumes that a GUI event handler is fired synchronously with the occurrance of the event. Script written to rely on this property can be run successfully within an invocation of a service declared with the syncgui modifier. In this case, the service will be dispatched on the graphics thread.

Value and Reference Arguments

If a function argument is declared as either a direct value type or a field reference then argument passing is by value, that is the argument is a new value instance. If the argument is passed by a call statement the new value is initialised from it, performing implicit type conversion if required. Changes the function makes to the argument are not reflected in that specified in the call.

Arguments declared with the anonymous type any are passed by reference. Such arguments are typically maps (or other containers) into which the function will place nodes as another way to return data to the caller. However, any type of argument can be passed by reference using this kind of declaration. Values passed in this way will show any changes made by the function to the caller. Here is an example showing both value and reference arguments:

function f(string byValue, any byReference)
{
  .
  .

You can see the effect this has by running the example in byrefbyvalue.inq

Arguments to a service are always effectively by value, because they have been received in a message from another Inq environment (though see section on event services) and there is no caller in the normal sense.

Inq Built-in Functions

Calling an Inq built-in function does not require the use of the call keyword and, in the majority of cases, arguments are unnamed and resolved by their position. Here is an example showing the datediff function, where d1 and d2 are of value type date:

any dayCount = datediff(DAY, d1, d2);

The datediff function returns a new int variable whose value is the difference between the two dates in the specified units.

Some functions have optional arguments. The startswith function, for example, tests whether the first string argument starts with the second, assuming a starting offset of zero unless stated otherwise:

startswith("hello world", "hello");
true
startswith("hello world", "world", 6);
true

In a minority of cases, functions can accept varied combinations of optional arguments. These functions name their optional arguments and their order is independent. We will look at the read function in detail when we discuss building structures. Here is its syntax:

"read" "(" <type_reference> ","
           <expression>    // the key value
           ( ","
             ( "keyname" "=" <expression>
             | "target"  "=" <expression>
             | "setname" "=" <expression>
             | "rowname" "=" <expression>
             | "alias"   "=" <expression>
             | "merge"   "=" <expression>
             | "max"     "=" <expression>
             )
           )*
       ")"

The read function has two mandatory parameters:

  1. a type reference (either a literal type or the result of the typeof function when an instance is available);
  2. a key value to be applied.

The remainder are all optional with the parser enforcing any mutual exclusion required for valid combinations of operands.

The package and import Directives

The optional package specification at the head of a parse module specifies a name space for the global functions and services (and typedefs) defined within it. References to these entities when not qualified by an explicit package name imply the current package.

An entity's containing package is qualified by preceding the entity name with the package name. For example, the function bar declared within package foo can be called as:

call foo:bar(arg, ...);

To protect package references from changes to the package name, the import directive can be used to create a local alias for a package within the module:

package com.somewhere.mypackage;

import foo as F;
   .
   .
call F:bar();