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 offers the ternary operator popular in C, C++, Java,
JavaScript, C#, etc…
Prompt := Qty > 100 ? 'Large order - thanks!' : 'Small order';
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));
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;
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.
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 supports Delphi's for loops,
including a new feature for the for‥in 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 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;
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 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).
Delphi allows narrowing conversions implicitly,
for example it will happily pass an Int64 value
to a function that requires a Byte.
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.
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.
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…
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…
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…
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…
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.
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;
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.
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
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.
Delphi has a few less-than-ideal behaviors
related to function results.
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.
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:
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…
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]);
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).
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 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.
⏱ Last Modified: 5/29 7:21:30 pm