-
Notifications
You must be signed in to change notification settings - Fork 21
Description
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"