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 #comptime, TValue: type #comptime) 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;
}