Skip to Content
Technical Articles

Learning Rust with CAP (Part 1)

What is this blog series about?

 

A (private) hobby project where I’m building a simple server in the programming language Rust, borrowing some concepts of the SAP Cloud Application Programming Model (CAP).

 

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
Thus, it would be imprudent to only stick with the present set of common knowledge, so let’s see what’s out there.

 

What is Rust?

 

Rust (Graydon Hoare, 2010) is a community-driven multi-paradigm compiled programming language with “zero cost abstractions” (no additional runtime overhead).

The performance characteristics are comparable to languages like C and C++.

 

And it feels very high-level – it comes with
  • 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`)
and much more.
What’s so special about it is that it guarantees memory safety without a garbage collector, accomplished by the concept of ownership, leading to low memory footprints and predictable performance.
Please have a look at the official website for more information.
According to Stack Overflow’s developer survey, it’s the most loved programming language for the fifth consecutive year.

 

Why CAP?

 

When learning new techniques it’s best to apply them to well-known situations first.

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

 

Let’s begin our journey with a guided tour of some of the source code of our Rust-CAP server.
It’s impossible to explain all nitty-gritty details in a few blog posts, so I’ll try to keep it short and crisp
and focus on the highlights.
For the sake of clarity I’ll depict unimportant things with `…`.
The working source code can be found on GitHub.

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

 

Rust has excellent documentation, so let’s see what this type is.
It boils down to
enum Result<T, E> {
   Ok(T),
   Err(E),
 }
which is very similar to error handling in functional languages like Haskell or OCaml.
It’s an enum, meaning it can either be `Ok(T)` or `Err(E)` with generic types `T` and `E`.
If a call to a function returns `Ok(T)` we know that everything went fine and can continue
with `T`.
This is usually done with pattern matching:
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)
};
There’s also syntactic sugar for this:
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.

 

Let’s add some routes
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.

|...| { ... }
To start our server we can simply use the command-line tool `cargo`.
$ cargo run

GET http://127.0.0.1:8080/

Please use proper routes.
Now we’ve got a running server.
Let’s concentrate on the main building blocks of CAP.

Core Query Notation

One of the most important things in CAP is our core query notation, or `CQN` in short.
It let’s you define queries in an abstract way to be run against databases or REST/OData services.
Let’s see how that can be done in Rust.
#[derive(Debug)]
enum CQN {
  SELECT(SELECT),
  INSERT(INSERT),
}
Our `CQN` is either a `SELECT` or an `INSERT`, we’ll ignore `UPDATE` and `DELETE` for now.
The strange `#[derive(Debug)]` line is a macro. You can think of it as a way to automatically
derive additional Rust code, minimizing the amount of boilerplate code you need to write.

This particular macro lets you print this enum to the console whenever you want.

Let’s see how `SELECT` is defined.

Structs

#[derive(Debug)]
struct SELECT {
  entity: Identifier,
  columns: Vec<Identifier>,
  filter: Vec<String>,
}
A struct is basically a group of fields with arbitrary types.
Here, we have something that defines an entity, the wanted columns and a filter.
`Vec<String>` just means a vector of strings, think of it as an array of variable size.

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

field is optional.

Having a look into the Rust documentation yields:

enum Option<T> {
  None,
  Some(T),
}

 

So an Option of type `T` is either `None` or `Some(T)`. Again, exactly how functional languages do it.
What’s great about structs is that you can define methods on them.
impl SELECT {
  fn from(entity: &str) -> SELECT {
    SELECT {
      entity: Identifier { reference: vec![entity.to_string()], alias: None },
      columns: vec![],
      filter: vec![],
    }
  }
  ...
}
Think of this as a static method `from` which produces a `SELECT` itself, kind of like a named constructor.
You can call it like this:
let select = SELECT::from("example_entity");
It’s also possible to define methods on instances, the only thing you need to do is to change the arguments to include some form of `self`. Here, it’s a mutable reference `&mut self`, that means you’re allowed to mutate itself.
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
  }
  ...
}
We use `map` to change a vector of strings to a vector of `Identifier`s. As simple as that.

 

Polymorphism

In Rust, polymorphism is achieved through generics and traits. You already saw generics in our enums, it also works in functions and structs.
A trait is similar to an interface, it’s something a struct can implement. Take this example:
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.

 

Let’s implement `SQL` for our `SELECT` struct.
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
  }
}
Now, every `SELECT` struct can be converted to an SQL string.
If you’re writing a function that just needs something which implements the `SQL` trait, you can simply write the following.
fn my_function(something: &impl SQL) {
  println!("My SQL string is {}", something.to_sql());
}

Automatic Testing

Rust comes with a testing framework built-in, you only need to use a few macros.
#[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")
  }
}
Run `cargo test` and see if it works.
$ 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.

Next Steps

In the next blog post we’ll integrate more CAP concepts into the world of Rust. Stay tuned!
/
4 Comments
You must be Logged on to comment or reply to a post.
  • 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