Polymorphism
Rules to configure and adjust program behaviour

See function specializations for syntax.

Polymorphism is an umbrella term to denote a number of techniques across different programing paradigms. They all share the same intention to provide the ability to easily recombine software components in a different way with as little manual work on developer's side as possible. It serves two major goals: specialization, which purports that software initially designed to support a wide range of use cases is being configured for a specific case, and extension – adapting software to an environment and conditions it was not specifically designed for.

In course of software engineering evolution, a number of polymorphism techniques was proposed and experimented with, all suited for different use-cases. Xreate presents a generalized and elegant approach that exhaustively covers the wide landscape of polymorphism variations.

Polymorphism in Xreate can be applied on two levels:

  • Functions level. Function in Xreate can have multiple specializations, and polymorphism is compiler's ability to decide which exactly specialization to use depending on various factors
  • Modules level. Multiple modules can provide the same service for users. Modules Resolution is a process to decide which exactly module to use.

Function Level Polymorphism

Basic idea is to allow developer to define several functions with the same name or, in other words, several specializations. Caller code then invokes the necessary function by its shared name, but cannot specify a particular specialization directly. The exact specialization to be invoked is decided later by a decision process called polymorphism resolution carried out by Transcend. This indirect invocation approach gives enough flexibility to use or replace different specializations depending on various conditions during compile time as well as at runtime.

Each specialization must have unique guard (among all specializations with the same name) to be discernible from others. To summarize, function invocation is a two-layered process, where the client code specifies the callee function's shared name, while polymorphism resolution specifies specialization guard, if needed.

For example, let us assume that we have been developing a software program to operate under specified time constraints. To model an implementation suitable for real time environment, one specialization of crucialOperation is defined with env(realtime) guard, i.e. it satisfies some fixed execution time constraints. Caller main specifies just a function name crucialOperation thus delegating the decision in respect of guard to polymorphism resolution done elsewhere, based on the environment's constraints the code is executed in.

tests/polymorph.cpp: Polymorphs.Doc_FnLvlPoly_1
guard::                         env(realtime)
{
  crucialOperation = function:: int 
    { 0 }
}

main = function::               int; entry 
{
  crucialOperation()
}

Polymorphism Resolution

SYNTAX: dfa_callguard(call-site-ref, guard)
  • call-site-ref reference to a call site in AST
  • guard resolved function specialization guard

When compiler encounters function invocation that has several specializations it refers to the table dfa_callguard to find out which specialization to call. It must have an entry with appropriate guard for every invocation site call-site-ref of a polymorphic function. Polymorphism resolution is a process of filling out dfa_callguard for the compiler based on custom Transcend rules reflecting one or another polymorphism strategy.

Late Polymorphism

Late Polymorphism is an extension to allow polymorphism resolution to be based on data known only at runtime, i.e. resolve function specializations dynamically. The Idea is to use Late Transcend to access runtime data. See Late Transcend for details.

Example below demonstrates test invoking polymorphic function compute:

tests/polymorph.cpp: Polymorphs.Doc_LatePoly_1
Strategy = type variant {fast, precise}.

guard::                   fast 
{
  compute = function::    int
    {0}
}

guard::                   precise 
{
  compute = function::    int
    {1}
}

test = function(s::  Strategy; alias(strategy))::   int; entry
{
  switch late (s)::       int
  {
    compute()::           int; guardalias(strategy)
  }
}

Function compute has two specializations, fast and precise. We see that test gets parameter s that dictates exact strategy to use. Clearly, resolution should work dynamically to deal with cases like this, for not only the value of the parameter s is unknown at the compile time, but also it can change with each test execution.

Operation switch late is compiled into several branches, two in this case, each branch executing appropriate compute specialization. The correct branch is executed depending on the current s value. Custom annotations alias(Alias) and guardalias(Alias) are used to assign an alias in order to specify which parameter to use as basis for resolution.

Auto Expansion of Late Parameters

In the previous example, operation switch late was used to facilitate calling of a polymorphic function with late polymorphism resolution. It is not that convenient to wrap each invocation using `switch

late` whenever we need to call a late polymorphic function. Compiler uses the late parameter auto expansion technique in order to specifically handle cases like this.

If compiler discovers that late(dfa_callguard()) entry exists for current invocation, and it does not have enclosing switch late already, compiler automatically generates different branches that invoke relevant specializations and transfers control to a branch depending on the late parameter's value. In other words, invocation is implicitly wrapped into switch late if needed.