You may be getting as sick of reading it written out full like that as I'm getting of writing it, so I think it's time to introduce a trait, to make things a little more readable, and to let us add some extensibility to our parsers. We've added a lifetime there, because the type declaration requires it, but a lot of the time the Rust compiler should be able to infer it for you. As a rule, try leaving the lifetime out and see if rustc gets upset, then just put it in if it does.
The lifetime 'a , in this case, refers specifically to the lifetime of the input. Now, for the trait. We need to put the lifetime in here as well, and when you're using the trait the lifetime is usually always required. It's a bit of extra typing, but it beats the previous version. It has just the one method, for now: the parse method, which should look familiar: it's the same as the parser function we've been writing.
To make this even easier, we can actually implement this trait for any function that matches the signature of a parser:. This way, not only can we pass around the same functions we've been passing around so far as parsers fully implementing the Parser trait, we also open up the possibility to use other kinds of types as parsers. But, more importantly, it saves us from having to type out those function signatures all the time. Let's rewrite the map function to see how it works out. One thing to note here in particular: instead of calling the parser as a function directly, we now have to go parser.
Otherwise, the function body looks exactly the same, and the types look a lot tidier. There's the new lifetime 'a' for some extra noise, but overall it's quite an improvement. Same thing here: the only changes are the tidied up type signatures and the need to go parser. Actually, let's tidy up pair 's function body too, the same way we did with map.
The code above is identical in effect to the previous version written out with all those match blocks. With pair and map in place, we can write left and right very succinctly:. We use the pair combinator to combine the two parsers into a parser for a tuple of their results, and then we use the map combinator to select just the part of the tuple we want to keep. Rewriting our test for the first two pieces of our element tag, it's now just a little bit cleaner, and in the process we've gained some important new parser combinator powers.
We have to update our two parsers to use Parser and ParseResult first, though. For identifier , just change the return type and you're done, inference takes care of the lifetimes for you:. Let's continue parsing that element tag. What's next? That should be our first attribute pair. No, actually, those attributes are optional. We're going to have to find a way to deal with things being optional. No, wait, hold on, there's actually something we have to deal with even before we get as far as the first optional attribute pair: whitespace.
Between the end of the element name and the start of the first attribute name if there is one , there's a space. We need to deal with that space. So this seems to be a good time to think about whether we could write a combinator that expresses the idea of one or more parsers. We've dealt with this already in our identifier parser, but it was all done manually there.
Not surprisingly, the code for the general idea isn't all that different.
Learn the best of web development
The code does indeed look very similar to identifier. First, we parse the first element, and if it's not there, we return with an error. Then we parse as many more elements as we can, until the parser fails, at which point we return the vector with the elements we collected. Looking at that code, how easy would it be to adapt it to the idea of zero or more? We just need to remove that first run of the parser:.
At this point, it's reasonable to start thinking about ways to generalise these two, because one is an exact copy of the other with just one bit removed. Here, we run into Rust Problems, and I don't even mean the problem of not having a cons method for Vec , but I know every Lisp programmer reading that bit of code was thinking it.
No, it's worse than that: it's ownership. We own that parser, so we can't go passing it as an argument twice, the compiler will start shouting at you that you're trying to move an already moved value. So can we make our combinators take references instead? No, it turns out, not without running into another whole set of borrow checker troubles - and we're not going to even go there right now.
And because these parsers are functions, they don't do anything so straightforward as to implement Clone , which would have saved the day very tidily, so we're stuck with a constraint that we can't repeat our parsers easily in combinators. That isn't necessarily a big problem, though. Let's leave that as an exercise for the reader, though. Another exercise might be to find a way around those ownership issues - maybe by wrapping a parser in an Rc to make it clonable? Actually, hold on a moment. We don't really want to parse the whitespace then parse the attributes.
But if there's an attribute, there must be whitespace first. Lucky for us, there must also be whitespace between each attribute, if there are several, so what we're really looking at here is a sequence of zero or more occurrences of one or more whitespace items followed by the attribute. Why is that silly? Because whitespace is also line breaks, tabs and a whole number of strange Unicode characters which render as whitespace.
Rust is one of the best C replacements we currently have · musicmatzes blog
Three, we can be clever, and we do like being clever. This has the added bonus of making it really easy to write the final parser we're going to need too: the quoted string for the attribute values. And the pred combinator also doesn't hold any surprises to our now seasoned eyes. We invoke the parser, then we call our predicate function on the value if the parser succeeded, and only if that returns true do we actually return a success, otherwise we return as much of an error as a failed parse would. Let's indulge ourselves in some brevity and call them space1 and space0 respectively.
With all that sorted, can we now, at last, parse those attributes? Yes, we just need to make sure we have all the individual parsers for the components of the attributes. We're short one quoted string parser, though, so let's build that. Fortunately, we've already got all the combinators we need to do it. The nesting of combinators is getting slightly annoying at this point, but we're going to resist refactoring everything to fix it just for now, and instead focus on what's going on here.
The outermost combinator is a map , because of the aforementioned annoying nesting, and it's a terrible place to start if we're going to understand this one, so let's try and find where it really starts: the first quote character. That's our opening quote. The second part of that right is the rest of the string. So the left hand part is our quoted string. And, between the right and the left , we discard the quotes from the result value and get our quoted string back.
But wait, that's not a string. Let's just write a quick test to make sure that's all right before we go on, because if we needed that many words to explain it, it's probably not something we should leave to our faith in ourselves as programmers. That, finally, is all we need for parsing attributes. First, let's write a parser for an attribute pair. Let's see if we can build one. Without even breaking a sweat!
Zero or more occurrences of the following: one or more whitespace characters, then an attribute pair. We use right to discard the whitespace and keep the attribute pair. Actually, no, at this point in the narrative, my rustc was complaining that my types are getting terribly complicated, and that I need to increase the max allowed type size to carry on.
A Story of Rust
It's a good chance you're getting the same error at this point, and if you are, you need to know how to deal with it. Fortunately, in these situations, rustc generally gives good advice, so when it tells you to add! Actually, just go ahead and make it!
- The Book of Romans in Outline Form (The Bible in Outline Form).
- Underground Storage Tank Cleanup Fund?
- Rust in the flower garden | UMN Extension?
- Rust is one of the best C replacements we currently have.
Full steam ahead, we're type astronauts now! At this point, things seem like they're just about to start coming together, which is a bit of a relief, as our types are fast approaching NP-completeness. So let's do the single element first, deferring the question of children for a little bit. With that in place, we can quickly tack the tag closer on it to make a parser for the single element.
Hooray, it feels like we're within reach of our goal - we're actually constructing an Element now! It's clear we can no longer ignore this problem, as it's a rather trivial parser and a compilation time of several minutes - maybe even several hours for the finished product - seems mildly unreasonsable.
If you've ever tried writing a recursive type in Rust, you might already know the solution to our little problem. A very simple example of a recursive type is a singly linked list. You can express it, in principle, as an enum like this:. As far as rustc is concerned, we're asking for an infinite list, and we're asking it to be able to allocate an infinite list. In many languages, an infinite list isn't a problem in principle for the type system, and it's actually not for Rust either. The problem is that in Rust, as mentioned, we need to be able to allocate it, or, rather, we need to be able to determine the size of a type up front when we construct it, and when the type is infinite, that means the size must be infinite too.
The solution is to employ a bit of indirection. Instead of our List::Cons being an element of A and another list of A , instead we make it an element of A and a pointer to a list of A. We know the size of a pointer, and it's the same no matter what it points to, and so our List::Cons now has a fixed and predictable size no matter the size of the list.
And the way to turn an owned thing into a pointer to an owned thing on the heap, in Rust, is to Box it. The generic function signature looks like this:. We rephrased the type annotation for v , w and the output using T : this means that we expect the same element type for the elements of both input slices and for our output value, just as we wanted.
The whole function looks like this now: RP. T is a type parameter, nothing more than a placeholder for a concrete type, like i32 or f Can we sum them together? Can we compare them? We could make a list of types satisfying these conditions: i32 , i64 , usize and a bunch of others. A whitelist , if you want. But what happens when someone else comes up with a new numerical type with a legitimate concept of addition and multiplication e.
We would have to modify our code to include its type into our whitelist - this would greatly hinder the reusability of what we are writing.
We need a way to define shared behaviour for the type parameters of our functions - this is where traits come into the fray. Do you want to print your variable with println? Its type must implement the Debug trait. Do you want to clone it with clone? Its type must implement the Clone trait.
Do you want to implement a new routine to read file from disk? If it implements the Read trait, it can replace std::fs::File , from the standard library. Traits are everywhere - but how do they actually work? How does a trait encode behaviour? How does a type implement it? A trait definition includes a list of method signatures. We want to define a trait to distinguish those types for which it make sense to sum together two values.
A first draft looks like this: RP. If Addition had two methods instead of one in its definition both of them would have had to be implemented in the impl Addition for i32 block - if we forget to implement some of the methods in a trait definition the compiler screams, loudly. In other words, if T implemented the Addition trait! Given this additional piece of behaviour, we can flesh out the function body: RP. We are using the only piece of information we have on T : it implements Addition , hence we can call sum method on its values.
The compiler can verify it, looking at our function body and at the definition of Addition , hence it does not complain. If we try it out using RP. Nonetheless, nothing prevents a fellow Rust programmer, in another corner of the world, from implementing the Addition trait that we have exposed in our project for one of its own types - thus unlocking all the functionalities we have already built it.
We listed some common Rust traits before, but we failed to mention that a lot of operators are actually traits in disguise! The standard library exposes a trait for addition, called Add. But there was one more thing: With Rust we were able to use resources efficiently and there was already the plan to move to Kubernetes. Being able to have small pods running on Kubernetes could be a real cost saver. There was a lot of communication with our lead and we got valuable feedback on the topics where we might need a bit more reasoning.
Well, things were moving slowly and the end of the year was near. At that point in time we had serious doubts that we would ever use Rust for productive systems. It was at the end of when it was announced that the teams would be restructured due to changing requirements. We were a team of six developers and would be reduced to four.
That was really unexpected and I have to admit that I did not really know what to respond to that.
- Explained: Futures in Rust for Web Development!
- Ecstatic Prophecy.
- Herlock Sholmes: The Book.
- One Program Written in Python, Go, and Rust – Nicolas Hahn;
- Like Haiku: Haiku ? Tanka ? Other Verse;
- Colonial America: Life in the Colonies Unit Study?
- Search Tricks?
Since we were planning to replace our old system with a new one, we almost immediately started to implement the first service we needed. It was a rather simple CRUD service, which was a good opportunity to onboard some of the team members to Rust. The service was ready to be used more quickly than expected, even though it was not yet fully finished.
Since we needed more applications to reach our goal, we started to implement the smallest applications in parallel, thereby gradually increasing the difficulty level for the team to the final service which fully utilizes non-blocking IO. In the end we managed to reach our goal in time, thereby introducing a new technology.
Currently we have two REST services, a streaming application and multiple batching applications written in Rust all running on Kubernetes. The new applications have been live serving data for two countries over and are expected to serve even more countries in the near future. The resource usage of our applications is far below our former Scala services and reduce costs remarkably.
Refactoring and even reengineering can be done quite fearlessly. The compiler is very helpful and even suggests solutions. A newcomer coming from Scala or C already knows concepts like closures and the Iterator API which makes things a lot easier. And there is the borrow checker. Given enough support, newcomers can learn to handle it while still being productive. When starting a project it is beneficial to have an experienced Rust developer on board and to not just start from scratch.
Related Rust: One
Copyright 2019 - All Right Reserved