Let’s say you need a count procedure that you pass an array and returns “numbers” or “words” depending on the input type.
Input
count({1, 2, 3})
count({"one", "two", "three"})
Output:
"3 numbers"
"3 words"
In Odin we have Explicit Procedure Overloading. The design goals of Odin were explicitness and simplicity.
// odin run counter.odin -file
package Counter
import "core:fmt"
count_numbers :: proc(items: []int) -> string {
return fmt.tprintf("%d numbers", len(items))
}
count_words :: proc(items: []string) -> string {
return fmt.tprintf("%d words", len(items))
}
// Explicit Procedure Overloading
// Notice this is a procedure without parenthesis after `proc`
count :: proc {
count_words,
count_numbers
}
main :: proc() {
numbers := []int{1, 2, 3}
fmt.println(count(numbers))
words := []string{"one", "two", "three"}
fmt.println(count(words))
}
Explicit overloading has many advantages:
- Explicitness of what is overloaded
- Able to refer to the specific procedure if needed
- Clear which scope the entity name belongs to
- Ability to specialize parametric polymorphic procedures if necessary, which have the same parameter but different bounds (see where clauses)
String concatenation
Let’s use procedure overloading and reimplement strings.concatenate
import "core:strings"
// My String is good
strings.concatenate({"My String", " is good"})
The declaration of strings.concatenate
is the following:
concatenate :: proc(a: []string, allocator := context.allocator, loc := #caller_location) -> (res: string, err: mem.Allocator_Error) #optional_allocator_error {…}
The declaration of fmt.tprintf
is the following:
tprintf :: proc(fmt: string, args: ..any, newline := false) -> string{...}
As you can see, the main problem with strings.concatenate
is that requries an array of strings as the first param. We can’t use variadic arguments like fmt.tprintf
because it would require to use named arguments to pass the allocator, which will be odd.
We can see this problem in fmt.aprintf
which uses variadic arguments and an allocator param.
fmt.aprintf("%s%s", strings.to_string(buf), text, allocator = allocator)
Can we have a strings.concatenate
procedure that accepts variadic params and allocators without passing an array and named params?.
String.concatenate("My String", " is good", context.allocator)
Yes we can, but it would require Procedure Overloading
and a bit of “Runic Magic”.
Runic Magic
I will call this technique as “Runic Magic” (Maybe it has another official name in other contexts or languages, please comment).
The main idea is that we can create different procedures depending on the number of params, is evident that these cannot be “infinite”, a limit must be put in place. I think 16 params are enough for most uses cases (A 16 param procedure maybe doing too much), for more amount is best to just use strings.concatenate
way of implementation or variadic args + named params as fmt.aprintf
did.
import "core:strings"
concatenate_2_strings :: proc(a: string, b: string, allocator := context.temp_allocator, loc := #caller_location) -> (res: string, err: mem.Allocator_Error) #optional_allocator_error {
return strings.concatenate({a, b}, allocator, loc)
}
concatenate_3_strings :: proc(a: string, b: string, c: string, allocator := context.temp_allocator, loc := #caller_location) -> (res: string, err: mem.Allocator_Error) #optional_allocator_error {
return strings.concatenate({a, b, c}, allocator, loc)
}
concatenate_4_strings :: proc(a: string, b: string, c: string, d: string, allocator := context.temp_allocator, loc := #caller_location) -> (res: string, err: mem.Allocator_Error) #optional_allocator_error {
return strings.concatenate({a, b, c, d}, allocator, loc)
}
concatenate_5_strings :: proc(a: string, b: string, c: string, d: string, e: string, allocator := context.temp_allocator, loc := #caller_location) -> (res: string, err: mem.Allocator_Error) #optional_allocator_error {
return strings.concatenate({a, b, c, d, e}, allocator, loc)
}
concatenate_default :: proc(a: []string, allocator := context.temp_allocator, loc := #caller_location) -> (res: string, err: mem.Allocator_Error) #optional_allocator_error {
return strings.concatenate(a, allocator, loc)
}
// concatenate_n_strings :: proc()
// ...
// concatenate_16_strings :: proc()
concatenate :: proc {
concatenate_2_strings,
concatenate_3_strings,
concatenate_4_strings,
concatenate_5_strings,
// ...
// concatenate_16_strings,
concatenate_default,
}
// This is a good string
fmt.println(concatenate("This", " is ", "a", " good ", "string"))
Why?
Runic Magic enables another way of accepting params in procedures. That can be used for more consistent apis. For example we have fmt.aprintf
that accepts var args and named arg context and strings.concatenate
that accepts an array and context. With this technique both can be used in the same way.
Consistency is key to achieve robust systems, and contribute the glorious goal of “joy of programming” and the design goals of Odin of explicitness and simplicity.