Technical Articles
Learning Rust with CAP (Part 1)
What is this blog series about?
But Why?
I love learning programming languages, especially the ones bringing novel ideas to the table since the past has repeatedly shown that radical software revolutions can happen.
Just to give three examples which changed your life:
- Dijkstra’s letter “Go To Statement Considered Harmful” (1968) which led to consensus about basic control flow statements
- the programming language Smalltalk (Alan Kay, 1972) introducing object-oriented programming
- LISP (John McCarthy, 1958) as the first functional programming language
What is Rust?
The performance characteristics are comparable to languages like C and C++.
- memory safety
- an advanced type system (with type inference)
- functional features (like closures, algebraic data types and pattern matching)
- support for asynchronous programming (async/await)
- generics and a trait system
- a module system
- a macro system
- productivity tools (for example a very helpful compiler, package manager, code formatter and documentation generator, all bundled in the tool `cargo`)
Why CAP?
Since I’m currently working on CAP, I’ll build a server borrowing some of its concepts.
Our server will
- read a compiled CDS model (in JSON representation)
- register simple generic CREATE and READ handlers
- use PostgreSQL as a database
- be super fast
A Guided Tour
Server
Let’s start with a simple web server.
There are numerous libraries/frameworks out there, this blog post gives you a good overview. I chose `tide` because it’s quite slim and I’m not using a lot of features anyway.
All Rust programs must have a `main` function which can return something called an `io::Result<()>`.
fn main() -> io::Result<()> { ... }
Error Handling
enum Result<T, E> {
Ok(T),
Err(E),
}
let number_str = "10";
let number = match number_str.parse::<i32>() { // try to parse the number to an i32 (this will return a Result)
Ok(number) => number, // if it works, extract the number
Err(e) => return Err(e), // if not, return Err(e)
};
let number_str = "10";
let number = number_str.parse::<i32>()?;
Notice the `?` operator.
Now let’s head back to our server.
fn main() -> io::Result<()> {
task::block_on(async { // spawns a task and blocks the current thread on its result, now we can use `await`
let state = State { ... }; // we can use this state object in our request handlers
let mut app = Server::with_state(state); // start a server with state
add_routes::add_routes(&mut app, ...); // add some routes
let url = "127.0.0.1:8080";
println!("Server listening on http://{}", &url);
app.listen(&url).await?;
Ok(())
})
}
Rust essentially has no runtime. In contrast to Node.js, to write an async program one needs an executor from a library, this is where `task` comes from.
fn add_routes(app: &mut Server<State>, ...) -> () {
app.at("/").get(|_req: tide::Request<State>| async move {
Ok(
tide::Response::new(StatusCode::Ok)
.body_string("Please use proper routes.".to_string()),
)
});
}
In Rust, closures can be created with the following syntax.
|...| { ... }
$ cargo run
GET http://127.0.0.1:8080/
Please use proper routes.
Core Query Notation
#[derive(Debug)]
enum CQN {
SELECT(SELECT),
INSERT(INSERT),
}
This particular macro lets you print this enum to the console whenever you want.
Structs
#[derive(Debug)]
struct SELECT {
entity: Identifier,
columns: Vec<Identifier>,
filter: Vec<String>,
}
Our `Identifier` is constructed to identify entities or columns.
#[derive(Debug)]
struct Identifier {
reference: Vec<String>,
alias: Option<String>,
}
The `Option` enum is very similar to our `Result` enum. We use it to say that this
Having a look into the Rust documentation yields:
enum Option<T> {
None,
Some(T),
}
impl SELECT {
fn from(entity: &str) -> SELECT {
SELECT {
entity: Identifier { reference: vec![entity.to_string()], alias: None },
columns: vec![],
filter: vec![],
}
}
...
}
let select = SELECT::from("example_entity");
impl SELECT {
...
fn columns(&mut self, columns: Vec<&str>) -> &mut Self {
let cols: Vec<Identifier> = columns
.iter()
.map(|col| Identifier {
reference: vec![col.to_string()],
alias: None
})
.collect();
self.columns.extend(cols);
self // implicit return
}
...
}
Polymorphism
trait SQL {
fn to_sql(&self) -> String;
}
Here, we define the `SQL` trait. If you want to implement it for a particular struct, you have to implement the `to_sql` function with the same signature. It’s also possible to provide default implementations, similar to abstract classes.
impl SQL for SELECT {
fn to_sql(&self) -> String {
let from_sql = &self.entity.reference.join(".").to_string().replace(".", "_");
let mut res = match &self.columns.len() > &0 {
true => {
let cols: Vec<String> = self
.columns
.iter()
.map(|c| c.reference.join(".").to_string())
.collect();
format!("SELECT {} FROM {}", cols.join(","), &from_sql)
}
false => format!("SELECT * FROM {}", &from_sql),
};
if &self.filter.len() > &0 {
res = format!("{}{}", res, format!(" WHERE {}", &self.filter.join(" ")));
}
res
}
}
fn my_function(something: &impl SQL) {
println!("My SQL string is {}", something.to_sql());
}
Automatic Testing
#[cfg(test)] // this is our testing section
mod tests {
use super::*;
#[test] // define a test
fn select_with_col_to_sql() {
let mut select = SELECT::from("example_entity");
select.columns(vec!["col1", "col2"]);
assert_eq!(select.to_sql(), "SELECT col1,col2 FROM example_entity")
}
}
$ cargo test
test cqn::tests::select_with_col_to_sql ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Conclusion
We’ve seen some basic features of the Rust programming language and how easy it is so create a running server and implement some of CAP’s elements. I hope I could convince you that Rust does not feel as low level as one would expect given its high performance and that it comes with useful features and tools.
?
David,
Many thanks for writing this blog post. I'm looking forward to a few more!
A few Sunday afternoon thoughts that occurred to me having read your post:
You chose a topic that is well known to you as a vehicle for exploration. You know CAP well and Rust less well. You are not struggling on multiple fronts. This reminds me of Kent Beck choosing to implement a unit testing framework when he was a Python newbie as a means of becoming fitter in Python. (Chapter 18 of Test-Driven Development by Example - Kent Beck)
You chose to post about a language that I'd guess isn't widely known within the SAP community. There are a lot of languages with their own grammars and expressive possibilities. Just like human languages, learning another computing language broadens your world view and helps to inform the use of the languages you already know.
Whilst you don't mention it the following link to the official Rust Language Documentation - "The Book" might well be helpful to those who would like to follow along ..
Thanks for the encouragement to explore outside of the present set of common knowledge.
Hello Andrew,
Thanks a lot for this nice comment. Indeed, I try not to fight on too many fronts.
There is an awesome talk by one of my developer heroes, John Carmack, in which he talks about the functional programming language Haskell. He also tried to apply something new to something he knows – so he rewrote Wolfenstein 3D in Haskell.
You’re absolutely right about learning new languages. It’s never only about new syntax, it’s more about having a different view on the same problems.
Haskell and Elm taught me for example to keep functions pure (same input yields same output, no side effects, just like a mathematical function), this can easily be applied to a lot of languages, even ABAP which doesn’t even have higher-order functions and other functional concepts.
Erlang taught me how to build robust systems with its “let it crash” philosophy and that object orientation is not about classes but about messaging (originally coming from Smalltalk).
Thanks for mentioning the Rust book, indeed this should be the very first start for every new Rustacean (as Rust programmers call themselves).
Best regards,
David
... and a shout-out to DJ Adams for advocating functional programming and giving insights into lesser-known topics!