Writing Polymorphic Functions

Kronos functions can be polymorphic. At the lowest level, you have access to a compile time if. Yes, it is Turing complete. Making full use of that is probably not a good idea - most type systems are incomplete by design. Fortunately we have quite reasonable abstractions for you to fall back on.

Interestingly, the Kronos runtime is Turing incomplete due to having no dynamic branches or loops.

Pattern matching

Usually the best way is to pattern match against data. To demonstrate, let's write a monomorphic function:

Square-Number(x) { x * x } Square-Number(3)9 ; Unfortunately, Square-Number("horse") * E-9995: Fatal{Eval (Anon-Fn nil) Fatal{eval nil Fatal{Square-Number horse Fatal{ (horse horse) Fatal{:Fallback:Binary-Op (:Fn{:Mul} Mul horse horse) Fatal{:Fallback:No-Match (Mul horse horse) Exception{Type mismatch{Could not find a valid form of ' Mul ' for arguments of type ' (horse horse) '}}}}}}}} | Square-Number(horse) | (horse horse) Cannot 'Mul' these types at compile time Received (horse horse) Cannot 'Mul' these types natively Received horse | :Fallback:Binary-Op(:Fn{:Mul} Mul horse ... ) | :Fallback:No-Match(Mul horse horse) Could not find a valid form of ' Mul ' for arguments of type ' (horse horse) ' (Type mismatch) Received (Mul horse horse)

The compiler barfs its type grievances. The salient information is on the last line: there's no 'Mul' for horses.

Now, let's devise an ingenious plan to make polymorphic Square that works for both numbers and strings, and also for a pair.

Square(x) { Square = x * x Square = String:Append("[" String:Append(x "]")) ; For a pair, let's square both elements (a b) = x Square = (Square(a) Square(b)) } Square(3)9 Square("horse")[horse] Square(10 9)100 81 ; Our definition of 'square of pairs' has some serendipitous ; consequences Square(1 2 3 4 5)1 4 9 16 25 Square((1 2) (3 4 "foo") 5 6)(1 4) (9 16 [foo]) 25 36

We have three definitions for the polymorphic function. The compiler tries them in reverse source order. Destructuring a pair and String:Append are low level operators that match against a type; if its input is not a string, the enclosing function form is rejected.

A good rule of the thumb is to write the forms from the least specialized to the most specialized.

Higher Order Functions

The Algorithm package contains some functional staples, like Map to apply a function to all elements in a set, and Reduce to combine elements of a set. Internally, they make use of polymorphism. In the rest of this example, we build small equivalents. If you wish, take a look at the implementations in the standard library.

Fold

Fold is a simple function that can be used to combine elements of an ordered set. It takes the head of the set, recursively folds the tail of the set, and applies a combinator function to the results.

MyFold(f set...) { ; if the set can't be destructured, assume it's a scalar MyFold = set... ; pattern match against a destructurable set (x xs) = set... MyFold = f(x MyFold(f xs)) } MyFold(Add 1 2 3 4 5)15

Unfortunately, this fold does not know how to operate on nil-terminated lists, because it thinks nil is a scalar.

MyFold(Add [1 2 3]) * E-9995: Fatal{Eval (Anon-Fn nil) Fatal{eval nil Fatal{MyFold (:Fn{:Add} Floatx3 nil) Fatal{MyFold (:Fn{:Add} Floatx2 nil) Fatal{MyFold (:Fn{:Add} Float nil) Fatal{f (Float nil) Fatal{:Fallback:Binary-Op (:Fn{:Add} Add Float nil) Fatal{:Fallback:No-Match (Add Float nil) Exception{Type mismatch{Could not find a valid form of ' Add ' for arguments of type ' (Float nil) '}}}}}}}}}} | MyFold(:Fn{:Add} Floatx3 n ... ) | MyFold(:Fn{:Add} Floatx2 n ... ) | MyFold(:Fn{:Add} Float nil ... ) | f(Float nil) Cannot 'Add' these types at compile time Received (Float nil) Cannot 'Add' these types natively Received nil | :Fallback:Binary-Op(:Fn{:Add} Add Float ... ) | Coerce-Binary(Float nil) | Type-Conversion:Implicit(nil Float) No match in C:\ProgramData\Kronos\Cache\kronoslang\core\0.12.7\Implicit-Coerce.k(8;19) Coerce-Binary = (Type-Conversion:Implicit(b a) b) ^^^ | Type-Conversion:Implicit(Float nil) No match in C:\ProgramData\Kronos\Cache\kronoslang\core\0.12.7\Implicit-Coerce.k(7;21) Coerce-Binary = (a Type-Conversion:Implicit(a b)) ^^^ No match in C:\ProgramData\Kronos\Cache\kronoslang\core\0.12.7\Implicit-Coerce.k(15;10) func(Coerce-Binary(a b)) ^^^ | :Fallback:No-Match(Add Float nil) Could not find a valid form of ' Add ' for arguments of type ' (Float nil) ' (Type mismatch) Received (Add Float nil)

The salient information is at the end: The compiler is trying to perform 3 + nil. Let's extend our understanding of a terminating condition:

; append a form after the fact with #[Extend] MyFold(f set...) #[Extend] { (x xs) = set... When(Nil?(xs) x) } MyFold(Mul [1 2 3])6

Map

We can build a Map function similarly. This time, we accumulate a list on the return side:

MyMap(f set...) { ; map trailing scalar MyMap = f(set...) ; always map terminating nil to nil MyMap = When(Nil?(set...) nil) ; destructure, map head and recur on tail (x xs) = set... MyMap = (f(x) MyMap(f xs)) } MyMap((* 100) 7 3 2 1)700 300 200 100 MyMap(Math:Sqrt [1 2 3 4 5 6])1 1.4142135 1.7320508 2 2.236068 2.4494898 nil

If you are experienced in functional programming, you might worry that these fúnctions are not tail-recursive. You would be correct in the case of MyFold. However, MyMap is good: the way Kronos operates, you can generally assume tail consing. In other words, the compiler produces constant-space recursions even if structuring or destructuring happens after the recursive call.

DSP with Map and Fold

You can plug any combinator/mapping function into the constructs we just wrote, including the standard library oscillators.

Import Gen vibr = Gen:Sin(5) * 0.01 + 1 freqs = MyMap((* vibr) 220 275 330) oscs = MyMap(Gen:Tri freqs) snd = MyFold((+) oscs) * 0.1