Interfaces and multimethods¶
Arza’s approach to polymorphism is probably the most original part of the language
If you want to learn in details about multiple dispatch you can read excelent Eli Bendersky’s articles https://eli.thegreenplace.net/2016/a-polyglots-guide-to-multiple-dispatch/
Arza uses generic functions with multiple dispatch and intefaces that restricts and formalises relations between types and generics.
Interfaces¶
Interface is a term that most often is an attribute of object oriented system. It describes set of operations that specific class can do.
But how can concept of interfaces can be applied to multimethods? In Arza interface can be described as a set of generic functions and specific argument positions in this functions.
For example
interface Storage =
get(storage of Storage, key)
interface Key =
use get(storage, key of Key)
In example above we declare two interfaces that share the same generic function get
.
Expression storage of Storage
bind first argument of function get
to interface Storage
.
Interface Storage
will be implemented by all types that act like first arguments in get
.
Interface Key
will be implemented by all types that act like second arguments in get
.
The same code can be written in shorter way
// all declarations are equivalent
interface Storage =
get(Storage, key)
//here I is interface alias
interface Storage(I) =
get(storage of I, key)
// or even shorter
interface Storage(I) =
get(I, key)
Generic function can be declared only inside interface. They can not be declared twice
interface Storage =
get(storage of Storage, key)
interface Key =
// this is error, function get already declared above
get(storage, key of Key)
// instead define reference with `use` expression
use get(storage, key of Key)
Interfaces do not create namespaces, our function get
will be available as get
, not as Storage:get
Generic function will dispatch only on arguments for whom interfaces are declared.
Interfaces in Arza perform two important functions
Formalisation of type behavior. Consider Arza’s pattern matching. If custom type used as first argument in
first
andrest
generics, it can be destructured byx::xs and [x, x1, ...xs]
patterns.Because in prelude there is interface
interface Seq = first(Seq) rest(Seq)
Compiler just perform this check
arza:is_implemented(customtype, Seq) == True
Also consider complex program with a lot of multimethods. In some point you may want to ensure that specific generics implemented for specifice types
Limit number of arguments to perform multiple dispatch. Multiple dispatch is a costly procedure, especially for dynamic languages. Even with cache strategies substantial amount of methods can degrade performance. By limiting number of dispatch arguments compiler can more easily fall back to single dispatch.
function
put
has one of the biggest call rate in Arza and because this function defined only with one interface it’s call time is more optimisedinterface Put(I) = put(I, key, value)
One interface can have multiple roles in one generic function
interface Eq(I) =
==(I, I)
!=(I, I)
Only types that have both first and second roles in == and != generics will implement Eq interface
Interfaces can be concatenated
interface Put(I) =
put(I, key, value)
interface At(I) =
at(I, key)
has(I, key)
// you can combine interfaces
interface Coll is (Put, At)
In some specific case there is a need to dispatch not on type of the argument but on argument value.
Example is the cast
function.
interface Cast(I) =
cast(I, valueof to_what)
interface Castable(I) =
use cast(what, I)
To specify that we need to dispatch on value keyword valueof
is used.
Afterwards we can use it like
import affirm
type Robot(battery_power)
def cast(r of Robot, type Int) = r.battery_power
def cast(r of Robot, interface Seq) = [r as Int]
def cast(r of Robot, interface Str) = "Robot"
def at(s of Robot, el) when el == #battery_power =
// casting back to Record type to avoid infinite loop
(s as Record).battery_power + 1
fun test() =
let
r = Robot(42)
in
affirm:is_equal(r.battery_power, 43)
affirm:is_equal(r as Int, 43)
affirm:is_equal(r as Seq, [43])
affirm:is_equal(r as Str, "Robot")
If concrete type defines custom method for at
generic
then to access it’s internal structure you must cast it to parent Record type.
Like in example above
def at(s of Robot, el) when el == #battery_power =
// casting back to Record type to avoid infinite loop
(s as Record).battery_power + 1
Most of the times our programs can be easily implemented with single dispatch. In some cases especially for mathematical operations double dispatch is very usefull. But sometimes there is a need for even bigger arity of dispatch function.
I never actually encounter them in my own work, but here I found this example of Triple Dispatch on the internet
Defining methods¶
To define new method for generic function use def
expression
interface Num =
//interface must be in both roles
add(Num, Num)
// only first argument
sub(Num, other)
type Vec2(x, y)
def add(v1 of Vec2, v2 of Vec2) = Vec2(v1.x + v2.x, v1.y + v2.y)
//However this would be an error
// because we define second argument to have specific type
def sub(v1 of Vec2, v2 of Vec2) = Vec2(v1.x - v2.x, v1.y - v2.y)
// This is correct
def sub(v1 of Vec2, v2) =
match v2
| Vec2(x, y) = Vec2(v1.x - x, v1.y - y)
Method definition can be simple function and two level functions but not case function.
Also method definition can have guards
interface Racer(R) =
race_winner(v1 of R, v2 of R)
type Car (speed)
type Plane (speed)
fun faster(v1, v2) = v1.speed > v2.speed
def race_winner(c1 of Car, c2 of Car)
| (c1, c2) when faster(c1, c2) = c1
| (c1, c2) when arza:at(c1, #speed) < c2.speed = c2
| (c1, c2) when c1.speed == c2.speed = c1
// plane always wins
// Double dispatch
def race_winner(c of Car, p of Plane) = p
def race_winner(p of Plane, c of Car) = p
There is a possibility to declare method not as function but as expression
def somegeneric(t of Sometype) as someexpression()
// often it's used with functions defined in native modules
// native module
import arza:_string
// assign native functions as methods
def slice(s of String, _from, _to) as _string:slice
def drop(s of String, x) as _string:drop
def take(s of String, x) as _string:take
Sometimes there is a need to override existing method
To do so use override
expression
interface F =
f1(F)
def f1(i of Int)
| 1 = #one
| i = i
// overriding
// expression (_) after override means that we do not need previous method
override(_) f1(i of Int) = 21
// here we bind previous method to name super and call it in our new method
override(super) f1(i of Int) = super(i) + 21
// this can be done indefinitely
override(super) f1(i of Int) = super(i) + 42
type Val(val)
// specifying builtin operator +
def + (v1 of Val, v2 of Val) = v1.val + v2.val
//overriding
override (super) + (v1 of Val, v2 of Val) = super(v1, v2) * 2
fun test() =
affirm:is_equal(signatures(f1), [[Int]])
affirm:is_equal_all(f1(1), f1(0), f1(10000), f1(-1223), 84)
let
v1 = Val(1)
v2 = Val(2)
in
affirm:is_equal((v1 + v2), 6)
Ensuring interface implementation¶
After implementing all interface roles type will put reference to interface in it’s list of implemented interfaces.
But if there is a need to ensuring that this type(types) implements one or more interfaces you
can assert this with describe
expression.
describe Symbol as Concat
describe String as (Eq, Repr,
Len, Coll,
Prepend, Append, Concat, Cons,
ToSeq, Slice, Empty)
describe (Triple, Pair) as Serializable
describe (Dictionary, Array, Pair, Triple, Single, SecondAndThird) as (Storage, GetSet)
If some of the types does not implement even one of the interfaces then exception will be thrown.
Traits¶
Trait in Arza is a function that can work on types. This function consist of one or more
def instance override
expressions. instance
expression is a trait application to specific
number of types.
Traits are tools for code reuse and expressiveness. If subtype-supertype relationship between types is unwanted traits can help to share behavior between them.
// creating trait
// trait excepts two types and defines for them two methods
trait TEq(T1, T2) =
def equal (first of T1, second of T2) = first == second
def notequal (first of T1, second of T2) = first != second
// applying previously defined trait to couple of types
instance TEq(Int, Int)
instance TEq(Float, Float)
instance TEq(Int, Float)
instance TEq(Float, Int)
Arza has special syntax for applying trait immidiatelly after it’s declaration
trait TValue(T) for MyType =
def val(v of T) = v.value
// to apply this to trait to more than one type
trait TValue(T) for [MyType1, MyType1, MyType3] =
def val(v of T) = v.value
// in case of more arguments
trait TEq(T1, T2) for (Int, Float) =
def equal (first of T1, second of T2) = first == second
def notequal (first of T1, second of T2) = first != second
// or to cover all relations
trait TEq(T1, T2)
for [(Int, Float), (Int, Int), (Float, Float), (Float, Int)] =
def equal (first of T1, second of T2) = first == second
def notequal (first of T1, second of T2) = first != second
Anonymous traits¶
If we do not care about reusing trait after declaration we can ommit trait name
trait (T1, T2)
for [(Int, Float), (Int, Int), (Float, Float), (Float, Int)] =
def equal (first of T1, second of T2) = first == second
def notequal (first of T1, second of T2) = first != second
// applying anonymous trait to multiple types in serial order
trait (T) for [Float, Int] =
// applying trait inside trait
instance TEq(T, T)
def - (x of T, y) as _number:sub
def + (x of T, y) as _number:add