inko-lang / inko

A language for building concurrent software with confidence
http://inko-lang.org/
Mozilla Public License 2.0
827 stars 38 forks source link

Find a way to reintroduce Self types without the complexity #643

Open yorickpeterse opened 8 months ago

yorickpeterse commented 8 months ago

Description

Inko used to have support for Self types, but this was removed due to the complexity and bugs this introduced to the type system. This change however makes certain patterns difficult or even impossible to implement. The trait std.clone.Clone is a good example of this. Ideally, the trait is defined as follows:

trait pub Clone {
  fn pub clone -> Self
}

This way if you use e.g. String.clone or Foo.clone you get a String or Foo back. This currently isn't possible, so we have to use generics instead:

trait pub Clone[T] {
  fn pub clone -> T
}

In this particular case it works, but results in a bit of type boilerplate/redundancy:

impl Clone[Array[T]] for Array {
  fn pub clone -> Array[T] {
    ...
  }
}

Versus just the following:

impl Clone for Array {
  fn pub clone -> Array[T] {
    ...
  }
}

While this case isn't too difficult, in other cases it can get really messy, requiring extensive workarounds.

I would like to explore options to reintroduce Self types in some reduced capacity, ideally without having to consider them in every place a type may occur. This means having to restrict Self types to trait definitions, such that we can more easily substitute them with real types.

Related work

No response

yorickpeterse commented 8 months ago

Another example is the Equal trait, currently defined like so:

trait Equal[T: Equal[T]] {
  fn ==(other: ref T) -> Bool
}

This requires implementations like so:

impl Equal[Array[T]] for Array { ... }

In contrast, with Self types we can reduce this (back) to:

trait Equal {
  fn ==(other: ref Self) -> Bool
}

impl Equal for Array { ... }

This does however introduce a new challenge: if we cast a type to Equal, we can do x == y where both x and y are typed as Equal, but the implementations aren't compatible (i.e. x is actually String and y is Float). To make this safe, we'd have to disallow casting to traits if any of its methods use Self in their arguments.

klieth commented 8 months ago

I agree with your Clone example, but are you sure your conclusions about the semantics for Equal are what you want? It's fairly common in many languages these days to use the == operator between two objects that are different types, and would require non-trait based solutions for common situations, such as testing equality between a ByteArray and Array[Int], or ByteArray and String.

For example, this would trivially make sense to test equality:

import std.fmt.(fmt)
import std.stdio.STDOUT

class async Main {
  fn async main {
    let a = [0, 0]
    let b = ByteArray.filled(with: 0, times: 2)

    STDOUT.new.print(fmt(a == b))
    # /tmp/main.inko:9:32 error(invalid-type): expected a value of type 'ref Array[T]', found 'ref ByteArray'
  }
}

(I realize that this brings up a larger discussion about the way that inko handles implementing traits on types and that this currently doesn't work for other reasons. Attempting to implement Equal a second time for Array (say, impl Equal[ByteArray] for Array) fails with the error that the trait Equal is already implemented for Array. I would also recommend changing that behavior, but that's probably a separate ticket.)

The way that Rust handles this is by using the Self trait as a default value for the type parameter -- trait PartialEq<Rhs = Self> -- allowing you to implement impl PartialEq for T. Is there a reason that sort of feature wouldn't be the right direction to consider?

yorickpeterse commented 8 months ago

@klieth To support what you're referring to, we'd need to add support for overloading methods based on the traits they originate from. This is something I want to avoid due to the complexity it brings with it.

Similarly, default type parameter values is something I also want to avoid, again to keep the compiler and type system complexity at a level that I'm comfortable with.

At some point in the future that may change, but it won't be the case for at least a few more years (if ever).