CrashCourse – 006 – Templates and relationals

Last time I introduced the Intrinsic class together with some handy relational operator related functionality. But since Intrinsic is now the home of operations like Clamp and Max and these operations are defined by templates, without filtering the template parameters they can receive, anything can be passed to these functions. Even things that are not comparable!

So let’s see what problems this can cause and how to solve them. Let us consider a simple Version class that holds the major, minor and revision number of some product. A very simple class, not meant to be functional, just a simple example:

class Version {
	val major = 0;
	val minor = 0;
	val revision = 0;
	
	this(maj: Int, min: Int, rev: Int) {
		major = maj;
		minor = min;
		revision = rev;
	}
}

To break up the monotony of large blocks of text on the blog, I shall show a screenshot of the typical ZIDE workflow that is used when developing the samples for this blog and anything else Z2 related:

006less01

A quick tour of the sample. On line 3 we define our simple Version class and on line 15 we define a class with a @main slot method to test the version class. On lines 17 and 18 we instantiate two version variables.

On line 20, we test the equality of these two variables. As described in CrashCourse – 003 – What you get for free, Z2 will figure out common task for you from a small pool of common tasks. Simple straightforward comparison of value types is one of these tasks. The compiler takes one look at the Version and has zero problems to test equality of two instances. You as a programmer you shouldn’t have to write such easy boring code that only compares 3 Ints. Same for line 21, where we test for inequality. These two lines work out of the box because aggregate value type equality if a well defined unambiguous operation. Such methods that are provided automatically by the compiler for you are called automatic methods. They are always provided, but you can suppress this for a class/method combination if not needed.

And this is the reason why line 22 fails to compile. On this line, we don’t use operators == or !=, but operator <. The “less” operator can’t be defined unambiguously for aggregate types and the compiler can’t resolve v1 < v2. This is what the error says. Maybe the error message could be improved though.

The solution for this compilation error is to provide a < since the compiler can’t provide one for us. In Z2, method names that start with @ are called slots and they are just regular methods, but in some context the compiler will call them implicitly. Like @main. The calling mechanisms of the slots makes them perfect candidates for defining operators in classes. This was deemed a better solution than using a keyword like in other programming languages and the literal operator. It is easier to read, type and manually call. And solves some additional problems, like with pre and post ++ operators.

The < can be defined using the @less method:

class Version {
	val major = 0;
	val minor = 0;
	val revision = 0;
	
	this(maj: Int, min: Int, rev: Int) {
		major = maj;
		minor = min;
		revision = rev;
	}
	
	def @less(const v: Version): Bool; const {
		System.Out << "call " << class << '.' << def.Name << " of class " << @neq.class << "\n";
		if (major < v.major)
			return true;
		else if (major > v.major)
			return false;
		else  {
			if (minor < v.minor)
				return true;
			else if (minor > v.minor)
				return false;
			else
				return revision < v.revision;
		}
	}
}

This updated Version class defines on line 12 the @less slot. Now, when we do v1 < v2, the @less method will be called on the v1 instance and v2 will be passed in as a parameter. This is of course just syntactic sugar since @less is just a normal method with a name that starts with @, so it can be called normally: v1 < v2 and v1.@less(v2) are equivalent and result in the same machine code. The actual implementation is not important. I used here a simple implementation of the top of my head for comparing versions and may not be the optimal way to compare versions in production code. It is just a sample. This method could be implemented to not work as a relational less operator, even though it represents that slot. It is a very good idea not to do this to avoid major confusion.

One strange thing about this method is line 13. This is just for this sample to test that this method is actually called. Normally, you wouldn’t add such tests in real code. It could have been a simple System.Out << "Hey, @less has been called"; but I opted for this more complicated statement to demonstrate some reflection. Z2 has both compile and run time reflection and these features combined with templates and can be quite powerful. But in this sample we only use reflection for basic debugging. First we print out class. This is equivalent with this.class and returns the compile time class information for this, a reference to the current instance. def is similar to this, but does not represent the current instance, but instead the current method. Like class.Name, def.Name will return the name of the current method. And similarly to this.class, def.class returns the class information for the current method. In Z2 everything is an instance of a class, even methods. The class of all methods is Def. But instead of printing out def.class, I printed out the class of another method: @neq.class. It is the same class and I used this in the sample both to demonstrate that the classes are the same and that automatic methods are there and present, even if it not obvious that they are: @eq is the == operator and @neq is the != operator. So the output of this program after the fix will be:

false
true
call Version.@less of class Def
true

With @less working, now we can try a much more complicated sample, where we use all relational operators, Min,Max, Clamp and so on:

class Test {
	def @main() {
		val v1 = Version{1, 2, 7000};
		val v2 = Version{2, 0, 1};
		
		System.Out << (v1 < v2) << "\n";
		System.Out << (v1 > v2) << "\n";
		System.Out << (v1 <= v2) << "\n";
		System.Out << (v1 >= v2) << "\n";
		
		System.Out << "Min: " << Intrinsic.Min(v1, v2) << "\n";
		System.Out << "Max: " << Intrinsic.Max(v1, v2) << "\n";
		
		val v3 = Version{1, 0, 0};
		Intrinsic.Clamp(v3, v1, v2);
		System.Out << "Clamp: " << v3 << "\n";
		
		val v4 = Intrinsic.Clamped(Version{7, 5, 6}, v1, v2);
		System.Out << "Clamped: " << v4 << "\n";
	}
}

call Version.@less of class Def
true
call Version.@less of class Def
false
call Version.@less of class Def
true
call Version.@less of class Def
false
call Version.@less of class Def
Min: 1 2 7000
call Version.@less of class Def
Max: 2 0 1
call Version.@less of class Def
Clamp: 1 2 7000
call Version.@less of class Def
call Version.@less of class Def
Clamped: 2 0 1

The interesting part of this sample starts on lines 6-9. We only defined operator <, but we can use >, <= and >=. Think of it as the compiler making up for the fact that it couldn’t provide you with a free < operator. If you have @less defined, but not @more(the > operator), the compiler can still handle > by swapping the two operands: v1 > v2 is compiled as v2 < v1. And since we provided less and @eq, the equality operator, is an automatic method, the compiler can use them both to provide operator , called @lesseq and @moreeq. And once all these operators are defined manually or automatically, you can now use all the stuff from Intrinsic. And the same applies for when you have @more defined but not @less.

This part of the language design may be a bit confusing at first, so let me reiterate the rules:

  • @eq (==) and @neq (!=) are automatic. You get them for free, but you can of course override them and do something different. For POD types you rarely need to, but or non POD types and types that embed pointers, you may need to provide a better implementation since the automatic one might not do what you need.
  • @less (<) and @more (>) are not automatic. You need to define at least one of them! If you define both, they are use appropriately. If you define only one, their opposite is resolved by swapping around the operands.
  • @lesseq (<=) and @moreeq (>=) are automatic only if you defined at least one of @less and @more. Once you define at least one of these two, you get all 4. You are free to override @lesseq and @moreeq as with the others, and sometimes it is worth it from a performance point of view. To do <=, the compiler may need to do both < and =. It is sometimes possible to implement <= in a more efficient way.

So to reiterate, in order to get full relational operator coverage, you need to define either @less, @more or both!

In the git repository you can find these samples and more, part of the daily unit-testing.

With this, the very basics of the core numerical types are covered. Next time I’ll extend upon this as a jumping off point to introducing vectors!

Advertisements

CrashCourse – 005 – Int and Intrinsic

Last time I wrote about the basics of the Z2 library using an older and shorter version of the Int class. It is an archetypal value type and behaves similarly in a lot of languages, so it is easy to understand. I described how it handles conversions and operators using intrinsic functionality, how one can use constants to allow a class to offer some basic information about its value range and showed a few methods and properties.

The design looks viable, but has a few problems. It is easy to see this once you try to expand upon the library by adding a few more basic types. Just adding a single class, like Double, the only dependency of Int in this sample, would see us repeat the same code with minor changes. Defining the constants each time makes sense, since they have different values. But how about some methods? Like GetMin and GetMax? Sure, they are short and having them copied over into each class, including third-party classes is not a big issue, but surely there must be a better method.

This is where intrinsics come in! Last time we talked about two types of intrinsics: conversion constructors and operators, both in the context of numerical types. These represent the highest level of intrinsic functionality: they just exist and are part of their respective classes without any formal element to hint at their existence. But there are more traditional ways to access intrinsic functionality, with the main one being the Intrinsic class. This is a class only with static methods, offering a wide-set of common functionality. And this functionality is accessed using normal methods in a normal class, so it becomes easier to gain awareness of what is available.

Determining minimum and maximum values is an example of such functionality. Intrinsic.Min will return the minimum of the provided parameters. Instead of using:

5.GetMin(9);

…you now use the much more natural syntax of:

Intrinsic.Min(5, 9);

This approach has multiple advantages, beyond the already mentioned more natural syntax. It solves the problem of having to repeat the body of GetMin in each class. Intrinsic.Min is now a template method, so it only needs to be defined once and works with all types. Additionally, while some methods inside the Intrinsic class don’t have a visible implementation, Min does and it can be useful to see what it does. And finally, this method, and its counterpart, Max, is designed to work not on individual values, but value providers, so you will be able to pass it any combination of containers.

With this first change, we eliminated two methods not only from Int, but from all comparable value types from the library. What about Clamp? First, let us ignore Intrinsic and focus on naming conventions. During the development of the library we introduced a convention related to actions that can be applied to instances: these actions are implemented using verbs. A verb in its base form describes a mutating action, one that modifies the instance. A few examples: Add, Insert, Delete, Clamp, Sort and so on. Naturally, these methods can’t be called on const instances. Verbs using the past tense do the same thing as the base form action, but do not modify the instance, instead returning a new instance and leaving the original unchanged. The same examples: Added, Inserted, Deleted, Clamped, Sorted and so on. This is just a convention and there is no obligation for third-parties to respect it. So using this convention, in our Int class, we should have two methods. If the variable a is an Int with value 5, a.Clamp(10, 100); would modify a to be clamped to the range of 10-100, in this case making it have the value of 10, while a.Clamped(10, 100); would leave a as 5, but return 10. Additionally, a.Clamp(10, 100); and a = a.Clamped(10, 100); are equivalent. This holds as a general rule, with foo.Bar(); being equivalent to foo = foo.Bared();, but the former may or may not be more efficient, depending on what operator = does and the quality of the compiler’s optimizer.

So using this convention, our second version of Int would have two methods instead of one: Clamp and Clamped. Which leaves us with the same problem: two methods which are almost always the same, having to be copied over to a bunch of classes. Intrinsic solves this again, by having two methods, Clamp and Clamped:

class TestClamp {
	def @main() {
		val a = 5;
		Intrinsic.Clamp(a, 10, 100);
		
		System.Out << a << " " << Intrinsic.Clamped(-5, -100, -10) << "\n";
	}
}

10 -10

This solves the problem, but there is more to it. 0.GetMax(-1) wasn’t the most natural syntax, but a.Clamp(min, max) is. In some cases we want a class to have a “clamp” method independently from the Intrinsic class. We could just add the method to such classes, ignoring code repeat. But there is a better method: method aliasing! In Z2, a method can be an alias for another method. Their parameters must be compatible and there are a few other requirements too which I won’t describe right now. Luckily for us, parameter compatibility includes the case where a non-static method of a class Foo is an alias of a static method from another class with N + 1 parameters, where the first parameter is of class Foo. Using method aliasing, we can add only the signature of the method to classes and let the compiler forward the call to another method, with zero performance overhead. Using this, we can add the following two methods to Int:

	def Clamp(min: Int, max: Int); Intrinsic.Clamp;
	
	def Clamped(min: Int, max: Int): Int; const Intrinsic.Clamped;

Int.Clamp(Int, Int) is now an alias for Intrinsic.Clamp(ref Int, Int, Int).

Int.Clamped(Int, Int) is now an alias for Intrinsic.Clamped(const Int, Int, Int).

I shall talk more about parameters in a future post, including how ref works, but for now it is important to understand that these are just aliases. The parameters match up, are compatible, and when you call Int.Clamp, the compiler actually generates code for a call to Intrinsic.Clamp. An alias is just a formal way to say “hey, I’d like to add a new method to an interface for some purpose which leaves the heavy lifting to someone else”. The method names do not need to be identical. They are identical here because it makes sense, but the alias name can be anything.

Now it is time to see our second version of the Int class:

namespace sys.core.lang;

class Int {
	const Zero: Int = 0;
	const One: Int = 1;
	const Default: Int = Zero;

	const Min: Int = -2'147'483'648;
	const Max: Int = 2'147'483'647;

	const IsSigned = true;
	const IsInteger = true;

	const MaxDigitsLow = 9;
	const MaxDigitsHigh = 10;

	property Abs: Int {
		return this > 0 ? this : -this;
	}

	property Sqr: Int {
		return this * this;
	}

	property Sqrt: Int {
		return Int{Double{this}.Sqrt};
	}

	property Floor: Int {
		return this;
	}

	property Ceil: Int {
		return this;
	}

	property Round: Int {
		return this;
	}

	def Clamp(min: Int, max: Int); Intrinsic.Clamp;
	
	def Clamped(min: Int, max: Int): Int; const Intrinsic.Clamped;
	
#region Saturation

	this Saturated(value: Int) {
		this = value;
	}

	this Saturated(value: DWord) {
		this = value > DWord{Max} ? Max : Int{value};
	}

	this Saturated(value: Long) {
		if (value > Max)
			this = Max;
		else if (value < Min)
			this = Min;
		else
			this = Int{value};
	}

	this Saturated(value: QWord) {
		this = value > QWord{Max} ? Max : Int{value};
	}

	this Saturated(value: Double) {
		if (value > Max)
			this = Max;
		else if (value < Min)
			this = Min;
		else
			this = Int{value};
	}

#endregion
}

We can see the changes from version 1: GetMin and GetMax are gone, replaced with calls to Intrinsic when needed, Clamp is now an alias to Intinisc.Clamp and we added Clamped. Additionally, a new section has been added to the class that handles saturation. Z2 as a systems programming language is designed to have rich and performant numerical processing capabilities. Things like clamping and saturation are considered common tasks and ass such receive full support. Saturation is a lengthy section that will get repeated in multiple classes, but here we consider it not to be a problem since third party value types will generally not offer generic saturation support and us covering the basic numerical types is sufficient. This section is surrounded by the #region/#endregion tags, a purely syntactical construct that allows you to create logically related blocks in code as a tool to facilitate organizing.

And finally, this version 2 also has one additional change from what it could do in version 1, but this change is remarkable not by adding something, but by omitting something that was planned to be added but was ultimately not. Z2 supports bit rotation, not just bit shifting. This is supported with the Intrinsic class and some time ago, we had two aliases in Int for this:

	def GetRol(bits: DWord): Int; const Intrinsic.Rol32;

	def GetRor(bits: DWord): Int; const Intrinsic.Ror32;

During the design process it was decided that bit rotations are useful enough to be fully supported but not common enough to have an alias for them in Int, so these two aliases were eliminated from all core numerical types. If you need bit rotation, you can use Rol8/Rol16/Rol32/Rol64 and Ror8/Ror16/Ror32/Ror64 directly from Intrinsic.

This second version of Int, together with Double and Intrinsic have been committed to a branch in GitHub. The main branch also has some associated UT.

Next time we’ll investigate how this generic solution for clamping and other operations works with third party classes.