Whiley / RFCs

Request for Comment (RFC) proposals for substantial changes to the Whiley language.
3 stars 2 forks source link

Traits #73

Open DavePearce opened 4 years ago

DavePearce commented 4 years ago

(see also #68)

Another important question is how we go about implementing traits in Whiley. This is already starting to happen in an ad-hoc fashion using open records. For example, the following type from std::collections::iterator:

public type Iterator<T> is {
    // Get value at iterator position.
    function get() -> Option<T>,
    // Advance the iterator one.
    function next() -> Iterator<T>,
    //
    ...
}

The real question is whether or not we need additional machinery to support traits or not? Some thoughts:

DavePearce commented 4 years ago

In Rust, there are two aspects to implementing a trait: the interface and the implementation. For example, an interface:

trait Write {
  fn write(&mut self, buf: &[u8]) -> Result<usize>,
  ...
}

And the implementation for some type:

struct SomeWriter { ... }

impl Write for SomeWriter {
  fn write(&mut self, buf: &[u8]) -> Result<usize> {
    ...
  }
}

In Whiley, we can represent the interface quite easily like this:

public type Write is {
  method write(byte[] buf) -> uint,
  ...
}

We can then define an implementation like so:

public function to_writer(SomeWriter w) -> Write:
  return {
    write: &(byte[] buf -> write(w,buf))
  }

private method write(SomeWriter self, byte[] buf) -> uint:
   ...

Question) what are the pros/cons of this versus the Rust approach?

  1. (Efficiency) This approach consumes more space because we create a thunk for each lambda to bind self.
  2. (Efficiency) When inlining generic methods, we presumably won't be able to know what functions are actually called, resulting in more virtual method call.s
  3. (Composability) For types which implement multiple traits, it's a bit icky. In particular, we can't add traits after the fact. Rust's approach allows the compiler to compose traits at the point of use, whilst the above requires the user do this manually.
DavePearce commented 3 years ago

The proposed approach is via an implements clause. Thus, it would be:

type Box<T> is { &T data }
implements ::drop<T>

This indicates that Box<T> implements the drop trait. This means there must be an implementation of that trait somewhere in the same source file.

But, how do we declare traits? Example syntax:

trait Reader is:
   function read(Reader this) -> (byte[] bytes)

type Reader is trait:
   function read(Reader this) -> (byte[] bytes)

type Reader is:
   function read(Reader this)

type Integral is (int x)
provides function add(int this)

type ToString<T> 
has function to_string(T) -> ascii::string

type ToString<T> traits:
   function to_string(T) -> ascii::string

type ToString<T>
implements function to_string(T) -> ascii::string

(then the concept of a trait is just a type, so we don't need to distinguish bounds in any way)

  1. (Resolution) How can we intermix trait resolution with normal resolution? Basically, whenever a named type is imported (either directly or indirectly) all trait methods are imported as well. That will work. The only real question is whether we want some kind of priority for trait methods over other methods. Maybe its not necessary.
  2. (Ambiguity) Imagine a trait method with a member function f(Reader,Reader). How do we disambiguate? Potentially, as highlighted above, the this parameter could take on a special status for disambiguation. We could still have a situation where we have two trait methods which apply: function f<T>(Reader x, T y) and function f<T>(T x, OtherTrait y). I suppose this would just return an ambiguous invocation.
  3. (State) Can a trait impose some constraints on the type implementing it? For example, a trait implemented by an integer type constraints its value in some way?
  4. (Primitives) How do we defined traits for primitive types?
  5. (Objects) How do trait objects work? Realistically, this requires some kind of bound. See how Rust handles this.
  6. (Size) Traits defined on a concrete type can be instantiated, whilst those which are purely abstract cannot.
DavePearce commented 3 years ago

Ok, here's an interesting syntax:

type ToString<T>
implements function(T this) -> ascii::string

type Point is { int x, int y }
extends ToString<Point>

Here, every nominal type can support one or more traits.

Q) Should we even require a trait to be explicitly implemented by a type? Or perhaps we can just require a cast? To be fair, aliases can help us here.

DavePearce commented 3 years ago

Right, so aliases should not be able to add traits me thinks. Otherwise they are more than just aliases. Furthermore we need explicit casts to check the trait requirements?