-
Notifications
You must be signed in to change notification settings - Fork 21
Function overload resolution
This article describes how all aspects of how function overload resolution works in CQL.
Function overload resolution is the process by which a set of arguments are bound to a function's operands. When two functions exist which have the same name but differ only by its list of operands, also known as its signature, we must determine which of these functions is the best possible match.
For example, consider these two overloads:
define function Foo(x Integer)
define function Foo(x String)
define "Call with Integer": Foo(1)
define "Call with String": Foo('hello')
In the above example, x
is the operand of Foo
. In the first overload, the type of the operand is Integer; in the second, it is String.
The value 1
as it appears in Foo(1)
is an argument. This is the value that will be bound to the operand x
in Foo
.
These terms will be used throughout this article and it is critical to keep them straight. These are also the terms we use in the SDK codebase.
There are three main aspects to overload resolution:
- Computing the cost of a coercion
- Inferring the value of generic arguments
- Comparing compatible overloads to pick the best one
We will discuss each one of these in detail.
Coercion is the process of binding an argument of one type to an operand of another type. This allows usage like this:
define function TakesDecimal(x Decimal)
define CallWithInteger: TakesDecimal(1)
If CQL didn't implement automatic coercion during overload resolution, then CallWithInteger
would be an error. The author would have been required to write this instead:
define CallWithInteger: TakesDecimal(ToDecimal(1))
This would make CQL unwieldy to use. Virtually every modern language that any programmer will ever use implements argument-to-operand type coercion, so this concept should be familiar to you.
Consider this scenario:
define function Foo(x Decimal): 'decimal'
define function Foo(x Integer): 'integer'
define CallWithInteger: Foo(1)
In this example, your programming instincts will tell you that CallWithInteger
will return integer
because it will call the overload that takes an Integer
- the language will not decide to convert 1
to Decimal
implicitly, like it does in the prior example, because a "better" overload exists.
The reason Foo(x Integer)
is a better signature than Foo(x Decimal)
is because the coercion is cheaper.
CQL defines the relative cost of the coercion it supports in §4.9. Conversion Precedence.
We implement this functionality through a CoercionProvider
class, which does two things:
- Computes the cost of a conversion, returning a
CoercionCost
enum value - Creates the coercion by changing the ELM of the argument
This function has a signature of:
CoercionCost GetCoercionCost(Expression from, TypeSpecifier to)
'from' is as an ELM Expression
for the argument which we are coercing. to
is the type of the operand to which we are coercing the argument.
For example, in this ELM:
define function TakesDecimal(x Decimal)
define CallWithInteger: TakesDecimal(1)
We would be passing a Literal
expression whose value is 1 as from
, and a NamedTypeSpecifier
for the system Decimal
type as to
.
This function returns a CoercionCost
enumeration whose values are as follows:
ExactMatch = 1
Subtype = 2
Compatible = 3
Cast = 4
ImplicitToSimpleType = 5
ImplicitToClassType = 6
IntervalPromotion = 7
ListDemotion = 8
IntervalDemotion = 9
ListPromotion = 10
Incompatible = 1000
This order corresponds to the language of the specification.