
Back to old tricks, or, baby steps in Rust - todsacerdoti
https://donsbot.wordpress.com/2020/07/04/back-to-old-tricks-or-baby-steps-in-rust/
======
enitihas
I completed the second interpreter in the craftinginterpreters book in rust.
One thing I noticed was all the places the author was explicitly keeping track
of lifetimes, I could do it via the rust compiler. For example, there is a
function which should only be called with strings which will live for the life
of the program. Easy to enforce with rust.

The only place where I couldn't find a zero cost abstraction was enum
encoding, where enums are of different sizes.

For example

pub enum OpCode { OpReturn, OpConstant(u8), OpNegate, OpAdd, ... }

It would be great if the compiler could use only 1 byte for the rest of the
enums other than OpConstant, and more bytes for OpConstant, and use maybe the
first 2 bits to store the number of bytes used, kind of like sds library by
antirez. This is the only place where I felt my rust implementation was less
efficient than the book's c implementation.

Off course, I could go the same route as the c code, and not use type safe
enums by just keeping an array of bytes and doing the decoding myself, but I
wanted to have full compile time type safety.

~~~
Twisol
> The only place where I couldn't find a zero cost abstraction was enum
> encoding, where enums are of different sizes.

Unfortunately, variable-sized types interact poorly with arrays -- you can no
longer access any element in constant time. UTF-8 has essentially the same
issue.

Did you end up abstracting over the byte array at all? I would imagine that
some kind of `BytecodeBuffer` could give pretty reasonable ergonomics over a
raw buffer without sacrificing efficiency or correctness.

~~~
masklinn
> Unfortunately, variable-sized types interact poorly with arrays

Unsized types interact badly with everything, really. That's why you usually
shove them behind a pointer.

------
adamnemecek
I've been writing Rust for some time and I think that people overuse closures.
I've been looking at a lot of GUI framework implementations and a lot of them
use closures for event handling. I think that the ECS
([https://en.wikipedia.org/wiki/Entity_component_system](https://en.wikipedia.org/wiki/Entity_component_system))
paradigm is a much better match. I've been working on an GPU-first, ECS based
GUI framework for my app ([http://ngrid.io](http://ngrid.io)) and couldn't be
happier. I might open source parts of it. The code ends up being really short
and and uses little memory.

A browser might have multiple representations of a DOM node in memory (the
pipeline from DOM to GPU is really long). I have like one.

I realize this is only tangentially related.

~~~
amelius
I just followed your link to the ECS Wikipedia page, and it seems to me that
ECS is a very dynamic way of creating objects and defining their behavior,
almost like how you can assign functions to objects in JavaScript without
defining them in a class. Doesn't this conflict with the more formal approach
of using a strong type system?

~~~
arc-in-space
This feels hard to answer, and I feel like you're making some fundamental
category error. Having a type system doesn't mean "don't do suspiciously-
dynamic-sounding things", it means "don't accidentally mix up data with
incompatible structure". Rust, C++, Haskell, etc. are all perfectly happy with
polymorphism, and a typical ECS certainly is well-typed. It's just another way
of doing mixins.

~~~
twic
It arguably conflicts a bit, because in an ECS, you can have notions of, say,
Movable and Damageable, but there's no way to write a function applies to
things which are both Movable and Damageable, right? The closest you could get
would be a function which takes both a Movable and a Damageable, and has to
trust that they correspond to the same entity.

~~~
adamnemecek
No actually, you query both and as you iterate through the results, the
current element is one that has both. Look at some examples of the Rust ECS
framework specs.

------
aazaa
I like the benchmark comparison between Box and Trait approaches. It's not
easy to find these comparisons.

Regarding overall approach, Iterators are very flexible, and I find it a
little surprising that you get Iterator behavior (the Stream trait) without
explicitly mentioning Iterator in the code.

If iterator performance is a concern in, for example, file I/O, there are ways
to address that:

[https://stackoverflow.com/questions/45882329/read-large-
file...](https://stackoverflow.com/questions/45882329/read-large-files-line-
by-line-in-rust)

------
Tempest1981
As a Rust newbie, how do I learn how to read this?

    
    
      pub struct Stream<'s, S: Seed, A: Copy> {
          next: Box<dyn Fn(S) -> Step<S, A> + 's>,
          seed: S
      }
    

Can someone summarize in a sentence or two? Or point to some docs?

~~~
dochtman
Stream is a struct (a type with named fields). It has three parameters, the
lifetime s, the type S and the type A. S must satisfy the Seed trait, and A
must satisfy the Copy trait. Stream has two fields, next and seed. The type of
next is a Box (basically an owned pointer into the heap) of a type that
implements the Fn(S) -> Step<S, A> interface (that is, it can be called with
an S as the single argument and produces something of the type Step
parametrized with S and A) and does not outlive the s lifetime. The field seed
is of the type S.

This chapter of the book is probably a good start:

[https://doc.rust-lang.org/book/ch10-00-generics.html](https://doc.rust-
lang.org/book/ch10-00-generics.html)

~~~
partiallattice
Small typo:

it can be called with an S as the single argument and produces something of
the type Step parametrized with S and A) and -does not- outlives the s
lifetime.

's refers to a minimal lifetime and not a maximal limit.

------
lilyball
This looks like a fun exercise, but ultimately it seems to just be reinventing
Iterators except each step yields a new copy of the iterator instead of just
mutating the existing iterator. The other difference is there's an explicit
Skip, which I don't understand the point of because the next function could
just be written as a loop until it either finishes or yields an element.

So this basically just seems to be "what if Iterator couldn't use &mut".

~~~
twic
I think the point of the exercise is to start with streams as they are defined
in Haskell and then gradually improve them, as an experiment or demonstration.
The post is aimed at Haskell programmers who might be interested in Rust.

If the goal was to build efficient and flexible streams in Rust, then indeed,
you wouldn't start like this.

------
zetalemur
It's interesting to see that many long-standing members of the Haskell
community take a look into (or even heavily use) Rust and add it to their
repertoire. This is really a good sign for Rust (Haskell folks normally have
high expectations from a PL).

It makes sense, as (currently) it's pretty much impossible to tackle some
important domains (e.g. image de/encoding) in Haskell while in Rust we already
have libraries for that. Also: Idiomatic Rust is already extremely fast, while
in Haskell it is non-trivial to implement high-performing code.

Prime example: The sieve of Eratosthenes (and I think we can agree that this
is a prime example). In Rust you can implement a trivial solution that
performs extremely well. In Haskell you will probably reach for the ST monad
and I think it's controversial if that's idiomatic or not (and maybe non-
trivial to grasp - as far as I know ST is implemented via compiler magic).

~~~
tome
> it's pretty much impossible to tackle some important domains (e.g. image
> de/encoding) in Haskell

What do you mean? Image de/encoding is certainly possible in Haskell.

~~~
zetalemur
Yes, I may have worded that wrong, it's certainly _possible_. But is it
_reasonable_? By that, I mean can it compete in performance with C/C++/Rust?

~~~
tome
Ah, not sure! Maybe you're right. But then it doesn't seem separate from your
second point: "Also: Idiomatic Rust is already extremely fast, while in
Haskell it is non-trivial to implement high-performing code.".

~~~
zetalemur
You are technically correct. The best kind of correct. :)

------
lachlan-sneff
This is essentially the iterator trait, right?

------
ed25519FUUU
I’m glad to hear that the compiler nudged you in the right direction for
optimal code. That nagging feeling that there’s a “better way” or more optimal
solution is really debilitating to me when writing in an unfamiliar language.

This is probably one of the reasons I’ve been drawn to Go, where that feeling
Is basically non-existent.

~~~
jasonjayr
What is it about go that makes it 'non-existent' ?

~~~
hombre_fatal
Because Go lacks powerful tools like sum types and generics. In a language
like Rust or Haskell, you're always asking yourself if you can better express
the problem in the type system, or which constructs you can use that, say,
make runtime errors impossible.

In Go, your tools are limited to copy-and-paste and `interface{}` type-
casting. There are no better options to consider, so you try to untrain your
brain from its reflex of "hmm this is error prone, I should find a better
way."

A good example of this is when you're parsing a binary file into a sequence of
record structs. In a language like Typescript or Rust, you just make one
ergonomic `Record = Record1(int, int) | Record2(string) | ...` and `fn
parse([]byte) -> []Record`.

In Go, the best you have is `fn parse([]byte) -> []interface{}` with type-
casting.

~~~
damnyou
That sounds like it would increase the load on your brain, not decrease it.

~~~
masklinn
I guess they mean it doesn't give you that feeling of "there must be a better
way" because chances are very high there is not, and so after some time
looking for better ways and never finding them, since there's no feedback loop
of "yeah I found a better way!" that feeling stops occurring.

------
kiaulen
I read the article, but had to use FF's reader mode. Why do people use #666
and a super light font weight for body text?!? My eyesight is fairly good, and
it's still incredibly hard to read.

------
garmaine
OP, why do you have gray text on a white background? It is literally
unreadable for someone that doesn't have young eyes.

Thankfully reader view in firefox cleaned it up, but otherwise it would be
inaccessible.

