Skip to content

Draft: Traits Proposal #70

@cschram

Description

@cschram

This is an incomplete draft.

Synopsis

Otter, at it's core, inherits DNA from both Rust and Python. It's compiled, typed nature, along with many of its semantics are inspired by or come directly from Rust, while it's syntax and simplicity is (perhaps superficially) inspired by Python. One design decision that has not yet been made is how to handle code sharing and polymorphism. Python handles this the classic OOP way through classes and inheritance, while Rust uses composition with traits. This leaves us at a crossroads with Otter for which direction, if either, should be taken.

Traits bring some notable advantages over inheritance. Many of the pitfalls of inheritance are avoided, such as multiple inheritance, complex hierarchies, and the diamond problem. Composition usually represents what the programmer intends more accurately than inheritance, due to it conceptually being about what a type can do rather than what it is.

Rust's particularly sophisticated trait system introduces additional benefits, such as blanket implementations and derived implementations. However, this comes at the cost of complexity. Rust's strict constraints on ownership and lifetimes, along with some of its other design decisions, impose design decisions on its trait system that add additional complexity, such as Sized issues with trait objects.

Otters goal for transparent interoperability with Rust means Rust's trait system plays a strong role in how we design around this problem. Designing our type system to represent as much of Rusts helps mitigate invalid usage of Rust interfaces in Otter. The lack of Rust's constraints means Otter can have a far simpler trait system, while still attempting to cover much of the expected interface of Rust traits. This proposal aims to outline a trait system that covers the most important aspects of general type safety while maintaining a relatively safe compatibility with Rust code's expectations.

Design

Syntax

Trait Definitions

Trait definitions should be familiar to those who have used Rust or other languages that feature traits.

trait Iterator<T>:
    fn next(self) -> Option<T>

Default implementations are also defined as you would expect:

trait Greeter:
    fn greeting(self) -> str

    fn greet(self):
        println(self.greeting())

Trait methods use the self as a first parameter idiom used in Python and Rust. This allows for method disambiguation described later, and also opens up the possibility of Rust-like associated functions (not part of this proposal).

Trait Implementations

Trait implementations also follows in the Rust convention, with implementation blocks being separate from struct definitions. This decoupling allows any module to implement a trait for any type.

struct List<T>:
    head: ListNode<T>
    tail: ListNode<T>

struct ListNode<T>:
    value: T
    prev: Option<ListNode<T>>
    next: Option<ListNode<T>>

struct LinkedListIterator<T>:
    current: ListNode<T>

impl<T> Iterator<T> for LinkedListIterator<T>:
    fn next(self) -> Option<T>
        match self.current:
            case Option.Some(node):
                return Option.Some(node.value)
            case Option.None:
                return None

One notable breaking change to the current Otter syntax and semantics, is defining struct and enum methods through "bare" implementation blocks not associated with a trait. Methods on structs will no longer be defined directly on a struct.

# No longer correct!
# struct List<T>:
#     head: ListNode<T>
#     tail: ListNode<T>
#
#     fn iter(self) -> LinkedListIterator<T>:
#         return LinkedListIterator(current = self.head)

impl List<T>:
    pub fn iter(self) -> LinkedListIterator<T>:
        return LinkedListIterator(current = self.head)

Semantics

TODO

Trait References

Passing and holding a reference to an object that implements a trait is intended to be simple, as opposed to the more involved syntax and semantics of traits in Rust. An object that implements a trait can simply be typed as that trait.

struct Person:
    name: str
    age: int

impl Greeter for Person:
    fn greeting(self) -> str:
        return f"Hello, my name is {self.name} and I am {self.age} years old."

fn main():
    let alice = Person(name = "Alice", age = 30)
    alice.greet()

Method Disambiguation

A common edge case in trait implementations is implementing multiple traits which use methods of the same name. In Rust, specifying which method should be called is called Fully Qualified syntax and requires casting a value to the trait. While Rust has it's own reasons for this syntax, in Otter the self parameter allows for a more straightforward syntax, calling the method on the trait itself and passing the value directly as self, as you would in Python.

trait IntThing:
    fn get_thing(self) -> int

trait BoolThing:
    fn get_thing(self) -> bool

struct MyThing:
    int_val: int
    bool_val: bool

impl MyThing:
    fn get_thing(self) -> str:
        return f"{self.int_val} {self.bool_val}"

impl IntThing for MyThing:
    fn get_thing(self) -> int:
        return self.int_val

impl BoolThing for MyThing:
    fn get_thing(self) -> bool:
        return self.bool_val

fn main():
    let thing = MyThing(int_val = 42, bool_val = true)
    print(thing.get_thing()) # "42 true"
    print(IntThing.get_thing(thing)) # "42"
    print(BoolThing.get_thing(thing)) # "true"

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions