Extending Functions

One of the age-old rifts between programmers is the one between those who think that using the operator + for strings makes sense and those who are right. In this example, we will finally settle this debate with the only semantics that make sense, and on the way, we learn how to extend built-in functions in Kronos.

Roman Numerals

Let us define arithmetic operators for strings that work with Roman numerals. The first step is to decode and encode numeral strings into the mundane numeric types Kronos readily understands.

Numerals = [("M" #1000) ("CM" #900) ("D" #500) ("CD" #400) ("C" #100) ("XC" #90) ("L" #50) ("XL" #40) ("X" #10) ("V" #5) ("IV" #4) ("I" #1)] To-Roman(value) { ; Compile-time loop using an inner function ; that recurs. encode(value decimals) { ; examine the head of current list of encodable ; numerals ((r d) next) = decimals When(; already encoded all numerals? value == #0 "" ; is largest encodable numeral applicable? value >= d ; if so, output it and append the rest. String:Append(r Recur(value - d decimals)) ; skip this numeral and try the next one Otherwise Recur(value next)) } encode(value Numerals) } From-Roman(numeral) #[Pattern] { Use Algorithm ; a loop decode(value previous numeral-chars) { ; split list of numerals into head and tail (vn rn) = numeral-chars ; find a matching numeral. '==t' checks for compile time ; type equality. num = Some((nn _) => (nn ==t vn) Numerals) ; Extract the increment (_ inc) = num When(; all numerals decoded? Nil?(numeral-chars) value ; not a valid numeral? Nil?(num) nil ; is current numeral smaller than previous? inc < previous ; then substract and decode the rest Recur(value - inc inc rn) Otherwise ; otherwise add and decode the rest Recur(value + inc inc rn)) } ; split string into characters in reverse len = String:Length(numeral) chars = Map(i => String:Take(String:Skip(numeral i) #1) Expand(len (- #1) len - #1)) ; check that we got a valid numeral result = decode(#0 #0 chars) When(Constant?(result) result) } test = Algorithm:Expand(#11 (* #2) #1) numerals = Algorithm:Map(To-Roman test) decoded = Algorithm:Map(From-Roman numerals) test#1 #2 #4 #8 #16 #32 #64 #128 #256 #512 #1024 nil numeralsI II IV VIII XVI XXXII LXIV CXXVIII CCLVI DXII MXXIV nil decoded#1 #2 #4 #8 #16 #32 #64 #128 #256 #512 #1024 nil

We put the #[Pattern] attribute on From-Roman. That enables the use of From-Roman as a pattern matching accessor. Any polymorphic function that makes use of From-Roman will move to the next form if the numeral can't be decoded.

Extending Arithmetic Ops

Now we have the basic plumbing to operate with Roman numerals.

; the Extend attribute allows us to append a polymorphic form to Add Add(a b) #[Extend] { ar = From-Roman(a) br = From-Roman(b) To-Roman(ar + br) } "XIV" + "VIII"XXII ; make sure that non-numeral strings don't match with From-Roman "foo" + "bar" * E-9995: Fatal{Eval (Anon-Fn nil) Fatal{eval nil Fatal{ (foo bar) Fatal{:Fallback:Binary-Op (:Fn{:Add} Add foo bar) Fatal{:Fallback:No-Match (Add foo bar) Exception{Type mismatch{Could not find a valid form of ' Add ' for arguments of type ' (foo bar) '}}}}}}} | (foo bar) | From-Roman(foo) No match Cannot 'Add' these types at compile time Received (foo bar) Cannot 'Add' these types natively Received foo | :Fallback:Binary-Op(:Fn{:Add} Add foo b ... ) | Coerce-Binary(foo bar) | Type-Conversion:Implicit(bar foo) 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(foo bar) 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 foo bar) Could not find a valid form of ' Add ' for arguments of type ' (foo bar) ' (Type mismatch) Received (Add foo bar)

Sorry for the slightly intimidating error: the type constraints in From-Roman causes "foo" to be rejected as a Roman numeral.

While we could overload all the arithmetic ops, it is more useful to hitch a ride on the type coercion machinery built into the standard library. Let's override the Binary-Op fallback point instead:

Fallback:Binary-Op(func name a b) #[Extend] { ar = From-Roman(a) br = From-Roman(b) To-Roman(func(ar br)) } "XIV" * "V"LXX Math:Pow("II" "VIII")CCLVI

This has the benefit of only hitting our code path if no other method succeeds (the builtin binary ops call Fallback:Binary-Op in that case), and it extends all the binary operators in one fell swoop.


Ok, this is getting ridicilous. However, I hope you enjoyed it, and maybe got something useful out of it. That's the best kind of ridicilous. If you're looking for a less tongue-in-cheek example of extending the number types, please have a look at the complex number type in the standard library.