EzRust thoughts

Hypothesis: Rust is "hard to learn" because experimenting is hard. Getting code to work requires a lot of thinking and "in-head" design. There are also many concepts needed to get started.

People ask to "disable the borrow checker".

What if we can create an EZ rust mode that is used to introduce users to Rust. It is a slightly different language, but is compatible with Rust itself. EZ mode is enabled / disabled at the crate level but ez-mode crates can call non-ez-mode crates and vice versa.

Ez-mode is primarily focused on app-developers (CLI, net services, etc). Ez-mode will not be zero-cost and will come with some runtime. Ez-mode will not use a GC as GCs are quite intrusive.

Infrastructure libraries (e.g. Tokio, Hyper, ) will still be implemented in Rust for performance. The idea is that the application layer business logic is more "scripting" and can have some level of runtime overhead without a measurable total performance impact.

Preample, what makes Rust fast?

Generally, Rust is overall fast and overall performance tend to be avoiding "death by a thousand cuts".

Features

  • Inline layout.
  • Monomorphization

Make borrow checking less prominent.

Inspired by: Revisiting a 'smaller rust'.

Goal: remove borrow-checking related challenges from the ez-path. They can still be there, but they are not required to be productive. Users can explore ideas without having to think-ahead.

Withoutboats proposes to decouple Copy and memcpy in order to support things like "persistent data structures" that feel natural. Example, instead of having &str, &String, &mut String, and String, there would just be String which would be Copy and could be passed by value.

struct Person { name: String, } fn println(what: String) { ... } impl Person { fn print_name(&self) { print(self.name); } }

In this example, String is hypothetically backed by a persistent data structure. This could also make slices possible.

fn print_first_few_chars(long_string: String) { println(long_string[..30]); }

What about user-defined structs? How do we remove borrowing from the happy path?

struct Pet { name: String, } struct Person { name: String, pet: Pet, } fn print_pet_name(pet: Pet) { println(pet.name); } impl Person { fn new(name: String, pet: Pet) -> Person { Person { name: String, pet: Pet, } } fn print_pet(&self) { print_pet_name(self.pet); } } // Implicit Copy definition impl Copy for Pet { fn copy(&self) -> Pet { // How does this work? } }

A naive implementation could essentially be clone, but that has problems with managing "resources" (as boats calls them) as well as letting functions mutate values.

Implicit Rc / Cell

Forgetting about thread-safety for a bit, what if structs had an implicit Rc and each field was implicitly Cell. It would be possible then to update fields. Example:

struct Person { pet: Pet, } struct Pet { age: u32, } fn inc_pet_age(pet: Pet) { pet.age += 1; } impl Person { fn new(pet: Pet) -> Pet { Pet { pet } } fn year_passes(&self) { inc_pet_age(self.pet); } }

Note that there is no mut mentioned here. Pet has an implicit Rc and each field has an implicit Cell. The Copy implementation of Pet increments the ref count and updating the age field is done using get and set semantics.

What about Send and Sync?

By using implicit Rc / Cell, we are forcing types to be !Send. How does this work? We can't give up concurrency.

Embrace light-weight tasks + message passing.

Instead of supporting arbitrary concurrency strategies, EzRust forces a light-weight task + message passing pattern (similar to Go / erlang).

In this pattern, logic is pinned to a "task". There is no shared data between tasks. Instead, tasks communicate by message passing.

But, how do we define a message if all structs imply Rc? We have different categories of types. One for "logic" and one for "data" / message passing.

// `object` have implicit `Rc` / `Cell` and can // have method implementations object Person { name: String, age: u32, pet: Pet, } object Pet { name: String, age: u32, } // These are more like Rust structs and imply // `Send` / `Sync`. However, they cannot have // method implementations and are intended to // be pure data (this is an arbitrary decision). data CreatureVitals { name: String, age: u32, } // Spawn a task to maintain a list of creatures, // odds are, since we are embracing message passing // we will want to have some first-class "actor" like // primitives let handle = spawn(actor { // Why not have syntax for maps while we are at it let vitals = {}; loop { let CreatureVitals { name, age } = recv(); vitals[name] = age; } }); fn register_person(person: Person) { // register person handle.send(CreatureVitals { name: person.name, age: person.age, }); // register pet handle.set(CreatureVitals { name: person.pet.name, age: person.pet.age, }); }

Data types

  • No methods.
    • This is kind of arbitrary.
  • Fields default to same viz as the type?
// With Rust: this is annoying pub(crate) Foo { pub(crate) one: String, pub(crate) two: String, }

RAII and ownership is still important

For resource types. E.g. a File should close on drop and guards (e.g. mutex guard) is still a useful pattern.

Opt-in borrow checker

TODO: But, in general, the borrow checker still works as "normal", it is just needed less often.

Traits?

  • Can we keep trait bounds simple?
  • Trait objects should be nice to use.

Gradual dynamic typing?

Another crazy idea but how plausible would it be to have an Object type that does dynamic dispatch and runtime typing? Think of this as the opposite of Typescript. Instead of doing gradual typing, we do gradual un-typing?

The idea would be to minimize the amount of work one has to do when exploring / experimenting and types could be added later (a lint could be used to disable untyping).

Maybe this could be a debug mode only feature.

Single impl block

pub struct TimeoutService<T: Service> { delay: Duration, inner: T, pub fn new(service: T, delay: Duration) -> TimeoutService<T> { TimeoutService { delay, inner, } } impl Service { type Request = T::Request; type Response = TimeoutResult<T::Response>; async fn call(&self, request: T::Request) -> TimeoutResult<T::Response> { time::timeout(async || { self.inner.call(request) }) } } }