Writing a Simple User Defined Type System in Kotlin — Part 2.5: Advanced Type Features

James
James
Oct 21 · 5 min read

This is part of a series about building a User Defined Type system using Kotlin. If you haven’t read the previous parts (Part 1 and Part 2), I recommend catching up before you do.

The Type system we defined is powerful and flexible. Unlike some UDT systems, it allows a type to be defined in code as easily as from user data, and to be used almost like a Kotlin type, as long as all you want to do is move data around. So we’re going to add some more features.

First, it would be nice to more easily access, from code, the properties of a UDT. It’s possible with the data class we’ve defined, but you’d have to iterate the list of properties, we’ll fix that with a similar utility to the one we used in the DType for its fields. As well as a map indexing the values, we’ve added a generic get method to the class, so we can get a typed value concisely.

We can define composite types with our system, but we have no concept of inheritance common to most class based type systems. This is not a problem in of itself, but it would be great if we could guarantee some fields, e.g. if a user could define the type, but we could ensure that certain fields always existed. For example, if we’re saving instances to a database, it would be great if we could ensure there is always an ID field. UDTs I’ve worked on in the past often have reserved identifiers, and hard code the ID field in, but we’re going to use our type system to build it.

Looking at our current type system, all a type provides is an identifier, name, and a list of fields. If we were to “subclass” a type, the only part we really want to keep is the fields, the new class will define the name and identifier.

We could take a DType and make a subclass method to add fields, a new name and a new identifier, but it is more usual to see that a Subclass extends a Class. Kotlin allows us to define new infix functions. Unfortunately we can’t define a :operator, nor can we use varargs in infix operators, but we can overload the function we define to take a single supertype, or a list. We can also define the function as an extension, allowing us to break it out into its own file so that the core type system file doesn’t grow any larger (this is a matter of preference, the core file is already larger than I usually allow source files to grow, but I think it’s important to keep all of the Dtype and Value core code together). We get something like this:

We can now extend a class like so

Creating a new pupil will accept positional arguments in the order specified first by the superclass, then by the subclass (or you can use kwargs to be explicit). So in this case we’ll create a pupil with arguments id, first name, last name, date of birth.

Looking at this code revealed a subtle bug that is present in the type system, but more obvious when you start inheriting classes: what happens if the same field is defined by multiple ancestors. Or even, if the field is defined multiple times in one DType block. Since we don’t reject it, we’ll create multiple properties in the same instance, which could, depending on if you use args or kwargs, match either definition of the identifier. We should prevent that. We’re also not terrible people, so we’re going to try to provide a useful error message about where the conflict occurs.

In DType, we add an init block to detect multiple definitions of the same field. We also rework the extends functions to detect and disallow clashes/overrides (we could allow specifying the same field twice if it was identical, but even that would make the invoke method ambiguous, so we’ve chosen to disallow it). Finally, we’ve rewritten the multiple superclass version of the extends function to work from right to left, and repeatedly call extends. This would be equivalent to A extends (B extends C) and will error the first time a clash is encountered. We could spend the time to make it point out every clash, but that’s overkill for what we expect to be fairly shallow inheritance hierarchies. (Incidentally, this is a very similar mechanism to Python’s multiple inheritance resolution)

We can now specify inheritance, which is great for hybrid User/System defined types (e.g. entities). It would be nice to have the counterpart to that: detecting if a DTypeValue is an instance of a DType. We have a few options for how to handle this.

We could approach this in a dynamic way, using the DTypeValue#get method and catching errors if the instance doesn’t have the field we try to access. Alternatively, we could test the type upfront. We’re going to take the second option, but making use of Kotlin’s features to make it a little more flexible.

The only thing we care about when testing types is if a particular set of fields exists on the instance. As fields are compared by the values of their properties (being data classes) the following implementation can check if the current value implements an interface:

That’s all there is to testing an implementation. We can now add DTypeValue extension methods to imitate methods on the DType. e.g. Adding a fullName method to the pupil type:

This can be cleaned up a bit more with a higher order function

There are various other ways to achieve this — we could implement a DTypeValue#send method similar to Ruby, define functions on the DType which could be called by name on the value — they’d have to accept the value as the first parameter, making them similar to Python, or we could even add function tables to instances like Lua does. But at that point we’ve diverged from the User-Defined Type system into a full, dynamic type system.

This concludes this section of the guide. We’ve moved from a User Defined Type that is just a structure for holding data, to one that can be a hybrid of system and user types, and can have methods defined on it. It resembles a dynamic/scripting language because all variables are of Type DTypeValue and checking the DType happens at runtime — but since the type is defined from a user it has to be a runtime check — the system exists in a world where the types only exist, and can change, during runtime. In building it, we’ve also seen a glimps behind the curtain at why dynamic languages such as Ruby, Lua and Python operate the way they do. In building our own system, even without looking to them for inspiration, our options have started to converge on the methods they use.

Image for post
Image for post

The Startup

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store