[fpc-announce] Feature announcement: implicit generic function specializations

Sven Barth pascaldragon at googlemail.com
Wed Apr 20 19:15:15 CEST 2022


Dear FPC community,

The FPC developers are pleased to announce the implementation of a new 
feature: implicit generic function specializations. This feature was 
implemented by Ryan Joseph, so thank you very much, Ryan.

This feature allows you to use generic routines (functions, procedures, 
methods) without explicitely specializing them (“<…>” in Delphi modes 
and “specialize …<…>” in non-Delphi modes) as long as the compiler can 
determine the correct parameter types for the generic.

This feature is enabled with the modeswitch 
ImplicitFunctionSpecialization and is for now not enabled by default as 
this has the potential to break existing code.

Assume you have the following function:

=== code begin ===

generic function Add<T>(aArg1, aArg2: T): T;
begin
   Result := aArg1 + aArg2;
end;

=== code end ===

Up to now you could only use this function as follows:

=== code begin ===

SomeStr := specialize Add<String>('Hello', 'World');
SomeInt := specialize Add<LongInt>(2, 5);

=== code end ===

However with implicit function specializations enabled you can also use 
it as follows:

=== code begin ===

SomeStr := Add('Hello', 'World');
SomeInt := Add(2, 5);

=== code end ===

The compiler will automatically determine the type of the generic 
parameters based on the parameters you pass in (this is always done left 
to right). Depending on the passed in parameters (especially if you're 
using constant values like in the example instead of variables) the 
compiler might however pick a different type than you expected. You can 
enforce a specific type by either explicitely specializing the method as 
before or by inserting a type cast. In the example above the compile 
will specialize the call with the parameters “2, 5” using an 8-bit 
signed type (Pascal prefers signed types) instead of a LongInt as in the 
explicit specialization. If you use “LongInt(2), 5” as parameters then 
the compiler will pick that instead, however with “2, LongInt(5)” it 
will still pick an 8-bit type, because the parameter types are 
determined left to right.

If there exists a non-generic overload for which the parameters types 
match exactly, the compiler will pick that instead of specializing 
something anew. So assume you also have the following function in scope:

=== code begin ===

function Add(aArg1, aArg2: LongInt): LongInt;
begin
   Result := aArg1 + aArg2;
end;

=== code end ===

In the case of “Add(2, 5)” the compiler will *not* pick the non-generic 
function, because it determines that an 8-bit type is enough, however if 
you use “Add(LongInt(2), 5)” the compiler will pick the non-generic 
function.

Aside from simple parameters the compiler also supports arrays and 
function/method variables:

=== code begin ===

generic function ArrayFunc<T>(aArg: specialize TArray<T>): T;
var
   e: T;
begin
   Result := Default(T);
   for e in aArg do
     Result := Result + e;
end;

type
   generic TTest<T> = function(aArg: T): T;

generic function Apply<T>(aFunc: specialize TTest<T>; aArg: T): T;
begin
   Result := aFunc(aArg);
end;

function StrFunc(aArg: String): String;
begin
   Result := UpCase(aArg);
end;

function NegFunc(aArg: LongInt): LongInt;
begin
   Result := - aArg;
end;

begin
   Writeln(ArrayFunc([1, 2, 3])); // will write 6
   Writeln(ArrayFunc(['Hello', 'FPC', 'World'])); // will write 
HelloFPCWorld

   Writeln(Apply(@StrFunc, 'Foobar')); // will write FOOBAR
   Writeln(Apply(@NegFunc, 42)); // will write -42
end.

=== code end ===

There are of course a few restrictions for this feature:
- all generic parameters must be used in the declaration of the routine 
(implementation only type parameters are not allowed)
- all parameters that have a generic type must not be default 
parameters, they need to be used in the call or their type must have 
been fixed by a parameter further left (as currently default values for 
parameters of a generic type are not supported this is not much of a 
restriction, but should that change (e.g. Default(T)) then this 
restriction will apply)
- the generic routine must not have constant generic parameters (this 
might be extended in the future with e.g. static arrays or file types, 
but for now this restriction stands)
- the result type is not taken into account, so if only the result type 
of a routine is generic then an implicit specialization does not work either
- function/method pointers to implicit specializations are not yet 
supported (pointers to explicit specializations are not yet supported 
either; once this changes the former will change as well)
- the compiler will silently discard generic functions that it can't 
specialize the *declaration* of; however if the declaration can be 
specialized correctly, but for whatever reason the *implementation* can 
not then this will trigger a compilation error

This feature is by and in itself Delphi compatible however there might 
be differences in what FPC can implicitely specialize and what Delphi 
can. Especially if Delphi can specialize something that FPC can not, 
this should be reported.

With kind regards,
Sven/Sarah Barth


More information about the fpc-announce mailing list