- Part 1 - Figuring out the design - A naive start;
- Part 2 - Learning the ropes in Rust.
Warning: contains pre-1.0 Rust code
I figured out CoffeeScript in a day, so, I thought, How Hard Can This Be?
Harder than I expected
In this post I will try to recollect some major walls I ran into. Back then, I had only a slight idea how the ownership and borrowing works, so, I thought this little DI project would be great for some practice.
It might be useful for readers who are trying to implement something similar or want to know how a Rust newbie with scripting-language background thinks.
Coming up with initial working prototype
From the tutorial and some experimentation before, I imagined traits to
be akin to something like interfaces in C#, Java or PHP. I also found
AnyMap, so I knew I could cast my boxed value into
type and keep items of different types in the same map.
Sneaky silent reference
I started making basic abstraction: the registry was going to contain all the getters for all defined items of different types:
I knew I could implement trait for any value. My idea was to make a trait that converts any compatible value to a getter of appropriate type:
I figured I could try to use
|| -> T as my getter, why not? I would then add
it to registry like this:
Then, for a simple start, I attempted to implement
ToFactory for an
value. Returning a fixed value worked:
Wow, I thought. It is going to work, I thought. And then I tried to return cloned self:
Being new to this, I though that closure would use
not a silent
&self reference! The error message in this case was completely
cryptic to me. I thought: “well, closure would capture
self into its
environment, so, no matter where I moved the closure, the
follow”. Why does it complain about
self not outliving scope? Why should it?
It took me quite a while to figure it out. Sadly, the unboxed closures were
very unstable back then (I got bunch of ICEs when attempting the
syntax), so, I thought, I can do it without closures.
Full Java Ahead!
And then I had my next lesson.
Traits are not like interfaces in other languages
Even though it did not work with a closure, I knew it was certainly possible to manage without it. I just needed an interface… I mean… trait that returns my value:
And, just for testing, I implemented
i32, so I could
Getter<i32> interface was required:
Then, instead of a closure,
ToFactory should return this trait:
Was it going to work? Well:
Why? What is this
Sized, I thought. After few shameful attempts to
transmute and store trait as an unsafe pointer instead of a
Box<Any> blew up
into my face, I tried to gently communicate to compiler that my getter
should always be sized.
I changed my factory to return an unknown getter:
I was surprised that I needed no
Sized when I did this:
Little did I know that this kind of signature, when used with an
i32 as an
argument, will essentially be equivalent to this:
Notice that no actual
Getter<i32> is getting boxed? And I thought
I have just managed to shove the trait into the box!
So, imagine my surprise when I saw this when I tested if I could get the value back:
Here we go… it needs
What did I do? I tried to workaround it again.
What? It doesn’t work without the underlying type for getter getting exposed?
The lesson which I learned was this: the traits are simply a collection of methods, like a vtable for virtual class in C++, but separate from the struct. And it can not be boxed or used as a trait again if the actual struct that is backing the trait can not be somehow resolved at compile time.
However, while it worked for this quick test, the real use of getter in DI
would have to be abstracted from the actual type behind
my getter would be constructed from varied underlying types.
So I ended up implementing a wrapper struct for my
Getter<T> trait that
looked like this:
Registry methods were greatly simplified:
I am still wondering about the exact meaning of
'static here :)
The biggest chunk of code in these crates is not this (now simple) DI mechanism, but code required for validation and error collection. I did not hit any major obstacles while implementing it, it was simply tedious.
Ownership and borrowing system is great. However, there are some cases
where it may not be intuitive, and some understanding is needed about the
things compiler does behind the scenes - like in the first case where I did
not know that the reference of
self was used.
Also, Rust does not initially look like systems language. Therefore, one may be tempted to skip thinking about things like stack or memory allocation. I did that when I tried to use the trait like an interface. I could only move on when I finally realised what is actually happening behind the scenes, and had a better mental model of the way Rust compiles generic methods, traits and structs, and how they are laid down in memory.
That’s all for this DI story for now, but I might return back with part 3
if I find this
di library useful and decide to improve it further.