jfecher
Joined 9 years ago
Joined 9 years ago
Ante shares parts of its approach with Valen - both having been based on the group borrowing concept. I'm in contact with its author and we share ideas every now and then. Ante is mentioned at the end of that article too. There are actually several upcoming languages using group borrowing (or something similar) as a base now - Ante, Valen, Carbon, Zeta, Fang, Mojo, and likely more.
There are a few parts of this overall approach that'd be difficult to fix in Rust without making it an entirely different language I think. The first would be separation of use vs reference, so users can pass around mutable references without the language thinking they may be mutated, and the second would be changing Rust's core semantics to allow shared mutability in general. With internal mutability like GhostCell this is allowed via UnsafeCell but I'd imagine making every reference UnsafeCell by default wouldn't be optimal and quite a few internal changes would be required.
As for cycles, `Rc t` in Ante isn't magic and when used in cycles it will leak. Ante does not use region inference, just the related, derived field of borrow-checking. The family tree there roughly resembles MLKit -> Cyclone -> Rust -> Ante in that regard.
When borrowing the inner element of an `Rc t`, the type system ensures that the resulting `ref t` cannot be dropped while it is still held. In particular, dereferencing an `Rc t` requires a unique value, but only gives you a shared value to the element: `Rc.as_mut: fn (uniq Rc t) -> mut t`. In practice, this means if you only have a shared ref to an Rc but need a reference to the element inside, you either need to clone the outer Rc (guaranteeing it won't be dropped while the borrow is alive), or use the local uniqueness conversion and avoid using any possible aliases while using the reference.
Ante's approach manages to blend a similar scheme for safe, shared mutability with Rc. There are some examples on the most recent blog post on its website of it. IMO combined with its shared types it emulates high-level GC'd code very well.
- You wouldn't have to edit the body of the function to thread through the parameter. - The `can Use Strings` part can be inferred (and in Ante's case, it is my goal to have the compiler write in inferred types for you so that top-level definitions can be inferred if desired but still annotated when committed for code review). - Most notably, the `can Use Strings` can be included in a type alias. You could have an alias `MyEffects = can Use Strings, Throw FooError`, etc for the effects commonly used in your program. If your state type is used pervasively throughout, this could be a good option. When you have such an alias it also means you'd just be editing the alias rather than every function individually.
Generally though, while I think the passing around of state through effects can be useful it isn't the most convincing use of effects. I mention it more for "here's another benefit they can have" rather than "here's this amazing reason you should definitely use them for"
Not quite. setjmp/lonjmp as they exist in C at least can jump up the call stack but not back down. I mention this at the end of the article but each language implements algebraic effects differently, and efficiency has improved in recent years. Languages can also optimize the effect differently based on how the handler is defined:
- Handlers which are tail-resumptive can implement the effect as a normal closure call.
- Handlers which don't call resume can be implemented as an exception or just return an error value at every step until the function exits the handler.
- Handlers which perform work after resume is called (e.g. `| my_effect x -> foo (); resume (); bar ()` can be implemented with e.g. segmented call stacks.
- Handlers where resume is called multiple times need an equivalent of a delimited continuation.
Another way to implement these generally is to transform the effects into monads. For any set of effects you can translate it into a monad transformer where each effect is its own monad, or the free monad can be used as well. The cost in this approach is often from boxing closures passed to the bind function.
Koka has its own approach where it translates effects to capability passing then bubbles them up to the handler (returns an error value until it gets to the handler).
With just a few restrictions you can even specialize effects & handlers out of the program completely. This is what Effekt does.
There really are a lot of options here. I have some links at the end of the article in the foot notes on papers for Koka and Effekt that implement the approaches above if you're interested.
Multi-threading in general is made safe by separate mechanisms which don't permit shared mutability, but there's little reason to restrict it on a single thread when it can be verified to be safe. You still cannot mutate a collection while iterating over it for example, but you can easily mutate disjoint parts of it which is something I commonly encounter in Rust.