OPN provides method definitions for the purpose of building abstractions for modeling. The typical expression and statement forms can be used. Some of the important differences to other languages are described in this section.
Method Definitions
Method declarations have a similar syntax as in languages C# or Java. OPN supports ref and out parameters in method declarations. A method that has no body will throw a not-implemented exception if executed at runtime. The following are some examples.
int Succ(int x) { return x+1; }
void Incr(ref int x) { x++; }
void Decr(ref int x); // Will throw not-implemented exception if called
Methods that are declared at module level are implicitly static. Methods declared at reference type level are implicitly instance-based, unless preceded with the static keyword.
Static methods can use the this designation on their first parameter to enable receiver style notations.
int Add(this int x, int v) { return x+v; } // Enables x.Add(v) notation
A property-style notation can be introduced by prefixing a method declaration with the get or set modifier, as in the following example.
type T
{
int x;
int get X() { return x; }
void set X(int v) { x = v; }
}
On instances of type T, the notation t.X can now be used to call the getter method X, and t.X = v to call the setter of X.
Properties can also be defined using the this designator. The previous declaration is therefore equivalent to the following.
type T
{
int x;
}
int get X(this T t) { return t.x; }
void set X(this T t, int v) { t.x = v; }
Operators and conversions can be defined using notation asfollows.
int operator + (int x, int y) { return x + y; }
int operator as (long x) { ... }
Patterns in Methods and Statements
As already mentioned, patterns can be used in all places where types do appear. Also, method declarations can use patterns for parameters and return values.
pattern PosInt = int where value >= 0;
PosInt M(PosInt x) { return x - 1; }
The meaning of the preceding example is identical to the following example.
PosInt M(int x)
{
assert x is PosInt;
int r = x – 1;
assert r is PosInt;
return r;
}
OPN allows omitting types and patterns in local declarations using the var keyword. Type inference is used to infer the types of such declarations, as discussed in section 2.1.8.
int Sum(int x, int y, int z)
{
var t = x + y;
return t + z;
}
The switch statement in OPN is extended to use not only constants but also patterns; those patterns can use capture variables to extract values. In difference to C-like languages, the variables introduced in the scope of a switch case are local to the case. Also, the case is implicitly terminated by a break, a break can be given if desired, but it is redundant.
int M(Frame x)
{
switch (x)
{
case Frame{Length is var l} => return l;
default => throw "Expected a Frame";
}
}
OPN supports exception, as apparent from the previous example. Any value can be thrown as an exception, similar as in C++. A catch clause can use a pattern to match the exception value.
try
{
// ...
}
catch (int x where value > 0) { ... }
Binders
OPN supports the foreach statement. The foreach statement uses a general concept of a binder (by the use of the in keyword) that is shared between other constructs. In the simplest form, a binder looks, together with foreach, as known from languages such as C# and Java.
var s = [1,2,3,4];
var r = 0;
foreach (int x in s) r += x;
assert r == 10;
Instead of using a type when declaring the bound variable, the binder can also use patterns designated by the where expression. In that case only those values will be iterated that match the pattern.
foreach (int x where x % 2 == 0 in s) r += x;
assert r == 6;
As in C#, the type of the bound variable can be inferred.
foreach (var x where x % 2 == 0 in s) r += x;
assert r == 6;
Indeed, one can also use a named pattern.
pattern EvenInt = int where value % 2 == 0;
foreach (EventInt x in s) r += x;
assert r == 6;
The foreach binder exprssion can iterate over more than one variable, and variables can be introduced by ranging over a collection or by assigning a value, as in the following example.
var s = [1,2,3];
var t = [2,3,5];
var r = 0;
foreach (var x in s, var y = x + 1, var z where value == y in t) r += x;
assert r == 3;
As in other modeling languages, the concept of a binder can be used in a few other constructs, where it has the same meaning as the foreach-in expression that regards the range of values being iterated. The difference is in what is done with the iterated values. First, universal and existential Boolean quantifier expressions are available as those have very common usage in expressing constraints over collections.
var s = [1,2,3];
assert all (var x in s) x > 0;
assert some (var x in s) x > 0;
Finally, there is a simplified form of LINQ selection.
var s = [1,2,3,4]
var t = from (var x where value % 2 == 0 in s) x+1;
assert t == [3,5];
The behavior for from is similar to foreach when iterating over more than one variable:
var s = [1,2];
var t = [3,4];
var r = from (var x in s, var y int t, x + y != 6) new Tuple(x,y);
assert r == [(1,3),(1,4),(2,3)];
The domain of OPN is not in querying data sets, therefore full LINQ functionality is not directly supported. However, with anonymous functions that are not explicitly named (such as lambda functions) being part of the notation and library functions makes the functionality indirectly available.
In order to enable iteration over collections in binders, the value does not need to implement a particular interface, but just needs to support methods matching the following signature.
S GetIterator(this T x);
V GetNext(this T x, S s);
bool MoveNext(this T x, ref S s);
Here, the type S is arbitrary, representing the state of the iterator. All collection types in the standard library support this pattern. For example, the array type uses the following methods.
int GetIterator(this array x) { return -1; }
T GetCurrent(this array x, int s) { return x[s]; }
bool MoveNext(this array x, ref int s)
{
if (s < x.Length-1) { s++; return true; } else return false;
}
Share with your friends: |