cobalt-language / cobalt-lang

A low-level, compiled programming language made as an alternative to C++ and Rust.
https://cobalt-language.github.io
MIT License
7 stars 0 forks source link

Add traits #125

Open treemcgee42 opened 11 months ago

treemcgee42 commented 11 months ago

Marker traits

A trait with no functions can be used as a marker, and is auto-implemented in the following way:

trait trait_1;

type Type1 = i32;
impl Type1: trait_1;

Trait arithmetic

A few operations can be performed on traits to compare them and create new ones.

Addition

The result of adding two traits is a trait which requires its summand traits to be implemented for a type before it itself can be implemented.

trait trait_a;
trait trait_b;

trait compound_trait = trait_a + trait_b;

Less than

We say that trait_a < trait_b if implementing trait_b would be sufficient but not necessary to implement trait_a.

Note that simply implementing trait_b on a type does not automatically implement trait_a on that type. However, after implementing trait_b, one can automatically implement trait_a:

trait trait_a :: {
  fn foo();
};
trait trait_b :: {
  fn foo();
  fn bar();
};

type Type1 = i32;
impl Type1: trait_b :: {
  fn foo() = {
    # ...
  };

  fn bar() = {
    # ...
  };
};

# automatic implementation
impl Type1: trait_a;

Equality

trait_c is considered equal to trait_d if and only if implementing trait_c is necessary and sufficient to implement trait_d. For example, taking the traits as defined above,

trait trait_c = trait_b;
trait trait_d = trait_a + trait_b;

The reason trait_c == trait_d is because trait_a < trait_b, so trait_a + trait_b <= trait_b + trait_b == trait_b. But also trait_b <= trait_a + trait_b (trait addition has the same semantics as adding positive integers).  

Automatic implementation

Scoping

Dynamic dispatch

There are situations where calling trait functions will result in dynamic dispatch, but this is not always the case. If the only thing known about an object is that it implements a trait, then calling a trait function on that object will result in dynamic dispatch. However, if the type of the object is known at compile time when calling a trait function, the compiler can directly include a call to the specific implementation of the trait function.

In the following few examples, we will use the following trait, and type which implements that trait:

trait trait_1 :: {
  fn trait_fn();
};

type Type1 = i32;
impl Type1: trait_1 :: {
  fn trait_fn() = {
    3i32
  };
};

This is an example of where dynamic dispatch occurs.

# Box syntax tbd
fn foo(): Box<trait_1> = {
  # ...
};

fn main(): i32 = {
  let my_obj = foo();
  my_obj.trait_fn()
}

This is an example of where dynamic dispatch does not occur.

fn bar(x: impl trait_1): i32 = {
  x.trait_fn()
};

fn main(): i32 = {
  let my_obj = 3i32 :? Type1;
  bar(my_obj)
};

Compile-time enforcement

It's useful to ensure at compile-time that dynamic dispatch does not occur. For instance, dynamic dispatch on hot paths can have a significant impact on performance.

To that end, we propose the following syntax:

fn bar(x: @kact impl trait_1): i32 = {
  # ...
};
(`@kact` stands for "known at compile time").
matt-cornell commented 11 months ago

trait_c is considered equal to trait_d if and only if implementing trait_c is necessary and sufficient to implement trait_d. For example, taking the traits as defined above,

trait trait_c = trait_b;
trait trait_d = trait_a + trait_b;

The reason trait_c == trait_d is because trait_a < trait_b, so trait_a + trait_b <= trait_b + trait_b == trait_b. But also trait_b <= trait_a + trait_b (trait addition has the same semantics as adding positive integers).

Should these compare as equal? Like how type defines a new type while const defines an alias, shouldn't trait define a new trait (that needs to be chosen to be implemented), while const simply aliases a collection?