You are viewing a read-only archive of the Blogs.Harvard network. Learn more.

Large Language Models in Software Engineering, today.

Mercifully, we are now at the point in time where the wave of immense fantasy over ChatGPT being a nascent, omniscient entity has started to crash onto the impenetrable rocks of reality.

People have slowly began moving on from the existential “shock and awe” into the territory of more mundane considerations, such as “can I use it to make my life easier?

The answer heavily depends on three factors.

The domain of the problem being solved.

Do you need help starting off a presentation on something you are not an expert at (say, Tensor Calculus)? No problem, ChatGPT will beautifully point you to the right direction.

The quality demands of the deliverable.

The vast majority of people are notoriously bad at creating presentations, therefore the expected performance bar you need to pass is low. ChatGPT can act like a little stool here for you to stand upon, no problem.

The expertise (or lack thereof) of the evaluators.

If you are giving a talk about Tensor Calculus to people who don’t know about that, does it really matter how robust your claims are? On the flip side, are you comfortable talking semi-nonsense when experts sit comfortably among the crowd?

In other words, if you are building a plane you are expected to be an expert at whatever it is you do and we definitely have hard expectations that the plane won’t crash for no good reason.

But what if you are a beginner in your journey for excellence, heroically making coffee for others at the office? Well, Large Language Models like GPT can definitely be helpful. And their so-called “instruct” versions make the interaction with them comfortable. So, it’s a great time to be alive.

What about using LLMs when developing software?

Is there a real benefit there?

Before we answer that, we must avoid the Wittgenstein trap of varying meanings of key concepts. Let us boldly make some steep assertions to clear up any lingering confusion.

A) LLMs do not reason.

Like, never. It certainly appears to a human that they do but they really don’t. How do we know? That’s because the recipe for making an LLM is no secret. Like the witches of old, we know exactly what is thrown into the cauldron to get the magic potion.

In a very high level approximation, LLMs are trained into knowledge by calculating a “distance” between words. Then, given some input, they guess what should come next.

Here we told GPT-3, if “man” relates to a “king”, what does a “woman” relate to? And it gave the correct answer.

But how did it know? Put on your sci-fi glasses and imagine a space with an untold number of dimensions. And within that humongous space, there are some tiny little stars, in galaxies far far away. You have been tasked by the Emperor to map the whole universe and root out the Resistance … but I digress.

It could guess “queen” because there is a directional path between “man” and “king”. If you walk that exact same “steps” of the path but begin from the word “woman”, you will eventually arrive somewhere. The nearest word around that place is the word “queen”.

That’s how AI “understands” words. LLMs take this concept one step further:

  • Download all the textual Internet and break it down to little “word fragments”.
  • Spend a vast amount of money to create a Universe of stars and create a map of all the highways, roads and mountain paths between them.
  • Write lyrics about the weather in the style of Snoop Dog.

How does it write those lyrics? Your prompt points it into some position on that multi-dimensional universe and then it tries to “statistically infer” (fancy speak for “guess”) what the next word to extend your input should be.

What ChatGPT (the “instruct” version of GPT) does, is that it adds a layer where the interaction between the prompt and the response is more aligned to human expectations.

A large amount of human curated Q&A has been added as training layer on top of GPT to make its output more palatable to our interaction sensitivities. And that’s great, OpenAI really hit the nail with it in terms of democratizing the use of LLMs.

All that said, note what the LLM did **not** do:

  • It did not have an ontology (what things are).
  • It did not have a methodology (how to investigate into the things).
  • It did not have an axiology (what is the value of things).
  • It did not have an epistemology (what is the nature of knowledge acquirable for the things).

Therefore, it cannot and does not reason. It is merely* trained to guess in a way that the output looks good to humans.

* I write "merely" to contrast its process with reasoning. In terms of pure technological feat, it is very very impressive.

B) LLMs don’t have a long “working” memory.

Currently, GPT-4 has a memory of about 8 thousand tokens to work with. That’s a lot for some tasks but very little for others.

The current problem with increasing memory is that its cost is quadratic. In simple terms, it can get computationally expensive (read: “pricey”) really fast. That’s one of the reasons why GPT-3, which has half the memory size, is faster than GPT-4.

There exist, of course, optimizations and tricks to move the needle further. Yet at the current state of the art, really huge windows are impractical.

And with that, we are finally ready to explore the answer to our original question.

Can it REALLY help me in my daily work creating software?

Not really, not yet – if you are a senior developer that is.

Sure, getting some small standalone functions created faster is great and all, but that’s not what you really spend most of your time on.

The biggest hurdle right now is that…

LLMs don’t have a big enough context window to consume a non-trivial code-base.

The “hack” of creating embeddings out everything and shoving them into a vector database doesn’t really fit what we need here. It doesn’t pay to “answer” questions by injecting into the prompt a few small excerpts of code found via a an external similarity search.

To be realistically useful, the LLM should have a full view of the code-base.

Software engineering requires reasoning.

If you are not working in toy projects, your responsibility is not to type random staff that compile. Your job is to think and produce solutions that add value. And we already know that’s not what LLMs do.

The time/effort it takes to refactor/correct the things an LLM throws out outweighs the benefit it provides – at least when it comes to non-trivial things.

But not all is bleak!

LLMs are great when you are learning. In this industry, nobody has learned it all and no one ever will, because tools & the knowledge domain evolve in an astonishingly fast pace. So be it a junior or a senior, you have to be a learner.

Despite LLM doing what it does best (“guessing things”), it can still act as a mediocre coach and give you examples to work with. That can accelerate the learning process because the hardest part in learning a new topic is overcoming the fog of utter ignorance, which can at times be very daunting.

 

Rusty Ownership and the Lifecycle’s Stone

This blog post is an exposition of the presentation I gave at a Dev Staff, a Developer Community in Crete. This post approaches the matter from a high-level language perspective, just like my previous one on Rust.

Rust is a low-level language that has the trappings of a high-level language. Rust developers gleefully enjoy a modern, streamlined experience.

Fact of life – just like oxygen and Doritos – is that for a long long time there existed an orthogonal relationship between performance and memory safety. You could pick only one.

Rust feels like a managed language (e.g. Node, Ruby) but under-the-hood, it (like C/C++) is not. That is to say, Rust produces very performant software – it is really fast.

In order to achieve this feat, Rust has introduced some new ways of solving the good-ole problems – and it is the only non-academic language that has managed to do so. When it comes down to memory management, Rust is a huge innovator.

Rust disposes of the garbage collector but does not impose on the developer the burden of dealing with the memory garbage. Traditionally, you either have to manage the memory yourself (à la C), or pass the burden down to a run-time feature of the language – heroically called “the garbage collector“.

This means that within your executable there exists another software bundled, and that software is responsible for cleaning up the memory mess we create as the user enjoys our application.

While deferring the hard work to the garbage collector sounds fantastic (and it mostly is so), that comes with its own sets of problems. The biggest of said problems is that the garbage collector has the annoying habit to “pause the world”. The garbage collector literally stops the application execution to do the cleanup, and then magnanimously resumes it.

This can and does lead to loss of performance, which is bad in situations that depend on it. In general, the garbage collector is an inefficient beast.

Random unrelated image.

Java Logo png download - 650*652 - Free Transparent Java png Download. - CleanPNG / KissPNG

It is not just 60-frames-per-second games that long for high performance. Any CPU-bound or repeated process also requires it.

Suppose for a moment the UN Committee of Software Developer Experience mandated that Python is now the only legal programming language in the world to code with. Python is two Orders of Magnitude (100x) slower than C. Suddenly, your $3,000 MacBook Pro barely beats an early-1990s-era 386DX computer.

Performance matters in systems programming. Keeping the memory from blowing up not only prevents software crashes but also keeps the bad actors away from your personal data & financial assets. Therefore, we need both.

Before Rust, we relied on the genius of the developers to juggle inside their heads the two giant boulders of the application: the business domain and the security domain.

Experience (i.e. a multitude of bugs, exploits & hacks) has clearly demonstrated that this is not a good path to walk. This path has been walked only because historically it was the only path in existence. Not any more!

Memory Basics

Developers working with high level languages almost never need to come in contact with how their application memory is structured and its mechanics. For this reason, let’s do a quick and dirty overview of how it all works.

There are two memory kinds that are available for your beautiful Rust application to use.

The Stack

The stack has a rigid structure. This makes the stack is easy to reason with.

  • Last-in, First-out.
  • Data stored has fixed length.

Make It Real Elite — Week 1: Stack & Queue | by Sebastian Zapata Mardini | Medium

You add (push) to the stack by adding to the top, and you remove (pop) from the stack by grabbing a plate from the top. Easy, peasy.

The stack is super fast and straightforward, but also limited in size. That’s because our applications can’t work only with data that can never be resized. For dynamically sized data (such as…, I don’t know…, useless things like Strings and Vectors), we need another type of memory.

The Heap

What is the heap?  Is it something like this?

ICS 311 #09: Heaps

No, put aside all the fancy CS stuff for a moment and let’s get back to the fundamentals.

Which basically translates to this in your application memory:

Automated Vertical Storage - Midwest Warehouse Solutions

The good part

You can mostly do whatever you want.

The bad part

You have to manage it or it will blow up.

But what exactly does memory management mean?

You must:

  • Keep a mapping between the parts of code the data they use of the heap.
  • Minimize data duplication.
  • Cleanup unused data.

Ideally, you want your heap neat, tidy and pristine.

AVITAS Inventory Appraisal Services -

But that’s a lot of pain to do. We have AGILE constraints. We have to pair program, mob program, extreme program and get those pesky story points done before the sprint ends because the burnout chart must look a specific way.

So in high level languages we’re back to deferring to the garbage collector.

Unless we code in Rust.

But how does Rust do it?

To understand this, we need to explore some new concepts that Rusts brings into play.

Ownership

This is an easy concept to understand, but the effects it has on the way you approach problem solving may be a bit more complex.

The players

Ownership of what? Ownership of values.

Who owns values? Variables own values.

let x = 5;

x owns 5

The rules

The 3 rules of ownership are:

1. Thou shall not have a value without an owner.

2. Thou shall not have multiple owners for a single value.

3. Thou shall sacrifice the value in a pyre once its owner’s life has no scope left.

Wut?

Let’s begin with the last rule (translated in modern vernacular):

3. Out of scope, out of memory.

What is the problem with this code?

The problem is that it does not compile.

😮

I can loudly hear your righteously undignified screams. Why on Earth wouldn’t THIS compile?

The compiler is our friend. And the compiler stops us on println!, complaining that it:

cannot find value `b` in this scope

Let’s take a look at the code again, noticing the scope.

  • The value 7 was owned by the variable b.
  • The variable b lives only for the duration of the inner scope. It is gone from memory after that.
  • Therefore, b does not exist when we try to reference it in println!()

The Rust compiler is a good butler. It wants to reduce our cognitive load. And there’s a lot of that involved on the art of making software. That is good. If something is trivial and doesn’t incur a cost, the compiler will do it on its own and hide that fact from us.

The hidden piece of action here is that the compiler tracked the variables scopes and then freed the values once each respected scope ended. We didn’t have to type drop() – and since we didn’t have to do it, it’s impossible to forget it by mistake.

Ok, sweet, nothing too radical here. Let’s move on.

2. One value, one ownER

Next question – what’s the problem with this code?

There is no problem, it compiles fine.

But you can’t compile it if your value is a string instead of a number.

Wtf?

This comes back to what we discussed earlier about the Stack and the Heap. The strings need dynamic allocation. You can append to a string. Therefore, they must live in the Heap.

Remember that one of the things we need to be mindful of when using the heap is to not perform unnecessary data duplication. So when we do let s2 = s, the compiler does NOT copy the memory value of “hello” into an new memory block. It simply creates a new pointer to the existing block in memory, like this:

s1 and s2 pointing to the same value

OK, but why doesn’t it compile?

This code uses the Heap and we have two pointers referencing “hello” in memory. So what? Well, let’s go back to the rule we discussed.

That’s what we’d expect the compiler to do, right?

But the compiler refuses. Why?

It’s a double free condition! Basically, we enter undefined behavior territory. Which is bad. Really bad. So the compiler won’t let us do it.

The Burdens of Ownership

Unfortunately, this “no copy” strategy creates some troubling inconveniences, especially with passing values to functions. Functions have their own scope, just like the if {} block above.

This code won’t compile, because the ownership of the “hello” String value has moved from main() to foo().

Passing a variable to a function means moving its ownership. And that ownership doesn’t magically come back on its own after the function scope ends.

The naive solution is obvious (because why bother RTFM?). We can pass the ownership back and forth, like this:

Obviously, this doesn’t scale at all. We’re supposed to reduce our cognitive load, not increase it geometrically.

Now What GIF - Finding Nemo Bags Floating - Discover & Share GIFs

References & Borrowing

Naturally, Rust has a solution for that. It’s called temporary ownership, or “borrowing” for friends, with benefits.

This code now works. We told Rust that foo() needs to read the value of s but that we also need it back once it’s done. No interest needed. Just give it back.

But can I play with it?

Rust variables are immutable by default. But they don’t have to be. And the same goes for borrowed references.

The mut modifier marks the variable as mutable. And since we want foo() to be able to modify s as well, we need to explicitly let the compiler be aware of it. So we use &mut to pass a reference in a way that allows for modifying the value.

But – surprise! – there are rules for borrowing. And the compiler will enforce the rules – the tyrant that it is – so we should be aware of them.

So, within the same scope:

1a. One mutable borrow at a time.

This will not compile. If it did, it could lead a race condition. Because there’s no mechanism used here for synchronizing access to the data value.

1A++.

It gets even worse: You can’t have read-only references at the same time with a mutable reference.

The code below works fine (mutable variable, no mutable borrowing, multiple read-only references):

The following though does not work:

Because we have a mutable reference to s and it must be the only reference to s.

So at any given time, you can have either one mutable reference OR any number of immutable references.

`1b. No Invalid References

An artistic depiction of a Dangling Pointer:

Beware of chasing the dangling carrot in front of your nose | by Tom Kupka | Designing Kiwi.com | Medium

What is a dangling pointer? It’s a step to towards nothingness.

Let’s look at the example bellow:

The dangle function creates a string value, owned by s.

Then it returns a reference to s. But remember that s goes out of scope once dangle() completes, so the value “hello” gets cleaned up from memory.

Then what does &s point to?

We will never know because the compiler refuses to build a binary. And the application users won’t get an exploitable binary.

Lifetimes

All the rules we’ve discussed so far are emerging properties of the Rust compiler’s inner workings. It’s like how you have rules for driving your car – they exist because of the nature of all the machinery under the hood.

While Ownership and Borrowing are fine concepts, they do not cover all cases. There are things that the compiler simply can’t infer based on those rules only. Even if it theoretically can do deeply nested inference, that is super super slow. So it won’t chose to.

In that case, it needs US – the benevolent, intelligent and beautifully handsome (or handsomely beautiful) programmers to give it a push.

But first, we need to dive into how the compiler works when it comes to borrowing – and talk about the concept of life.

Épinglé sur The Life Cycle of Plants

Not that one exactly.

The compiler needs to keep track of where a variable MIGHT get used. In all the places in the code where that variable MIGHT get used, the variable is considered & marked as LIVE.

The same concept exists in Borrowing. A reference is LIVE at some parts of the code and … dead everywhere else.

An easy way to think about it is by looking at the lines of code. Obviously the compiler does not use “line of code” to reason about but it’s a good enough approximation for our purposes.

We note that:

x is LIVE on lines {1, 2, 3}. 

r is LIVE on lines {2,3}

The set {1, 2, 3} is larger than the set {2,3}

Consider this though:

Here, things are a bit different:

x is LIVE on lines {3,4} 

r is LIVE on lines {1,2,3,4,5,6}

r outlives x, but its value comes from a loan from x.

This is the dangling pointer issue we saw earlier.

The critical point (pun intended) here is that the compiler can reason about it. That’s because it has a way of figuring out when r & x are LIVE.

This area of code where a variable is LIVE, is called a lifetime.

OK, so we understand the compiler was able to calculate here the lifetime of a variable. But let us look at this code:

This code does not compile. Why not this time?

The problem is that the compiler can’t calculate the lifetime for the value of z (i.e. the return value of max()).

Is the lifetime of z related to the lifetime of s1 or the lifetime of s2? We can’t tell before runtime.

As an aside, yes, technically, we COULD have the compiler analyze all the calling cases of max() and have it decide that in this specific case, the lifetime of z should equal the lifetime of s1. And it would work for this toy code. Now imagine asking the compiler to do that for a real code-base. You'd be taking very very long compilation breaks.

Therefore the compiler stops and asks: “Oh mighty coder, shed your light here“.

The compiler wants us to enhance the function signature.

So we need a way to tell the compiler, “you know what, have the function require that the lifetime of its return value is related to s1 & s2 somehow“.

And we’ll do that by using lifetime annotations.

They look like generics and they’re ugly looking. Thankfully, we don’t have to use them often.

Hey, this compiles now! Sweet.

The compiler knows that the lifetime of the return value of max() MUST be such that matches s1 & s2.

Because the function now is expressively clear, the compiler can reason about the code main(). And in this case, the program compiles.

DO NOTE: Lifetime annotation does not enforce. It requires, in the sense of a contract. It’s up to the programmer to make sure the contract requirements hold when calling the function, lest the compiler throws a fit.

If main() was a bit different, the compiler would stop us.

The compiler stopped us from making a mistake because it knows exactly what the function max() needs in term of its parameter lifecycle.

Outro

Rust offers an innovative, breakthrough solution to the “Fast or Safe?” dilemma.

The good news

The compiler is there to help us.

The bad news

It takes a bit of practice to learn how to ride a bike well.

Is it worth it?

All in all, Rust is a fun and enjoyable systems programming language. It also happens to be fast & safe.

That’s not to say that Rust is the end-all-be-all of software development though. High-level languages thrive for good reasons. Rust’s domain isn’t the same and it doesn’t try to replace them. The Rust domain is the lower level applications and it does not pretend to conquer the world. Don’t use Rust to replace Python or Node because your productivity will take a hit. At least, for now. Rust lang is evolving and it may come a time that it does make sense to use it in the core domain of managed languages.

Rust from a JavaScript perspective

 A tongue in cheek walk-through

I’ve been having a lot of fun using Rust for writing small tools. My day to day work involves a ton of JavaScript and Rust provides a familiar feeling, so trying out Rust was an easy decision to make. But at the same time, actually doing meaningful work in Rust requires a lot of rethinking on how to structure and reason about your code. The compiler – true to its call – is merciless; yet, for some reason, it emerges quite a pleasure in tinkering your code to make it so that it – finally! – compiles.

In this post, I am documenting – albeit in a bit funny way – some thoughts in my journey so far in Rust land, coming from the viewpoint of a hardcore JavaScript enthusiast.

The good news

Modern Rust *appears* pretty similar to modern JavaScript. You declare your variables with let, your functions look pretty similar, types aren’t a stranger because of TypeScript , there’s async/await and overall it exudes a sense of familiarity.

The bad news

Unfortunately, the good news section ends pretty fast.  The core of the issue isn’t  syntax, but the way Rust reasons about the internals of your program. In high-level languages, you get cushy abstractions that shield you from the way computers work. And that makes perfect sense; if your goal is to commute to your workplace, you just need to know how to drive the car  – you don’t have to fiddle with the internals of its combustion engine. In contrast, on low-level languages you get the bolts and screws and you have to be a car mechanics to drive to the grocery store.

Each approach has its own set of advantages and disadvantages; for that reason they are mostly used in different problem domains.  Rust aims to sit right in the middle. It gives you access to all the raw facilities while also providing the lucidity and ease of high level abstractions. But – and there’s always that but – it needs you, the developer, to pay a price for this: you must learn a new way of reasoning about your program.

Let there be memory management

A computer program relies on reading and writing values in memory. There’s no way around it, just like there’s no way to go about cooking without ingredients. Therefore, somehow we have to procure the ingredients, slice them in the right quantities, throw them into the pan by the proper order and eventually take care of the mess in the kitchen after we’re done.

High-level languages are like dear parents. They are there to patiently clean up for you, so that you can perform your …art without getting your hands dirty.  Mindfully, they provide you with a nice & helpful Sancho Panza – lovingly dubbed as “the garbage collector” – for some much needed backup as you fiercely engage those aggressive windmills.

When it comes down to memory management, Rust is like “meh – real chefs clean up their own trash”. And there is good reason for this, because a garbage collector comes with its own set of esoteric issues that can hurt you when you least expect it. At the same time though, Rust draws from past experience of other languages and accepts that forcing the programmer do the memory management is as wise as commissioning Douglas Adams to write the “Starship Titanic”.

In order to get around both humans and too-sophisticated-for-our-own-good code, Rust came up with a new scheme. It can be summed up as “We are all Lannisters now”.

A rose by any other name would smell as sweet

… as rightly so says Shakespeare. Yet, what about the …not so sweet smells? Does the saying stand true for the negative case? Considering the long tradition in humankind’s use of euphemisms, we can be relatively sure that it is not so.

A 55-meter long ship somewhere in the Pacific, posing as a tiny island to escape detection in WWII; it actually worked.

Rust playfully engages in the time-honored art of misdirection by words.  Prominent in its literature is the concept of “ownership”.  Yet in Rust, “ownership” has no big benefits for the owner. Instead, “ownership” means that some relative passed down their debt to you; and now you own that debt. And – make no mistake – there’s no defaulting on debts here.

In the Rust’s Westeros continent (dare I say, Westerust ?), there are tiny, small, big and huge fiefdoms. The twist here is that each and every fief is occupied by Lannisters. Fiefs do their own things internally and when they need “goods” from the outside, they will incur a debt in order to obtain said goods. In the end, the debt must be paid back to the Westeros Gods. Rust is the draconian Queen of this world – it will oversee everything from high up to make sure the Gods get their dues.

In Rust, every scope is its own fief. Variables (memory) that is procured from the system and used inside the scope stays – like secrets in Vegas – only inside that scope. If a scope is done and no longer serves a purpose, the memory is returned to the system. The compiler makes sure of that because it transparently injects code in your hand-crafted fiefs that does exactly that. So it’s impossible for your scopes to escape their fate. This is the iron rule of Rust (hurrah for the levels of irony).

But can it run Crysis?

Let’s see how it works.

We have 2 scopes here. The outer one from main and an explicit inner one for demonstration. Rust ownership here works like this:

  1. main owns a and b
  2. a wants to work with the inner scope, so main transfers ownership of a to  inner scope
  3. inner scope does its thing with a and then completes.
  4. Rust’s hidden code drops a.
  5. main does its thing with b and completes.
  6. Rust drops b.

Notice that the debt of ownership is paid back to the system, not to the scope it  originated from. Ownership of a does not return to main scope.

But wait, this sounds very dangerous. What if we had the following code?

Here, main scope wants to use a again, but we said that Rust has already dropped a when the inner scope ended.

Won’t the program just crash and burn when it reaches that point in execution?

Yes, it will. But as the Spartans responded to Alexander’s father, King Philip II of Macedon: IF it reaches to that point.

The Ultimate Bureaucrat

The Rust compiler is a proud disciple in the tradition of Legalism, so much so that Han Fei-tzu would have exuberantly outlawed all other languages.

Nothing happens in Rust-land unless the Compiler stamps it with its seal of approval. The Compiler will thoroughly check everything and evaluate if the program is safe to run. Only if satisfied, will it ever produce an executable.

In our case, it realized that our debt handling skills were subpar, and our request for a binary got refused.

It is the role of the programmer to learn what the Rust Law is and make sure that it is adhered. All its ins and outs, all its quirks, all its assumptions. Otherwise, the Rust Compiler will yell at us. On the other hand, the Rust team has been trying to make things accessible by creating a ton of syntactical sugar and – most importantly – legible error messages. That, coupled with decent documentation and a great community makes working with the language fun.

The Rust RPG

In the Rust continent, variables are the players. Players must belong to some class – a mage, a priest, a struct. Moreover, each player may have different equipment. That makes sense; you could have two priests, one with a staff and one with a wand, right?

Remembers that dbg!() from before? That’s a macro, a rough equivalent of JavaScript’s console.log. Let’s create our own Typed variable and log it.

We created a struct, which is basically a Type. Then we created an object of that Type. Finally, we asked to log that object.

Nope. Our player is so much of a noob that it does not even have the ability to provide debug info. Really! Amazing…

The key point here is that your handcrafted variables all start as Level 1 peons, with no equipment. And here is where the equipment (Traits, in Rust lingo) come into play.

Let us press F.

This time, it works. The only difference is the line at the top. Here, we equip Noob with the Debug trait. Now, our player is eligible to be logged – what a milestone!

Rust has a ton of equipment. Some more prevalent that others. And, as expected, it lets you forge new ones by using your very own designs.

Some Traits can be generated automatically by the compiler for us. This is what happened here – the compiler was able to relieve us from trivial work. Other times, the actual implementation is left to you. You want a graphite armor for your mage? Not a problem, certainly is doable but you have to provide the code on how it actually works.

Traits are deeply embedded in the fabric of Rust. Let us circle back to the ownership example that blew up  earlier. Reading the error message carefully, we notice that the compilers explains to us that the variable’s ownership had to be `moved` because a String does not implement that Trait Copy.  Otherwise, the compiler – wise as it is – would have made a copy of it instead of a move.

The Copy trait means that you take a section of memory and you memcpy it somewhere else, operating directly on bytes.

Ok, so String doesn’t have a Copy trait, do we just need to tell the compiler to provide it with one? Unfortunately, no. Copy is too low level for a String to safely use it. The compiler knows that is so, so we get nothing. Of course, Rust wouldn’t be a useful language if the story ended there. There’s a more explicit Trait that does almost the same thing – Clone, as it’s called. String does have the Clone trait, so we simply want to use that instead of Copy.

We will adjust the code a bit to look like this:

What happens here is that the compiler sees that we want to use a inside the `inner scope` but now it also sees that we can do our work perfectly fine with a clone of it instead of the actual a. So we have the following:

  1. a is owned by main
  2. a.clone is created and borrowed to inner scope
  3. inner scope does its thing and completes
  4. Rust drops a.clone
  5. main uses a with no problem because a was always owned by it.

Beautiful. Of course, that’s not the only way to solve this particular issue but it ties nicely with our little exploration of ownership and Traits.

Before wrapping up this post, there’s one more thing we should touch upon a bit. We talked about ownership and how it relates to scope, but truth is this was just a simplification. Rust employs the concept of Lifetimes to keep proper track of ownership. It just happens that most of the time, lifetimes and scopes coincide. Sometimes though, the compiler needs help. Therefore we are allowed to work directly with those lifetimes – in some cases we must.

FIN?

Absolutely not! If Rust is an iceberg, this can’t be considered even the tip. If Rust is a cake, this isn’t even the glossing. If Rust is a cryptocurrency, this isn’t even a satoshi. But I am clearly not conjuring inspirational metaphors today, so let’s let it rest.

In my experience, learning Rust is fun. There is a steep curve but at the same time, the amount of complexity is “just right” for the value you get by investing your time in it. I will definitely be continuing my journey with Rust!

 

 

An Autumn Excursion in Space

Being a complete neophyte in the world of music production, I am getting my feet wet today by releasing “An Autumn Excursion in Space”.

I have been a huge admirer of Brad Mehldau‘s work and I wanted to create something in the spirit of “Taming the Dragon”.

“An Autumn Excursion in Space” is about the excitement of discovery and the uneasiness that emerges as we encounter the world’s natural adversity. Harmonies refuse to resolve. But space is an always forward leaning path. Motion creates hope. Hope encounters resistance. A struggle ensues. Luckily, we survive.

Listen on SoundCloud: https://soundcloud.com/kapolos/an-autumn-excursion-in-space