Back to old tricks, or, baby steps in Rust
donsbot.wordpress.com
donsbot.wordpress.com
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.
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.
Unsized types interact badly with everything, really. That's why you usually shove them behind a pointer.
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.
Problem with them is disrupted control flow and implicit capturing of the context. However, in cases where you have to guarantee something to happen after user code, the RAII guards can't replace the closures, because drop() isn't guaranteed to be called even in safe code...
The ECS GUI integration is an interesting direction. I think Rust ecosystem looked at it, and it was hard.
I think that we need more time to explore this. It took the OOP GUI frameworks a long time to mature.
Also I find code like this to be anti-ECS https://docs.rs/orbtk-api/0.3.1-alpha2/src/orbtk_api/macros..... They use macros as a workaround for lack of inheritance in Rust. I would not call that ECS.
As a comparison, imagine your bank's lending products. You probably don't want to hardcode them into a type hierarchy with a virtual method AccrueInterest(Date), but rather have flags that indicate which loans accrue daily, which loans accrue monthly, which loans have some secondary interest etc.
Like you can't have a type mismatch with systems, since you get the data you queried.
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...
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?That doesn't fully describe all of it, and there's some inferences there, but that's how I hear it in my head. Some of it is experience, and some of it is context, too. Especially that "an iterator" part. I'd expect to see Iterator when talking about iterators, but the 'shape' for a lack of a better word looks like one to me.
Rust's syntax is pretty regular and modular. After some practice, the bits start to fit together, and the way to tackle more complex signatures is the same as how you tackle complex code: break it up into smaller bits, and then assemble your understanding of the bits.
This chapter of the book is probably a good start:
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.
In Rust you work a lot with types. You can write code that supports several different types but you usually have to specify which constraints these types have to fulfill. You specify these constraints between <> brackets. In this example we have 3 such constraints:
1) <'s, S: Seed, A: Copy>
2 & 3) <dyn Fn(S) -> Step<S, A> + 's>
with 2) <S, A>
with 3) <dyn Fn(S) -> Step<S, A> + 's>
Because these constraints are all part of the same struct definiton, the names in these 3 constraints refer to the same things. The names are: s, S, A
Constraints in Rust can either be lifetimes or traits (similar to interfaces in Java). Lifetimes are marked with a single quote (') followed by the name of the lifetime (in this case: 's)
Let's look at constraint number 1):
I) We declare a lifetime 's but we do not specify where it will be applied in the struct yet.
II) We declare a type constraint S. Every type that is used as S must implement the Seed trait.
III) We declare a type constraint A. Every type that is used as A must implement the Copy trait.
Before I go into constraint 2&3, I must explain that `Fn(S) -> Step<S, A> + 's>` describes the type signature of a closure. The syntax is basically `Fn(parameter-list) -> return-value`.
Constraint 2) says that the return value must be a struct `Step` with the type parameters `S` and `A`.
Constraint 3) says that the next field of the struct must be a `Box` that contains an object of unknown size (hence the `dyn`) which is a closure that takes one `S` parameter and returns a `Step` struct with the type parameters `S` and `A`. The lifetime of the closure must be `'s`.
Finally, the seed must be of type `S`.
Rust is just great! I feel liberated from Java and Node.
But what intrigues me is why do programmers insist on making code look and feel difficult? Is there a secret obsession in good programmers to make their code look clever? Is variable naming not a basic etiquette?
I have gone back and forth on whether to align with the general convention on single-letter generic type names, but I’ve tended to come back to the single-letter approach any time I try making them more specific. The issue is of course that they are generic, and so naming them too explicitly robs them of some of their “this is any type” genericness. For lifetimes, I will definitely sometimes name them more explicitly than `’a`, but usually only when there’s more than one, as a way of being sure I don’t mix them up. However, the association of lifetimes with references tends to be straightforward enough that it often seems clearer and less messy to just name them with single characters.
We talk a lot about user experience and usability in our end products but we do not extend that thought to our fellow programmers at code level.
So this basically just seems to be "what if Iterator couldn't use &mut".
If the goal was to build efficient and flexible streams in Rust, then indeed, you wouldn't start like this.
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).
What do you mean? Image de/encoding is certainly possible in Haskell.
This is probably one of the reasons I’ve been drawn to Go, where that feeling Is basically non-existent.
It's just Worse is Better The language for better or worse.
I actually think Rust has a better story of what it "nudges" devs to write. Go strives for a very fuzzy feeling of surface simplicity, but in practice it's way less principled.
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.
Thankfully reader view in firefox cleaned it up, but otherwise it would be inaccessible.