Polymorph Functions

Compile using blc my-file-name.bl and run ./out.

// =================================================================================================
// Polymorphic (Generic/Templated) Functions
// =================================================================================================

/*
    Use 'blc --no-warning -run polymorph.bl' to execute this file.
 */

#import "std/array"

intro :: fn () {
    // Consider the following example; we need simple 'compare' function to perform comparison of two 
    // values.
    compare :: fn (a: s32, b: s32) bool {
        return a == b;
    };

    // Now we can call this function with any numbers.
    a1: s32 = 10;
    b1: s32 = 20;
    result1 := compare(a1, b1);
    print("compare(%, %) = %\n", a1, b1, result1);

    // This is great, but what if we want to do the same with floating point values?
    a2: f32 = 10.f;
    b2: f32 = 20.f;
    // result2 := compare(a, b); // This wont compile, the function expects 's32' not 'f32' numbers.

    // In this case we can write another 'compare' function taking 'f32' numbers instead of 's32'
    // or we can use polymorphic function in this case and let the compiler do all work.
    compare_poly :: fn (a: ?T, b: T) bool {
        return a == b;
    };

    // This function does not exist until it's used somewhere, we can think about it as if it was
    // a recipe for 'compare' function. Type '?T' (it can be any other name '?TValue', '?TKey' etc) is
    // replaced with the actual type used for the argument 'a' when the function is called. The 
    // question mark before the type name 'T' here is important, it means basically that 'T' can
    // be replaced. However, all other 'T' in the argument list are just references to the actual
    // replaced type used for 'a' argument.
    // So if type of 'a' argument is 's32', the type of 'b' is 's32' also.

    // Use of the function.
    a3: s32 = 10;
    b3: s32 = 20;
    result3 := compare_poly(a3, b3);
    print("compare_poly(%, %) = %\n", a3, b3, result3);

    // Since the type of 'b' is dependent on the type of 'a' argument, the following code would 
    // not compile.
    a4: s32 = 10;
    b4: f32 = 20.f; // using floating point number
    // result4 := compare_poly(a4, b4); // Compiler error 

    /* 

        polymorph.bl:47:29: error(0035): No implicit cast for type 'f32' and 's32'.
           46 |     b4: f32 = 20.f; // using floating point number
        >  47 |     result4 := compare_poly(a4, b4); // Compiler error
              |                                 ^^
           48 | } 

     */

    // But we call call the same function with different value types now. 
    a5: f32 = 10.f;
    b5: f32 = 20.f;
    result5 := compare_poly(a5, b5);
    print("compare_poly(%, %) = %\n", a5, b5, result5);

    // One important thing to know is that we have internally two 'compare_poly' functions generated
    // by the compiler. One for 's32' and one for 'f32' types. When we call 'compare_poly' again with
    // 'f32' values, compiler will try to reuse already existing function if possible (same for
    // 's32').

    // What if we call the 'compare_poly' function with arguments which cannot be compared by '=='
    // operator? 

    a6: string_view = "hello";
    b6: string_view = "world";
    // result6 := compare_poly(a6, b6); // Compiler error

    /*

        polymorph.bl:30:18: error(0035): Invalid operation for string_view type.
           29 |     compare_poly :: fn (a: ?T, b: T) bool {                                 
        >  30 |         return a == b;                                                      
              |                  ^^                                                         
           31 |     };                                                                      

        polymorph.bl:29:5: In polymorph of function with substitution: T = string_view;
           28 |     // or we can use polymorphic function in this case and let the compiler do all work.                                                                        
        >  29 |     compare_poly :: fn (a: ?T, b: T) bool {                                 
              |     ^^^^^^^^^^^^                                                            
           30 |         return a == b;                                                      

        polymorph.bl:79:16: First called here:                                              
           78 |     b6: string_view = "world";
        >  79 |     result6 := compare_poly(a6, b6);                                        
              |                ^^^^^^^^^^^^                                                 
           80 | }                                                                           

     */

    // We can handle such situations quite easily, see the next section.
}

specialization :: fn () {
    // Remember that we have separate implementation of our function for every type internally.
    // Keeping this in mind, we can create specialization for some types if needed. So how to make
    // out 'compare' function to work even with strings?

    compare_poly :: fn (a: ?T, b: T) bool {
        are_the_same := false;
        // #if is evaluated in compile-time and only one of the branches is actually "inserted" into
        // the code based on the condition.
        // One important think here to know is that the condition value must be known in compile-time,
        // in this case the compiler knows that 'T' has been already replaced and does not change in
        // the function-type specialization.
        #if T == string_view {
            // Do this when 'T' is 'string_view'.
            are_the_same = std.str_match(a, b);
        } else {
            // Do this for all other types. 
            are_the_same = a == b;
        }
        return are_the_same;
    };


    a1: s32 = 10;
    b1: s32 = 10;

    // The function is called with integers so the second branch is used when the function
    // implementation is generated.
    result1 := compare_poly(a1, b1);
    print("compare_poly(%, %) = %\n", a1, b1, result1);


    // The same but now with strings.
    a2: string_view = "hello";
    b2: string_view = "hello";

    // The function is called with strings so the first branch is used when the function
    // implementation is generated.
    result2 := compare_poly(a2, b2);
    print("compare_poly(%, %) = %\n", a2, b2, result2);


    // So now we can call the "same" function with numbers and integers, but what if we call it with
    // structure type?

    Data :: struct {
        a: s32;
        b: bool;
    };

    a3: Data;
    b3: Data;
    // result3 := compare_poly(a3, b3); // Compiler error

    /*

        polymorph.bl:123:30: error(0035): Invalid operation for Data type.
           122 |             // Do this for all other types.
        >  123 |             are_the_same = a == b;
               |                              ^^
           124 |         }

        polymorph.bl:111:5: In polymorph of function with substitution: T = Data;
           110 |
        >  111 |     compare_poly :: fn (a: ?T, b: T) bool {
               |     ^^^^^^^^^^^^
           112 |         are_the_same := false;

        polymorph.bl:158:16: First called here:
           157 |     b3: Data;
        >  158 |     result3 := compare_poly(a3, b3);
               |                ^^^^^^^^^^^^
           159 | }

     */

    // Yes, again the '==' operator is not valid for our 'Data' type. We can consider this as
    // mistake in our program and introduce some meaningful error report.

    compare_poly_safe :: fn (a: ?T, b: T) bool {
        are_the_same := false;
        #if T == string_view {
            are_the_same = std.str_match(a, b);
        } else {
            is_number :: T == s32 || T == f32;
            #if is_number {
                are_the_same = a == b;
            } else {
                compiler_error("The compare_poly_safe function works only with numbers and strings!");
            }
        }
        return are_the_same;
    };

    // Let's try it again:

    a4: Data;
    b4: Data;
    // result4 := compare_poly_safe(a4, b4); // Compiler error

    /*

        polymorph.bl:194:18: error(0084): The compare_poly_safe function works only with numbers and strings!
           193 |             } else {
        >  194 |                 compiler_error("The compare_poly_safe function works only with numbers and strings!");
               |                  ^^^^^
           195 |             }

        polymorph.bl:185:5: In polymorph of function with substitution: T = Data;
           184 |
        >  185 |     compare_poly_safe :: fn (a: ?T, b: T) bool {
               |     ^^^^^^^^^^^^^^^^^
           186 |         are_the_same := false;

        polymorph.bl:204:16: First called here:
           203 |     b4: Data;
        >  204 |     result4 := compare_poly_safe(a4, b4); // Compiler error
               |                ^^^^^^^^^^^^^^^^^
           205 | }

     */
}

function_interface_matching :: fn () {
    // The polymorph type matching can follow type patterns in function declaration. Consider the
    // following example:

    push_anything :: fn (array: *[..]?T, value: T) {
        array_push(array, value);
    };

    // Such a function accept only pointers to dynamic arrays '*[..]' containing any type '?T'.

    numbers: [..]s32;
    strings: [..]string_view;

    // Cleanup at the end of the scope.
    defer array_terminate(&numbers);
    defer array_terminate(&strings);

    push_anything(&numbers, 10);
    push_anything(&strings, "hello");
    print("%\n", numbers);
    print("%\n", strings);

    // Now the 'array' argument must be pointer to any dynamic array every time and value type must
    // be the same as the array element type.

    // push_anything(numbers, 10); // Compiler error.

    /*

        polymorph.bl:235:33: error(0081): Cannot deduce polymorph function argument type 'T'. Expected is '*[..]T' but call-side argument type is '[..]s32'.
           234 |
        >  235 |     push_anything :: fn (array: *[..]?T, value: T) {
               |                                 ^
           236 |         array_push(array, value);

        polymorph.bl:256:19: Called from here.
           255 |
        >  256 |     push_anything(numbers, 10); // Compiler error.
               |                   ^^^^^^^^
           257 | }

     */

    // push_anything(&numbers, "hello"); // Compiler error.

    /*

        polymorph.bl:274:31: error(0035): No implicit cast for type 'string_view' and 's32'.
           273 |
        >  274 |     push_anything(&numbers, "hello"); // Compiler error.
               |                              ^^^^^
           275 | }

     */
}

function_interface_matching_with_sub_types :: fn () {
    // One really cool feature of BL is possibility to access "sub-types"; take a look at following
    // example showing naive implementation of hash table:

    Table :: struct {
        keys: [..]s32;
        values: [..]string_view;
    };

    // In general we can do following to get type of any structure member:

    TypeOfKeys :: Table.keys;
    TypeOfValues :: Table.values;
    print("TypeOfKeys = %\n", TypeOfKeys);
    print("TypeOfValues = %\n", TypeOfValues);

    // Or go even more deeper.
    TypeOfKey :: @Table.keys.ptr; // use @ to convert *T to T 
    print("TypeOfKey = %\n", TypeOfKey);

    // There is probably no need to use this feature like this, but it can be really useful in
    // polymorph functions.

    // The following function can insert values into our table:
    insert :: fn (table: *?T, key: @T.keys.ptr, value: @T.values.ptr) {
        array_push(&table.keys, key);
        array_push(&table.values, value);
    };

    // And we also want to lookup our values in the table...
    get :: fn (table: *?T, key: @T.keys.ptr) @T.values.ptr {
        default_value: @T.values.ptr; // zero initialized by default
        loop i := 0; i < table.keys.len; i += 1 {
            if table.keys[i] == key {
                return table.values[i];
            }
        }
        return default_value;
    };

    table: Table;
    insert(&table, 10, "hello");
    insert(&table, 20, "world");

    v1 := get(&table, 10);
    v2 := get(&table, 20);
    print("v1 = %\n", v1);
    print("v2 = %\n", v2);

    // But wait! Why we should use polymorphic functions like this if type of table is always
    // 'Table'?

    // Because we can do this:

    AnotherTable :: struct {
        keys: [..]u64;
        values: [..]bool;
    };

    another_table: AnotherTable;
    insert(&another_table, 10, true);
    insert(&another_table, 20, false);

    v3 := get(&another_table, 10);
    v4 := get(&another_table, 20);
    print("v3 = %\n", v3);
    print("v4 = %\n", v4);

    // This is just one step from 'generic' hash table, because we can generate the type of the
    // table in compile-time based on some inputs. This is not really part of this demo, but type of
    // the table can be generated by compile-time function:

    GenericTable :: fn (TKey: type, TValue: type) type #comptime {
        // Return new type.
        return struct {
            keys: [..]TKey;
            values: [..]TValue;
        };
    };

    generic_table: GenericTable(s32, string_view);
    // The function 'GenericTable' is called during compilation and it's result is used as constant; in
    // this case it's type of out table.

    // And we can still use the same functions to modify the table.
    insert(&generic_table, 10, "hello");
    insert(&generic_table, 20, "world");

    v5 := get(&generic_table, 10);
    v6 := get(&generic_table, 20);
    print("v5 = %\n", v5);
    print("v6 = %\n", v6);
}

main :: fn () s32 {
    intro();
    specialization();
    function_interface_matching(); 
    function_interface_matching_with_sub_types(); 
    return 0;
}