Sage
Tusk
Welcome to Sage
Volume (4%) Hide Volume
Topics
Extensions to Delphi

While Tusk offers a high degree of compatibility with Delphi, it also includes a number of (optional) extensions and enhancements. This article discusses the most important of these, summarizing them in boxes like this…

Tusk Feature

Tusk is highly compatible with Delphi, but offers a number of optional enhancements.


Ternary Operator

Tusk offers the ternary operator popular in C, C++, Java, JavaScript, C#, etc…

Prompt := Qty > 100 ? 'Large order - thanks!' : 'Small order';

Tusk Feature

Tusk supports the ternary (.. ? .. : ..) operator.


One-Line Routines

Tusk offers slightly more concise notation for defining short functions and procedures…

// Traditional approach function Sum(x, y: Integer): Integer; begin Result := x + y; end; // More concise alternative function Sum(x, y: Integer): Integer = x + y; // Traditional approach procedure Display(a, b: Integer); begin Writeln(a, ' + ', b, ' = ', Sum(a, b)); end; // More concise alternative procedure Display(a, b: Integer) = Writeln(a, ' + ', b, ' = ', Sum(a, b));

Tusk Feature

In Tusk, a function that does nothing other than returning an expression can be written in a more concise "one-liner" notation.


Inferred Return Type

Like Delphi, Tusk offers type inference when declaring variables…

var a := 123; // implicitly of type Integer var s := 'hello'; // implicitly of type string

Tusk extends this feature to the return type of a function (when using the one-line approach described above)…

// Implicitly returns Integer function Sum(x, y: Integer) = x + y;

Tusk Feature

In Tusk, the return type of a one-liner function can be inferred.


Trailing Commas

Like many languages in the C family, Tusk permits trailing commas and semi-colons…
  • When declaring function/procedure parameters;
  • When specifying arguments to a function/procedure call;
  • When specifying expressions in a case statement;
  • When building a list literal.

For example…

function f( x: Integer = 0; y: Integer = 1; z: Integer = 2; ): Double; begin Writeln( 'X is ', x, '; ', 'Y is ', y, '; ', 'Z is ', z, '; ', ); ProcessList([x, y, z,]); end;

Tusk Feature

In Tusk, trailing commas and semi-colons are allowed.


First-Class Types

In Tusk, the IType data type represents a type, somewhat like Delphi's PTypeInfo. In Tusk, however, IType is more flexible and powerful. For example…

type INbrList = IList<Integer>; const Nbr = ValueType(INbrList); // Nbr is Integer var z: Nbr := 223; // z is an Integer

Tusk Feature

In Tusk, types may be manipulated as first-class values, using the IType interface.


Indexed For..In Loops

Tusk supports Delphi's for loops, including a new feature for the forin loop. Instead of this…

var i := 0; for var v in List do begin Writeln('List[', i, '] = ', v); Inc(i); end;

You can do this…

for var v at var i in List do Writeln('List[', i, '] = ', v);

The index variable, which is introduced with the at keyword, is zero for the first iteration, one for the next, and so on. This variable can be local to the loop, as shown above, or can be an existing variable…

var i: Integer; for var v at i in List do Writeln('List[', i, '] = ', v);

When the variable is local to the loop, you can specify the data type, but it defaults to NativeInt

for var v at var i: Int64 in List do Writeln('List[', i, '] = ', v);

Tusk Feature

In Tusk, the forin loop can include a loop index variable, introduced with the at keyword.


Strict Case Statements

Tusk offers a case statement that is similar to Delphi's, with one notable improvement. Instead of this…

var Status: TYesNo; case Status of ynUnknown: Writeln('Uncertain'); ynNo: Writeln('No'); ynYes: Writeln('Yes'); else InternalError; end;

You can do this…

var Status: TYesNo; case:strict Status of ynUnknown: Writeln('Uncertain'); ynNo: Writeln('No'); ynYes: Writeln('Yes'); end;

Tusk Feature

In Tusk, it is a parse-time error if a strict case statement does not have every choice from the underlying data type.


Anonymous Methods

In Delphi, an explicit cast is required to convert an anonymous method to Variant or to IInterface. We typically use TAnonMeth.ToVar and TAnonMeth.ToIntf, respectively.

Tusk Feature

In Tusk, anonymous methods can be implicitly converted to Variant as well as IInterface.


Object Type

Tusk offers the built-in type object, which is like Variant, except for the following: when converting a function expression to object, Tusk will not attempt to add empty parentheses. For example…

function f: string = 'hello'; var v: Variant := f; // v is 'hello' (implicit function call) var o: object := f; // o is the function f (no implicit call) Show(v); // prints: 'hello' Show(o); // prints: function f: string = 'hello' Writeln(v); // prints: hello Writeln(o); // raises an exception

Above, the last call to Writeln raises an exception because there is no implicit conversion from interface to string (in Tusk, functions are interfaces).

Narrowing Conversions

Delphi allows narrowing conversions implicitly, for example it will happily pass an Int64 value to a function that requires a Byte.

Tusk Feature

In Tusk, narrowing conversions require an explicit cast, as opposed to Delphi, where narrowing conversions happen implicitly.


Out Parameters

In Delphi, out parameters pass the argument by reference, like var parameters. The difference is that, for managed types, out parameters are cleared out immediately before the called function executes. For other data types, out and var behave identically.

Tusk Feature

In Tusk, out parameters are consistent: they are always initialized at the point of call, regardless of data type.


Function Compatibility

Delphi has a very strict system for function compatibility: function types must match exactly. Tusk offers a more flexible system, with four main advantages over Delphi.

Functions as Procedures

Consider this Delphi code…

type TStrProc = procedure(const Value: string); procedure StrProc(const Value: string); begin Writeln('<<', Value, '>>'); end; function StrFunc(const Value: string): Boolean; begin Writeln('<<', Value, '>>'); Result := Value <> ''; end; var sp: TStringProc; begin sp := StrProc; // This is fine; sp := StrFunc; // This is a compile error. end;

Above, Delphi will not allow StrFunc to be used as a TStrProc, even though, logically, it could (by simply discarding the result).

The above code works fine in Tusk, because…

Tusk Feature

In Tusk, a function converts to a procedure type if the parameters are compatible (discarding the function result).


Covariance on Return Type

Consider this Delphi code…

type TDoubleFunc = function: Double; function DblFunc: Double; begin Result := 123.45; end; function IntFunc: Integer; begin Result := 12345; end; var df: TDoubleFunc; begin df := DblFunc; // This is fine; df := IntFunc; // This is a compile error. end;

Above, Delphi will not allow IntFunc to be used as a TDoubleFunc, even though, logically, any function that returns an Integer should work as a function returning Double.

The above code works fine in Tusk, because…

Tusk Feature

In Tusk, a function returning T is compatible with a function returning Q if T is compatible with Q.


Contravariance on Parameter Types

Consider this Delphi code…

type TIntegerProc = procedure(Value: Integer); procedure IntProc(Value: Integer); begin Writeln('<<', Value, '>>'); end; procedure DblProc(Value: Double); begin Writeln('<<', Value, '>>'); end; var ip: TIntegerProc; begin ip := IntProc; // This is fine; ip := DblProc; // This is a compile error. end;

Above, Delphi will not allow DblProc to be used as a TIntProc, even though, logically, it could (by simply promoting the Integer to Double).

The above code works fine in Tusk, because…

Tusk Feature

In Tusk, a routine taking a parameter of type T (by value or const) is compatible with a routine taking a parameter of type Q (by value or const) if Q is compatible with T.


Note

The above feature does not apply to var or out parameters, which must match exactly when comparing routine types.


Const Parameters

In Tusk, a const parameter is passed by value, but cannot be changed within the function itself. Whether a parameter is passed by value or by const affects the called function, but not the caller. Therefore, the following code works fine in Tusk…

type TIntProc = procedure(x: Integer); procedure p1(y: Integer); begin end; procedure p2(const z: Integer); begin end; var ip: TIntProc; ip := p1; ip(3); ip := p2; // In Delphi, this would not compile ip(4);

The above code works fine in Tusk, because…

Tusk Feature

In Tusk, value and const parameters are equivalent, for purposes of type compatibility.


Optional Params in Data Types

In Tusk, the type of a function includes information about which parameters are optional. For example, to define the variable p as a procedure that takes one string and an optional integer…

var p: procedure(const s: string; i: Integer = ?);

Note that the data type does not indicate the value of default parameters — just that they are optional. Thus, p (once assigned) can be called in two ways…

p('hello'); p('goodbye', 22);

To assign a value to p, we could do something like this…

p := procedure(const: string; i: Integer = 9); begin Writeln(s, ': ', i); end;

In Delphi, a procedural type can specify optional parameters, but the default value is baked into the type.

Tusk Feature

In Tusk, parameter default values are not baked into the type, rather, they belong to the routine itself.


Default Parameters

Using Earlier Parameters

In Tusk, default parameters can be expressed using previous parameters…

procedure p(x: Integer; y: Integer = 2*x; z: Double = Sqrt(y)); begin Writeln(x, ' ', y, ' ' , z); end;

In Delphi, the above would have to be an overloaded trio…

procedure p(x, y: Integer; z: Double); overload; begin Writeln(x, ' ', y, ' ' , z); end; procedure p(x, y: Integer); overload; begin p(x, y, Sqrt(y)); end; procedure p(x: Integer); overload; begin p(x, 2*x); end;

Tusk Feature

In Tusk, later parameter default expressions may refer to earlier parameter values.


Data Types

In Tusk, there are no restrictions on what type of parameters can have default values. In Delphi, only certain data types can have defaults — for example, Integer and string are supported, but not Variant, objects, static arrays, or records.

Tusk Feature

In Tusk, parameters may have a default value, regardless of data type.


Late Evaluation

In Delphi, default parameter values must be compile-time constants. In Tusk, default parameter values may be any expression (evaluated at the time of call, but only if the argument was omitted). For example…

var x: Integer := 123; procedure Work(z: Integer = x); begin Writeln(z); end; Work(3); Work; x := 999; Work;

The above program prints…

3 123 999

Tusk Feature

In Tusk, default parameter expressions are evaluated for each call of the routine.


Frozen Functions

In Tusk, functions may be frozen, meaning that they exhibit no side effects, and always return the same value for a given set of inputs. Examples of such functions include Pos and Sqrt. Frozen functions can be used to make constants known at parse time. For example…

type MyInt = Integer; // type declarations are always frozen const k = Sqrt(16); // k is frozen, because Sqrt and 16 are frozen var j: Integer; // variables are not frozen case j of 2: Writeln('two'); k: Writeln('four'); // k is allowed, because it's frozen end;

Some functions are always frozen, and others are frozen only if all their arguments are frozen. Others, of course, are never frozen (even if their arguments are), including Inc and Dec (due to side effects) and Now, which can return a different value with each call.

Tusk Feature

In Tusk, functions may be frozen, allowing them to be called at parse time (for example, Sqrt, Abs, and GetComputerName).


Function Results

Delphi has a few less-than-ideal behaviors related to function results.

Return Value Optimization

For managed types, Result may be bound to a local variable in the caller's scope. This can lead to a couple of odd issues: Result may have a non-zero initial value, and it may apply changes to the caller's variable before the function completes.

Tusk Feature

In Tusk, Result is never aliased to any other value.


Intial Value

For non-managed types, Result has no defined value. Typically, the compiler warns if you use Result before giving it a value, but this is not reliable. For example…

procedure Work(var z: Integer); begin end; function Compute: Integer; begin Work(Result); Writeln(Result); Result := 0; end;

Above, Compute writes a random number, but the code is warning-free.

Tusk addresses this issue in a simple manner:

Tusk Feature

Result is always zero-initialized.


Undefined Result

Delphi typically warns if a function doesn't assign a value to Result (but only for non-managed types, and never for records).

Tusk addresses this issue in a simple manner:

Tusk Feature

In Tusk, Result is always zero-initialized.


Properties

Non-Array Properties

In Tusk, a property (of an interface) can be used anywhere a value of that type would be expected. This includes taking the address of a property or passing a property by reference (to a var parameter).

For example…

// IEmployee has a read/write Name property var Emp: IEmployee; Emp.Name := 'Joe Cool'; Writeln(Emp.Name); var p: ^string := @Emp.Name; p^ := 'Jane Smith'; Writeln(p^); procedure ChangeStr(var s: string); begin s := 'Wilma Flitstone'; end; ChangeStr(Emp.Name);

The above works fine in Tusk, even though most of it wouldn't work in Delphi, because…

Tusk Feature

Tusk supports taking the address of a property, and passing properties by reference.


Array Properties

For array properties, the above discussion applies to individual elements of the array property…

var List: IVariantList := [4,5,6]; Writeln(List.Items[1]); Writeln(List[2]); var p: ^Variant := @List.Items[0]; p^ := 888; Writeln(p^); procedure ChangeVar(var v: Variant); begin v := 333; end; ChangeVar(List[1]);

Tusk Feature

Tusk supports taking the address of an element of an array property, and passing an element of an array property by reference.


Pointer Math

For certain array properties, Tusk supports pointer math…

var List: IVariantList := [4,5,6]; var p: ^Variant := @List.Items[0]; while InBounds(p) do begin Writeln(p^); Inc(p); end;

To enable pointer math on an array property, use the [TuskArray] attribute. For example, here's how IReadOnlyVector is exposed (in the unit Tusk_GenUtil)…

Tusk_IReadOnlyVector = class public Obj: IReadOnlyVector; function get_Items(i: Integer): Variant; [TuskArray('Count')] property Items[i: Integer]: Variant read get_Items; default; function get_First: Variant; function get_Last: Variant; property First: Variant read get_First; property Last: Variant read get_Last; end;

The [TuskArray] attribute is only supported on indexed properties with a single index parameter, which must be an integer type.

The [TuskArray] attribute takes an argument, as shown above, a string specifying the name of the property that indicates the element count for the array property. This argument defaults to "Count", so the above is actually written as follows…

[TuskArray] property Items[i: Integer]: Variant read get_Items; default;

The count property (whatever it is called) must also be an integer type, and may (as in this example) be defined in the class that exposes an ancestor type (Tusk_IReadOnlyVector, in this case).

Tusk Feature

Tusk supports pointer math on array properties, such as the Items property of IReadOnlyVector.


Associations

Variants

In Tusk, expressions of type Variant support the dot operator (requiring the value to support IAssociation)…

var v: Variant := Something; Writeln(v.x); // TType.VarAs<IAssociation>(v)['x']

Similarly, Variant expressions support the square bracket operator for IAssociation and IReadOnlyVector objects…

var v: Variant := SomeAssociation; Writeln(v['y']); // TType.VarAs<IAssociation>(v)['y'] var w: Variant := SomeList; Writeln(w[2]); // TType.VarAs<IReadOnlyVector>(w)[2]

Tusk Feature

Tusk allows the use of square brackets on a Variant that happens to hold an IReadOnlyVector or IAssociation.


IDotBag

Tusk offers a built-in type, IDotBag, which is assignment-compatible with IAssociation. IDotBag does not expose any properties or methods of IAssociation or its ancestor interfaces. Instead, IDotBag allows you to access the default array property of IAssociation using the dot or square bracket operators…

var s := 'z'; var a: IAssociation := Something; Writeln(a['x'] + a['y'] + a[s]); var b: IDotBag := a; Writeln(b.x + b.y + b[s]);

In this way, IDotBag is similar to Variant, but it is more restrictive because IDotBag is still an interface, so (unlike Variant) it cannot be used with arithmetic operators, doesn't implicitly convert to other types, etc.

Tusk Feature

Tusk includes the IDotBag data type, which is assignment compatible with IAssociation, but offers a cleaner notation for accessing named properties.


Last Modified: 5/29 7:21:30 pm
In this article (top)  View article's Sage markup
5/29 7:21:30 pm