Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
yorickpeterse opened this issue Nov 12, 2023 · 3 comments
Open

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

yorickpeterse opened this issue Nov 12, 2023 · 3 comments
Labels
compiler Changes related to the compiler feature New things to add to Inko, such as a new standard library module

Comments

@yorickpeterse
Copy link
Collaborator

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 yorickpeterse added feature New things to add to Inko, such as a new standard library module compiler Changes related to the compiler labels Nov 12, 2023
@yorickpeterse
Copy link
Collaborator Author

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
Copy link

klieth commented Nov 19, 2023

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
Copy link
Collaborator Author

yorickpeterse commented Nov 20, 2023

@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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
compiler Changes related to the compiler feature New things to add to Inko, such as a new standard library module
Projects
None yet
Development

No branches or pull requests

2 participants