Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Variant Comparison

We can compare variants with quite a few of different things. Comparing means the equals and not-equals operations == and !=, nothing else. We cannot see if one variant is bigger than another variant, for example, but we can definitely see if they are equal to one another. We can also compare variants to types to see if they contain a value of that type. But, let's look at all different cases in isolation.

Comparing Types

We can compare a variant to a type to do a quick "does it hold that value" check:

use Core.print

variant MyVar:
	i32, f32, bool;

def main():
	MyVar var = 3.14;

	if var == i32:
		print("holds i32 value\n");
	else if var == f32:
		print("holds f32 value\n");
	else if var == bool:
		print("holds bool value\n");

This program will print this line to the console:

holds f32 value

The == T check actually is semantically more of a Does it hold a value of type T? check. If you look closely, you can see the similarity of the above code with a switch statement:

use Core.print

variant MyVar:
	i32, f32, bool;

def main():
	MyVar var = 3.14;

	switch var:
		i32(v):  print("holds i32 value\n");
		f32(v):  print("holds f32 value\n");
		bool(v): print("holds bool value\n");

with the same output as the other program. But in a switch we direclty gain an additional reference to the inner value of the variant we are working with. But, if we only want to do an action on only one type of the variant and do nothing on all other types, we can't really express this through a switch statement. Because how do we define doing "nothing" in a switches branch? That simply isn't possible in Flint.

So, for that reason we can compare variants to types to see if it holds a value of that type. Note that T must be a valid type of the variant. If T is not a valid type of the variant, you will get a compile error stating that the type you try to compare the variant with is not a part of the variant's possible types.

Comparing Tags

Just like we can compare a variant to it's type, we can compare a variant to it's tag to see if it holds a value of that type. As described in the chapter abour tagged variants, a variation with a tag must be accessed and checked through this tag.

use Core.print

variant MyVar:
	Int(i32), Float(f32), bool;

def main():
	MyVar var = -7;

	if var == MyVar.Int:
		print("holds Int value\n");
	else if var == MyVar.Float:
		print("holds Float value\n");
	else if var == bool:
		print("holds bool value\n");

This program will print this line to the console:

holds Int value

Just like in the last example, the similarity to the switch statement is pretty easy to spot:

use Core.print

variant MyVar:
	Int(i32), Float(f32), bool;

def main():
	MyVar var = 3.14;

	switch var:
		MyVar.Int(v):   print("holds Int value\n");
		MyVar.Float(v): print("holds Float value\n");
		bool(v):        print("holds bool value\n");

which will have the same output once again. So, as you can see we can compare a variant value with one of it's possible tags in addition to comparing it with it's possible types. But, just like described in the chapter about tagged variants, we cannot do var == i32, even though i32 is a valid type of the variant. We must check against it's tag when comparing and are not allowed to compare it to the underlying type directly if a tag is provided.

Comparing Variants

Comparing two variants is pretty straight forward. Two variant's are considered to be equal if both their types as well as their values match up. You already know that the underlying structure of a variant is a { u8, byte[N] }. Given this information you could quickly see that we cannot just compare the whole structure of a variant to another structure and if they are equal the variants are considered equal. It is a bit more nuanced than that. Let's say that we have the variant variant<i32, f32, i64>. In that case the variant has space for 8 bytes. So, if we have two variables and store -1 and 1 as an i64 in both of them all 8 bytes of the variant structure will be set. But if we then store the i32 value of 7 in both of them, only the fist 4 bytes are overwritten and the last 4 bytes stay the same as they were before. This means that comparing variants through their whole structure would not only compare what is stored in the variants but also what was stored in them, and this is semantically very incorrect. When we compare both variants and both hold an i32 value and that i32 value matches, they should be considered equal. Relying on the whole structure would actually be undefined behaviour, and we don't want that. So, that's why we only compare the first N bytes depending on the active type of the variant. That's more work internally, but it results in deterministic and defined behaviour.

So, here is a small example of comparing variants to one another:

use Core.print

variant MyVar:
	i32, i64, i32x3;

def main():
	MyVar var_1 = i64(-5);
	MyVar var_2 = i64(6);

	if var_1 == var_2:
		print("are equal\n");
	else:
		print("differ\n");

	var_1 = 5;
	var_2 = 5;
	if var_1 == var_2:
		print("are equal\n");
	else:
		print("differ\n");

This program will print these lines to the console:

differ
are equal

This output is expected. When we store different i64 values in the variant they obviously do not match and then we store two equal i32 values in the variant, so the variants should equal each other. If we would compare the whole structures of the variant to one another the second comparison of the two i32 values would yield false which would be extremely counter-intuitive.

The active_type field

Next we look at accessing the active_type field of the variant. You know the strucutre of the variant is { u8, byte[N] } and the active_type field of a variant is the first field of that struct. We can access this value (readonly) by just doing var.active_type on an variant. This will return a u8 value. And because we can compare u8 values we can actually check whether two variant variables hold the same type. This is especially useful for situations where we just want to know whether two variants hold the same type, independent from which type that is.

use Core.print

variant MyVar:
	i32, i64, i32x3;

def main():
	MyVar var_1 = i64(-5);
	MyVar var_2 = i64(6);

	if var_1.active_type == var_2.active_type:
		print("hold the same type\n");
	else:
		print("hold different types\n");

	var_1 = 5;
	if var_1.active_type == var_2.active_type:
		print("hold the same type\n");
	else:
		print("hold different types\n");

This program will print these lines to the console:

hold the same type
hold different types
are equal

You will see in the next big chapter about Error Sets and you actually have seen it before for getting the length of strings that you can access fields of primitive types or builtin types, fields which you normally do not see.

Note that the active_type indices start at 1. This means that i32 has the value 1, i64 the value 2 and i32x3 the value 3. Why the value 0 is not used here and why we have a one-based indexing in this case will be explained in a later chapter, stay tuned for that!

Comparing Values

It is not yet entirely clear whether this feature will be implemented at all

While useful, this feature would make comparing variants ambiguous. When comparing a variant to a type or another variant of the same type it can be clearly seen what's actually happening. But in the example below, doing var == ten brings a bit of ambiguity with it, as now ten could be of type MyVar, i32, i64 or i32x3. All types are entirely possible. For the parser this is no problem whatsoever, but for the person actually reading the Flint code this leads to additional cognitive load, whereas when not having this feature you can be sure that when you see var == ten that ten is definitely of type MyVar and can't be of any other type. So, it is still thought about whether to actually implement this feature. It is not implemented for now, as it's just syntactic sugar and doing that comparison coould be done through other ways as well. We will reconsider adding this feature at a later point in time, when time has shown that we actually want and need it. But it is most likely for this feature to stay pretty uncommon.

Lastly, we can also compare variants to variables and literals of a given type directly. Just like we can do var == i32 we can also do var == 5 and this will do two checks for us: Is var of type i32? If yes, does it's value match the value we compare it to? It's the same as if we would write var?(i32) != none and var!(i32) == 5 but we look at that syntax in the next chapter. Just be assured: It would be pretty hard to check if the variant matches a given value if this feature would not exist. This feature is something we would call syntactic sugar. It is not necessarily required to be implemented, but it makes our lives quite a lot easier.

use Core.print

variant MyVar:
	i32, i64, i32x3;

def main():
	MyVar var = i64(-5);

	if var == i64(-5):
		print("equals i64(-5)\n");

	i32 ten = 10;
	var = 10;
	if var == 7:
		print("equals 7\n");
	else if var == ten:
		print("equals ten\n");

This program will print these lines to the console:

equals i64(-5)
equals ten

As you can see it makes our lives quite a lot easier.