Thoughts on generics in YAFL

I have had to take a break from YAFL development for the last month due to work commitments. For my own sanity, I have a clear priority ladder of family, work, hobbies, in that order. But now things are starting to lighten up, the clouds are parting, and I need to start collecting my thoughts again.

Before I go into the generics discussion I had one small thought about constructors. Currently I have gone for an approach similar to Kotlin, where the class is effectively also a function of the same name, which is the constructor. I want to move that to be a static member function named ‘new’ instead. For me it just provides more clarity, but so far I have avoided supporting static member functions. I think I still will avoid them, but with caveat that’ll become clear as we dive into generics.

The model I want to follow has its roots in Haskell and Rust, which have similar concepts referenced by the ‘class’ and ‘trait’ keywords respectively. The basic idea is that you can declare a set of well defined functions in a named group, and a type is said to be generically compatible with that named function group if a specialised set of implementations is available.

I’ll take that concept, but not exactly as defined in these languages. In Rust for example the ‘trait’ concept is conflated with the ‘interface’ concept that can be found in Java/Kotlin/C#, which makes sense for the Rust memory model, but is harder to fit into a languages with that hide memory management from the programmer like Java/Python etc. That aside though, I do have issue with the idea that the same named thing can be thought of as a type in the strict sense but also as a generic constraint. These, to me, are substantially different ideas.

YAFL already has ‘interface’ and ‘class’ keywords that behave in a similar way to Java/Kotlin/C#, and I am happy with that. I don’t want to force anything in there, so the generics idea will be kept separate, at least for now whilst we get this first implementation sorted.

Now it might be best to show some ideas, and explain what I am aiming for. The syntax will borrow from Rust and Kotlin:

trait Numeric<T> {
    let T::MAX: T
    let T::MIN: T
    fun `+`(l: T, r: T): T
    fun `-`(l: T, r: T): T
}

impl Numeric<Int32> {
    let Int32::MAX = 2147483647
    let Int32::MIN = -2147483648
    fun `+`(l: Int32, r: Int32): Int32 => someAddThingy(l, r)
    fun `-`(l: Int32, r: Int32): Int32 => someSubThingy(l, r)
}

fun processNumber<T>(a: T, b: T): T where Numeric<T> => a + T::MAX

‘::’ means scoped by. It’s a syntax used in Rust and C++ that I like, and used for module scoping. Here I propose to use it for type scoping as well, so we have MAX scoped by the type T. In the function we require that the type T complies with the Numeric trait, which it does. The function can be proven to be correct for any T that has an implementation of Numeric.

Anyone familiar with Rust will be looking at that syntax with furrowed eyebrows, wondering why I put the T in angle brackets next to the trait name. It’s because this isn’t about one generic type. Try this on for size:

trait Convertible<X, Y> {
    fun X::new(r: Y): X
}

impl Convertible<Int32, String> {
    fun Int32::new(r: String): Int32 => parseToInt(r)
}

fun addToString<X>(a: X, b: String): X where Convertible<X, String>, Numeric<X> => a + X::new(b)

That’s nice. We’ve effectively added a new constructor to ‘Int32’ that takes string. We’ve required that X is convertible from ‘String’ and that it’s a numeric type so that we can add. It’s all built on the idea of a trait that defines the relationship between multiple types.

Back to my opening statement, I don’t want to support static members on classes. This trait syntax clearly has done something semantically similar, so we’ll utilise that. The primary constructor of a class will be defined as ‘new’ in an anonymous impl block, and all other constructors can be explicitly declared by the programmer in the same way like so:

impl {
    fun MyClass::new(a: Int32, b: Float32): MyClass => MyClass::new(1, 2, 3, a, b)
}

Looks like rust, and as I said before it borrows a lot, un-ashamedly so, as Rust is brilliant. But I also borrow much from Kotlin, C# and even a little from Python. It’s a chimera.

There is one more thing though. YAFL tries to infer a lot in order to reduce code bloat, so where possible I’d like for it to avoid the need to declare trait constraints on generic parameters. Ideally, it should note the operations used in a function body, and locate trait definitions in the imported namespaces that uniquely match. If found, we can imply the constraints. So:

fun addToString<X>(a: X, b: String) => a + X::new(b)

Implicitly, we can see that X is expected to have a function ‘new’ that takes a parameter of type String. In our imports there is a trait defined for this, uniquely. The result X is added to ‘a’, another X. There is another trait defined for maths operations, so we can imply that as the next constraint. Ultimately, I believe we can remove much of the boiler plate, whilst retaining good code clarity.

I’ll stop here, and mull over this a bit. So far though, this feels good, and keeps the concepts simple but powerful.

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s