
Notes on a Smaller Rust - telotortium
https://boats.gitlab.io/blog/post/notes-on-a-smaller-rust/
======
rishav_sharan
As someone who recognizes himself as beginner programmer, i do not consider
the borrow checker as my major issue with the language. Heck, i haven't even
reached that issue.

Everytime that I have turned away from Rust, despite trying half a dozen times
is due to its super busy and alien looking syntax. I just can't parse the
code.

~~~
pdpi
Honestly, I don't see any intrinsic problems with Rust's syntax that makes it
unusually ugly.

Rather, I think the problem is that the syntax is 100% fine in isolation, but
has some issues when taken together with experience in other languages — when
you skim the code, some parts of it beg to be looked at like C++, while other
parts push you to read it like a functional language. Unsurprisingly, neither
fully works, which makes the experience jarring. Once you accept that Rust is
its own language with its own rules and stop trying to borrow (hah!) from your
experience with other languages, it feels much better.

~~~
sagichmal
> Honestly, I don't see any intrinsic problems with Rust's syntax that makes
> it unusually ugly.

You literally just got a user experience report claiming that it is.

~~~
munmaek
It's coming from someone who isn't comfortable with C or Java either.

There's no winning over everyone. Of course Rust's syntax will be completely
jarring if you're coming from Crystal or Ruby.

In my personal experience it's a little jarring at first and then it
completely goes away.

~~~
spease
I’m comfortable with C/++ and Java, to a greater extent than a lot of people
I’ve worked with. There are still things in Rust that are papercuts:

* The need to put :: before <> in expressions for generic parameters, but not in type signatures.

* Using :: instead of just . even though most of the time the naming convention will delineate the difference anyway

* Error handling with futures

* Self-referential structs

* Passing a shared pointer through multiple move closures

IMHO the frustration involved in these things is not typically worth being
more explicit. The :: thing may seem a bit pedantic, but it is simply more
physical effort (Shift-colon-colon vs dot) and creates more visual noise.

If/when all of the things above are addressed, I think Rust could give Python
a run for its money.

------
dwenzek
I like how the strengths of Rust are summarized by 3 points:

* Algebraic data types

* Resource acquisition is initialization

* Aliasable XOR mutable

I think it can help not only to design a Smaller Rust as discussed here, but
also to grasp the key design points of this language which is a bit daunting.

~~~
nudq
I like how these three points aren't just "strengths of Rust", but...

> the necessary components ... to make imperative programming work as a
> paradigm

You know, before Rust imperative programming just didn't work. Didn't work as
a paradigm, even!

~~~
_bxg1
It had state mutation as a massive liability - which is probably why FP has
seen such a surge in recent years. Rust is the only language that mostly
solves that _without_ totally changing the paradigm.

~~~
0815test
It doesn't really change the paradigm compared to FP even, it just enforces a
clear separation at all times between _sequential_ state mutation on the one
hand and references to _shared_ (and generally immutable, except as provided
for by explicit mutability mechanisms) state on the other. Which is
essentially what FP languages end up doing, though they go about it somewhat
differently.

~~~
_bxg1
Right. The problem is that functional purity always comes with overhead; data
structures like Clojure's help a whole lot, but there's still a cost.

But one of the primary motivators for functional purity is to avoid unintended
side-effects.

In pure FP, You never even have to bother your mind with _unexpected_ side-
effects because there are _no_ side-effects.

Rust instead gives you the vocabulary to carefully articulate _intended_ side-
effects, preventing all other, _unexpected_ side-effects at the same time. So
you can colonize the wilderness, instead of avoiding it altogether, and for
that you get to skip the overhead of treating everything as immutable. Not
that this is an indisputable improvement for every use-case, but it's a novel
tradeoff and one that is definitely preferable in many domains.

------
MrBuddyCasino
> I would probably experiment with exceptions if I were making this language.

Hard disagree. Rust made conscious design decisions (i.e. limiting type
inference) to enable local reasoning, and exceptions are non-local.

~~~
barrkel
Checked exceptions (like Java) are isomorphic with Rust's Result type. You
could convert a checked exceptions syntax into Result exception-carriers on
each return with a mechanical transformation, of a Rust+exceptions program
into Rust-today program.

~~~
lmm
Suppose you have

    
    
        fn foo(i: i32) -> string throws IOException
        let x: Vec<i32> = ...
        let y = x.iter().map(foo).collect()
    

What's the type of y ? There are no good answers here: if it's Vec<string>
then where did the errors go? If it's Vec<Result<IOExecption, string>> then
the user is pretty confused about where the Result came from. You can't
propagate the IOException through the map() call in the general case (it might
be in a third-party compiled library etc.). In Java this is a compilation
error and the reason checked exceptions are useless in practice.

~~~
barrkel
In Java, you'd expect map() to throw, and the type of y to be Vec<string>, in
Rust parlance. If one of the conversions failed, you'd expect the stack to
unwind as the exception propagates, and you'd expect that the use of map()
means you're not particularly interested in which item failed, you just want
to abort (roll back) on failure. This is the 99.9% use case for exception in
Java, or C#, or Delphi; you almost never catch exceptions.

If you're interested in which item failed, ideally you'd be using a method
that doesn't throw at all, and instead returns an error condition. I like how
C# makes this clear, with Parse and TryParse on Int32. You use Parse when you
want unwinding and abort behaviour including stack unwinding, and TryParse
when you want to handle errors, and you don't use exceptions for expected
errors, since expected errors are not exceptional.

(If the map is lazy, you'd expect the materialization in collect() to throw.
But the return type wouldn't change.)

IMO, the kind of code you write on a daily basis is the primary determinant of
your preference for error codes vs exceptions. If you need to handle error
conditions frequently, you'll prefer error codes, because they're data, like
all other data, and you can use the general compositional tools at your
disposal to manipulate them. If you handle error conditions exceedingly
rarely, and mostly just want to abort, unwind, roll back, go back to the main
loop and log them, then exceptions are your friend.

I do not think there is a fundamental superiority either way. I think there
are tools for purposes. I do think that Java's checked exceptions are half-
baked; they're half-way into the compile time type system in a language whose
applications are generally better suited to run time exceptions.

~~~
lmm
> In Java, you'd expect map() to throw

You'd have to use unchecked exceptions to achieve that though. There's no way
to propagate the checked exception across that call, because there's no way to
make map() generic over exception-or-not in a Java/Rust-like type system.

> If you're interested in which item failed, ideally you'd be using a method
> that doesn't throw at all, and instead returns an error condition. I like
> how C# makes this clear, with Parse and TryParse on Int32. You use Parse
> when you want unwinding and abort behaviour including stack unwinding, and
> TryParse when you want to handle errors, and you don't use exceptions for
> expected errors, since expected errors are not exceptional.

Result gets you that without needing to write two implementations of every
method. You call unwrap-or-panic in the cases where error is not
expected/handled, and handle it in the cases where you want to handle it.

------
lmm
Interesting. Coming from an FP background I'd say the opposite - the confusing
part of Rust is all these special-case imperative control flow keywords, a
more smalltalk-like "everything is just values and functions" syntax would be
the best way to simplify it.

I'd agree with stepping away from guarantees about when allocations happen
etc. But I think at the point where you remove those from Rust you really just
have OCaml.

~~~
cmrdporcupine
Well, it'd be OCaml without a garbage collector. And to be honest, that would
be a very useful and handy language for systems development.

~~~
lmm
OCaml is already a good language for systems development - people are far too
scared of garbage collectors. And if you make the changes in the article then
I think you give up most of the concrete benefits of non-GC - you won't have
easy C interop without control of stack/heap/allocation, and since trait
objects could end up nested arbitrarily far, I think you might still get GC-
like pauses where a simple-looking code line ended up doing an arbitrary
amount of unwrapping and freeing work.

~~~
cmrdporcupine
I guess it depends on your definition of systems development. If you mean
"writing systemsy applications" like IoT things or whatever, sure, fine, a GC
isn't going to be an issue and probably will help.

If you're talking about a language to write drivers or an operating system,
etc in then a GC is just a big No. In that domain we don't even have
Malloc/Free, let alone GC services. Having the overhead and normative
lifestyle assumptions of _any_ kind of runtime is a big Nope.

~~~
atombender
Counterpoint: The Oberon System successfully implemented a garbage-collected
OS kernel:
[https://en.wikipedia.org/wiki/Oberon_(operating_system)](https://en.wikipedia.org/wiki/Oberon_\(operating_system\)).

------
tomp
I'm really conflicted about

 _> Aliasable XOR mutable_

because it prevents _eventual_ synchronisation (e.g. single writer, multiple
readers of a "monotonic" data structure like a counter)... I'm not convinced
that it's entirely necessary, nor how easily it can be proved safe, but AFAIK
many garbage collection runtimes work using eventual synchronisation...
Probably other algorithms as well!

Another thing that's not (AFAIK) covered by Rust's ownership system is
flexible memory pools... where you can have _proofs_ that some memory is
accessible and pass those proofs around (whether at runtime, or just during
compilation on the type system level)... although that might be entirely
isomorphic to just passing around owned pointers to memory (pools), so maybe
Rust _does_ support it.

People with lots of experience with Rust, what scenarios do you need to use
unsafe Rust for?

~~~
dbaupp
Interior mutability via atomic values ([https://doc.rust-
lang.org/std/sync/atomic/index.html](https://doc.rust-
lang.org/std/sync/atomic/index.html)) works great for the first one, and,
indeed, some sort of atomicity/synchronisation is required even in languages
without the XOR rule (such as C++) to prevent undefined behaviour.

There's a variety of safe arena types that give allocation inside pools too:
[https://crates.io/search?q=Arena&sort=downloads](https://crates.io/search?q=Arena&sort=downloads)

~~~
tomp
Atomics are very useful indeed, but an overkill for some situations. In
particular, if there's just one writer and multiple readers, the write doesn't
need to be synchronised (as there's no possibility of someone else mutating
the value in the middle of you incrementing it). An example would be the GC
thread incrementing the _epoch_ id/count that all mutator (user) threads read.

Presumably you'd still need _some_ sort of synchronisation otherwise the
memory will stay mutated just in one CPU's cache, you need to "flush" the
updated memory so that other CPUs can then see it, but that's likely cheaper
than executing _the whole_ operation atomically... Basically, increments are
fundamentally not atomic (i.e. composed of multiple separate operations)
whereas reads (of a single word) are, so if all mutations happen from a single
thread (assuming a sensible CPU architecture and cooperative compiler) there's
nothing else that needs to be done to ensure atomicity, you only need to care
about the "happens before" relationship.

But then I'm not a low-level programmer so the above is probably all wrong :)

~~~
dbaupp
They have to go via the atomic types or it is undefined behaviour. One can use
a very weak ordering (such as Relaxed) to require little or no "physical"
synchronisation by the CPU. Like C++, Rust atomics offer a range of levels of
synchronisation, weaker than sequential consistency.

A platform like the JVM disguises this because every read and write (even
without 'volatile') use minimal synchronisation to avoid the worst aspects of
the danger here. This ensures that programs that do it wrong (e.g. forget a
'volatile') are "only" incorrect, but not unsafe.

~~~
tomp
Ah, you're right, weaker ordering takes care of a lot of these issues! Good
point, thanks!

------
skohan
> Resource acquisition is initialization

I recently built a small prototype Entity Component System, and one of the big
learnings for me was to what extent RAII is an anti-pattern with respect to
performance. It's actually amazing how fast a modern CPU can be when you're
not using it to allocate and initialize small blocks of memory at a time.

I understand the benefits of RAII in terms of bookkeeping, but it seems like
there is opportunity for a new resource management paradigm which is more
optimized for using computing resources.

~~~
PyroLagus
Can't you just initialize everything at once before you need it rather than
initializing one object here and there?

~~~
skohan
It depends heavily on the problem domain. In some cases this is possible, but
it's often the case that you will not know what resources you will need at the
beginning of execution.

Also the implementation of "initialize everything at once" in an RIAA paradigm
is not necessarily going to be the most efficient possible implementation. For
instance, I might want to do something like this (in a made-up language):

    
    
        struct A { ... }
    
        func init() {
            let a1 = new A();
            let a2 = new A();
            let a3 = new A();
        }
    

The most efficient way to implement this would be to allocate enough memory to
store a1, a2, and a3 and then run the initialization method 3 times over that
memory. But it's likely that instead the compiler will allocate a1, initialize
a1 and so on with a2 and a3. Since the allocations are separate, maybe another
thread takes precedence between the allocation of a2 and a3 and allocates some
memory, so these values are not contiguous in memory, and I get worse caching
performance when accessing them.

In this toy example with 3 values it doesn't make aa lot of difference, but if
you add up all these marginal costs in a large application it absolutely does
cost a lot of performance.

~~~
0815test
> The most efficient way to implement this would be to allocate enough memory
> to store a1, a2, and a3 and then run the initialization method 3 times over
> that memory.

Only if a1, a2, a3 are also _freed_ at once. In which case, you can just put
all three in a struct that is allocated as a single block. Rust doesn't yet
support this very well _in the general case_ , because its support for what
C++ calls "placement new" and customized allocation in general is not complete
or stabilized. But doing this in a more hackish special-cased way is already
possible in many cases.

~~~
skohan
> Only if a1, a2, a3 are also freed at once.

Yes exactly. This is meant to be illustrative of the case suggested in the
parent comment, where all required runtime objects are known and can be
initialized together at the beginning of execution.

edit: it may be possible to program around some of the worst performance
pitfalls of RAII in many cases, but you will end up with very non-idiomatic
code, which I would take as evidence that RAII is not ideal for memory
performance.

------
akavi
The lack of ADTs in mainstream languages is really quite baffling, given their
obvious utility.

Thankfully it looks like they’re trickling in via Typescript/Swift/Kotlin.

~~~
sambal
Do TS and Kotlin support them now? Last I checked TS was relying on string
comparisons and Kotlin wasn’t even bothering outside sealed classes

~~~
sixbrx
TS is a bit more powerful than it appears here, because string literal values
are recognized at the type level, so can be checked by the compiler when
distinguishing cases. See for example: "Discriminated Unions" at
[https://www.typescriptlang.org/docs/handbook/advanced-
types....](https://www.typescriptlang.org/docs/handbook/advanced-
types.html#discriminated-unions).

------
Aardappel
See, everyone has a different thing they find important about Rust. He suggest
abandoning zero cost abstractions (letting stack/inline allocation be decided
by the compiler instead) which I think would remove most of the power of the
language. Default thread safe primitives sounds really wasteful too.

I think languages similar to Rust but with less complexity will be a very cool
space to watch going forward. All sorts of interesting language designs
possible.

I’m working on a language that does “Rust-like” things, but unlike the
article, what I am going for is the efficient memory management, in this case
the inline structs (zero cost abstraction) and compile time memory management
(automatic lifetime analysis). The biggest difference with Rust is that it is
fully automatic: when ownership can’t be determined at compile time, by
defaults it falls back to a reference count increase at runtime (in Rust this
would just be an error). The advantage is zero annotations, and a language
that can be used mostly by people that don’t even understand ownership. There
may be ways to explicitly (optionally) enforce ownership in the future, for
those who want more control.

Language: [http://strlen.com/lobster/](http://strlen.com/lobster/) Details on
memory management:
[http://aardappel.github.io/lobster/memory_management.html](http://aardappel.github.io/lobster/memory_management.html)

------
nixpulvis
> As I said once, pure functional programming is an ingenious trick to show
> you can code without mutation, but Rust is an even cleverer trick to show
> you can just have mutation.

Yea, I like this.

------
coldtea
> _In other words, the core, commonly identified “hard part” of Rust -
> ownership and borrowing - is essentially applicable for any attempt to make
> checking the correctness of an imperative program tractable. So trying to
> get rid of it would be missing the real insight of Rust, and not building on
> the foundations Rust has laid out._

Sure, but people wanting a smaller Rust might not care about the "real insight
of Rust", but of the surface syntax, speed, apis, tooling (e.g. cargo), and so
on...

I'd like a Rust like language that's GCed for example, and drops all the
lifetime annotations and so on...

~~~
mathw
I guess it depends what you think is the real core of Rust - for me, Rust's
primary innovation and primary differentiation point is giving you safe memory
management without a garbage collector. Anything that steps away from it (so
stepping away from the borrow checker, lifetimes and so on) becomes something
that isn't Rust anymore.

After all, just about everything else in Rust is in Haskell.

~~~
mrfredward
The borrow checker is undoubtedly the core innovation of Rust, but it's not
what people new to the language want. When I first came to rust, I was looking
for a c++ alternative that had similar syntax and low level power, but also a
sane standard library and modern language features like closures.

When I first learned about the borrowchecker I thought it was brilliant. Then
I spent some time fighting it and I hated it. Then I learned to love
it...until I got backed in to a corner and had to re-architect a project I'd
been working on for months. Now I have mixed feelings about it.

So I agree Rust isn't Rust without the borrowchecker, but I empathize with all
the noobs who want a rust without it because many people just want a language
with some traction that fixes c++ past mistakes, and they end up getting a
massive paradigm shift thrown at them instead.

~~~
skohan
It's surprising to me that a language hasn't emerged which is more purely a
modernization of C. It seems like you could go a long way just keeping C's
basic abstraction in place, reworking some of the clunky bits and adding some
basic quality-of-life improvements from the past 30 years of language design.

