Introduction

What This Book Is

This work is a collection of examples that demonstrate similar concepts, idioms, design patterns in diffrent programming languages. The goal is to get the feeling of how handy these languages are for developers.

What This Book Is NOT

This work doesn't attempt to explain any theory of programming languages nor be any scientific. It is not indended to get deep into details. It should not be considered as a reference design. It also doesn't make any considerations about performance of the applications. Finally, usability shouldn't be the only factor to be considered when deciding which language to use.

Contributions

Feel free to open a pull request or discussion. Don't hesitate to give a star if you like this book.

Languages

Following programming languages have been looked at in this book:

Python

  • Interpreted1
  • Dynamically typed2
  • GC-based memory managemnet
  • Error handling based on exceptions
1

Although there are compilers like numba and codon that can turn Python into machine code.

2

Annotations in conjunction with type checker like mypy can be employed to turn Python into statically typed language. Standard library supports type hints through typing module.

Rust

  • Compiled
  • Statically typed
  • Ownership-based memory managemnet
  • Error handling based on return values

Crystal

  • Compiled
  • Statically typed
  • GC-based memory managemnet
  • Error handling based on exceptions

Snippets

Snippets presented in this book are available in the repo. Most of them are single files that can be directly run or compiled. The goal is to use standard libraries unless it is missing some essential functionality. In remaining cases we use third-party libraries and prioritize those that are the most popular.

Python

Code is prepared mostly for CPython ~3.8. Most of the snippets contain mypy-style type hints. However, in some cases they are skipped for clarity.

File-based examples can be run as: python3 example.py

Project-based examples can be run as:

cd example_py
poetry install
poetry run python -m example

Which requires poetry to be installed.

For checking type hints, mypy can be used.

Rust

Code is prepared mostly for edition 2021, rustc ~1.73.

File-based examples can be compiled & run as: rustc example.rs; ./example

Project-based examples can be run as: cd example_rs; cargo run

For clarity, some of the examples employ unsafe constructs like unwrap. More sophisticated error handling may be desired in the final applications. Also some of the examples use String type despite of its impact on performance.

Crystal

Code is prepared mostly for crystal ~1.8.

File-based examples can be compiled & run as: crystal run example.cr

Project-based examples can be run as: cd example_cr; shards run

Features of the Arguments

Objectives:

  • Implement function with default values of arguments.
  • Specify arguments by name in the function call.

Python

def foo(bar=True, baz=123, qux="abc"):
    print(f"bar={bar}, baz={baz}, qux={qux}")
    
foo()
foo(qux="hello")
foo(qux="hello", bar=True, baz=987)
foo(False, qux="hello", baz=987)

Additionally Python also supports:

  • Variadic positional args.
  • Variadic keyword args.
  • Keyword-only args.

Rust

Example shows how to implement keyword arguments, but positional arguments are not available at the same time.

#[derive(Default)]
struct Foo {
    bar: bool,
    baz: i32,
    qux: String,
}

impl Foo {
    fn default() -> Self {
        Self{
            baz: 999,
            ..Default::default()
        }
    }

    fn bar(mut self, val: bool) -> Self {
        self.bar = val;
        self
    }
    
    fn baz(mut self, val: i32) -> Self {
        self.baz = val;
        self
    }
    
    fn qux(mut self, val: String) -> Self {
        self.qux = val;
        self
    }
    
    fn call(&self) {
        println!("bar={}, baz={}, qux={}", self.bar, self.baz, self.qux);
    }
}

fn main() {
    Foo{bar: true, baz: 123, qux: "abc".to_string()}.call();
    Foo{bar: true, ..Default::default()}.call();
    Foo::default().call();
    Foo::default().qux("hello".to_string()).call();
    Foo::default().qux("hello".to_string()).bar(true).baz(987).call();
}

Notes:

  • default(), call() and each setter needs to be called (builder pattern).
  • Builder can also implement variadic args (by appending values to a vector) and variadic keyword args (by appending name and values to a hashmap).
  • Separate names can be used externally and internally (e.g. field in Foo can have a different name than corresponding setter).

Crystal

def foo(bar = true, baz = 123, qux = "abc")
  puts "bar=#{bar}, baz=#{baz}, qux=#{qux}"
end

foo()
foo(qux: "hello")
foo(qux: "hello", bar: true, baz: 987)
foo(false, qux: "hello", baz: 987)

Additionally Crystal supports:

  • Variadic positional args.
  • Variadic keyword args.
  • Keyword-only args.
  • Internal/external arg names.

Boolean Arguments

Objective: Show how boolean arguments can be used to enable/disable features of a function. Focus on readability.

Note: Some of the concepts below can be extended to arguments that take more than two arbitrary values.

Python

Variant 1:

def foo(*, something_is_on):
    if something_is_on:
        print("Something is ON")
    else:
        print("Something is OFF")

foo(something_is_on=True)
foo(something_is_on=False)

Keyword-only arguments are used to enforce readability. Suffix like _is_on may feel verbose but provides clarity.

Variant 2:

from enum import Enum

class Something(Enum):
    on = "ON"
    off = "OFF"
    
def foo(something: Something):
    if something == Something.on:
        print("Something is ON")
    else:
        print("Something is OFF")

foo(Something.on)
foo(Something.off)

Something enum must be imported together with foo if they happen to be defined externally.

Variant 3:

from typing import Literal

Something = Literal["on", "off"]

def foo(*, something: Something):
    if something == "on":
        print("Something is ON")
    else:
        print("Something is OFF")

foo(something="on")
foo(something="off")

No need of importing Something. Keyword-only arguments are used to enforce readability.

Rust

Using bool variable may not be a good practive. There are keyword arguments, so in invocation it wouldn't be visible what the flag is doing.

enum Something {
    On,
    Off,
}

fn foo(is_something: Something) {

    match is_something {
        Something::On => {
            println!("Something is ON");
        },
        _ => {
            println!("Something is OFF");
        },
    }
}

fn main() {
    foo(Something::On);
    foo(Something::Off);
}

Something enum must be imported together with foo if they happen to be defined externally.

Crystal

Variant 1:

def foo(*, something_is_on : Bool)
  if something_is_on
    puts "Something is ON"
  else
    puts "Something is OFF"
  end
end

foo(something_is_on: true)
foo(something_is_on: false)

Keyword-only arguments are used to enforce readability. Suffix like _is_on may feel verbose but provides clarity.

Variant 2:

enum Something
  On
  Off
end

def foo(*, something : Something)
  if something == Something::On
    puts "Something is ON"
  else
    puts "Something is OFF"
  end
end

foo(something: :On)
foo(something: :Off)

Keyword-only arguments are used to enforce readability. Suffix _is_on not needed and no need of importing Something.

Enum can be reused for other arguments.

Variant 3:

No need of importing Something. Readability is enforced despite of the fact that function is called with positional arguments.

enum Something
  SomethingOn
  SomethingOff
end

def foo(something : Something)
  # fully qualified name is needed here
  if something == Something::SomethingOn
    puts "Something is ON"
  else
    puts "Something is OFF"
  end
end

foo(:SomethingOn)
foo(:SomethingOff)

Implementation of the function and of the enum looks too verbose.

Also note that enums in crystal can be mapped only to integers (no booleans).

Arguments of Optional Type

Objective: Define a fucntion that takes argument x of Optional[int] type. Return 123 if x is None. Otherwise return x.

Caution: don't confuse optional arguments (that can be skipped at the function call) with arguments of Optional type (that cannot be skipped, but can take value of None).

Python

from typing import Optional

def foo(x: Optional[int]):
    #x = x or 123  # not valid, foo(0) would give 321
    x = 123 if x is None else x
    print(x)

foo(321)  # 321
foo(0)    # 0
foo(None) # 123

Rust

fn foo(x: Option<i32>) {
    
    let x = match x {
        None => 123,
        Some(x) => x,
    };
    
    println!("x={}", x);
}

fn main() {
    foo(Some(321)); // x=321
    foo(Some(0));   // x=0
    foo(None);      // x=123
}

Alternatively, Into allows for a convenient definition where Somecan be skipped at the function call:

fn foo<T: Into<Option<i32>>>(x: T) {
   
    let x = x.into();
    let x = match x {
        None => 123,
        Some(x) => x,
    };
    
    println!("x={}", x);
}

fn main() {
    foo(Some(321)); // x=321
    foo(321);       // x=321
    foo(Some(0));   // x=0
    foo(0);         // x=0
    foo(None);      // x=123
}

Crystal

def foo(x : Int32?)
  # note that here only nil evaluates to false
  x = x || 123
  puts x
end

foo 321 # 321
foo 0   # 0
foo nil # 123

Arguments of Bool type need special attention. Short syntax above can provide unintended results. Universal alternative:

def bar(x : Bool?)
  # x = x || true # not valid, it always gives true
  x = x.nil? ? true : x
  puts x
end

bar false # false
bar true  # true
bar nil   # true

Chaining Methods

Objective: Demonstrate how to chain methods commonly used in functional programming.

Python

data = [2, 3, 7, 4, 1]
sqr = map(lambda x: x**2, data)
lim = filter(lambda x: x < 20, sqr)
res = sum(lim)
print(res)

Alternatively we can use generator-expressions (or list comprehensions):

data = [2, 3, 7, 4, 1]
sqr = (x**2 for x in data)
lim = (x for x in sqr if x < 20)
res = sum(lim)
print(res)

Rust

fn main() {
    let data = [2, 3, 7, 4, 1];
    let res = data.iter()
                .map(|x: &i32| x.pow(2))
                .filter(|x| x < &20)
                .sum::<i32>();
                
    println!("{:?}", res);
}

Alternatively:

fn main() {
    let data: Vec<i32> = vec![2, 3, 7, 4, 1];
    let res: i32 = data.iter()
                .map(|x| x.pow(2))
                .filter(|x| x < &20)
                .sum();
                
    println!("{:?}", res);
}

Alternatively crate cute can be used to mimic comprehension-like style:

#[macro_use(c)]
extern crate cute;

fn main() {
    let data: Vec<i32> = vec![2, 3, 7, 4, 1];
    let sqr = c![x.pow(2), for x in data];
    let lim = c![x, for x in sqr, if x < 20];
    let res: i32 = lim.iter().sum();
    dbg!(res);
}

Crystal

data = [2, 3, 7, 4, 1]
puts data.map { |x| x**2 }
  .select { |x| x < 20 }
  .sum

Alternatively:

data = [2, 3, 7, 4, 1]
puts data.map(&.** 2).select(&.< 20).sum

Handling Errors

Objectives: Write an application where errors can be signalled in multiple places. Write common error handler that prints error message if error occures. Otherwise display execution results. Error handler should be able to distinguish errors.

Python

from contextlib import contextmanager

def div(a: float, b: float) -> float:
  if b == 0.0:
      raise ZeroDivisionError("Division by zero is illegal")
  return a / b

def div_and_add(a: float, b: float) -> float:
  n = div(a, b)
  m = div(b, a)
  s = m + n
  if s >= 10:
      raise OverflowError("Sum is too large")
  return s

def show(a, b):
    try:
        res = div_and_add(a, b)
    except ZeroDivisionError as exc:
        print(f"Zero division error: {exc}")
    except OverflowError as exc:
        print(f"Overflow error: {exc}")
    except Exception as exc:
        print(f"Error: {exc}")
    else:
        print(f"Result: {res}")

show(2.0, 4.0)   # Result: 2.5
show(0.0, 4.0)   # Zero division error: Division by zero is illegal
show(2.0, 0.0)   # Zero division error: Division by zero is illegal
show(100.0, 1.0) # Overflow error: Sum is too large

Rust

fn div(a: f32, b: f32) -> Result<f32, &'static str> {
    if b == 0.0 {
        return Err("Division by zero is illegal");
    }
    Ok(a / b)
}

fn div_and_add(a: f32, b: f32) -> Result<f32, &'static str> {
    let n = div(a, b)?;
    let m = div(b, a)?;
    let s = m + n;

    if s >= 10.0 {
        return Err("Sum is too large")
    }

    Ok(s)
}

fn show(a: f32, b: f32)  {

    fn disp_res(x : &dyn std::fmt::Display ) {
        println!("Result: {}", x);
    }

    fn disp_err(x : &dyn std::fmt::Display ) {
        println!("Error: {}", x);
    }
    
    match div_and_add(a, b) {
        Ok(res) => { disp_res(&res); },
        Err(msg) => { disp_err(&msg); }
    };
}

fn main() {
    show(2.0, 4.0);   // Result: 2.5
    show(0.0, 4.0);   // Error: Division by zero is illegal
    show(2.0, 0.0);   // Error: Division by zero is illegal
    show(100.0, 1.0); // Error: Sum is too large
}

Note: in the example above, in the error handler there is no way to distinguish error types (other way than looking at the error message).

Alternatively:

#[derive(Debug)]
enum DivError {
  DivisionByZero(String),
}

#[derive(Debug)]
enum AddError {
  Overflow(String),
}

type DivResult = Result<f32, Box<dyn std::any::Any>>;

fn div(a: f32, b: f32) -> DivResult {
  if b == 0.0 {
    return Err(Box::new(DivError::DivisionByZero(
      "Division by zero is illegal".to_string())));
  }
  Ok(a / b)
}

fn div_and_add(a: f32, b: f32) -> DivResult {
  let n = div(a, b)?;
  let m = div(b, a)?;
  let s = m + n;

  if s >= 10.0 {
    return Err(Box::new(AddError::Overflow("Sum is too large".to_string())));
  }

  Ok(s)
}

fn show(a: f32, b: f32)  {
    match div_and_add(a, b) {
        Ok(res) => { println!("Result: {}", res); },
        Err(e) => {
            if let Some(v) = e.downcast_ref::<DivError>() {
                match v {
                    DivError::DivisionByZero(msg) => {
                        println!("Zero division error: {}", msg);
                    }
                }
            } else if let Some(v) = e.downcast_ref::<AddError>() {
                match v {
                    AddError::Overflow(msg) => {
                        println!("Overflow error: {}", msg);
                    }
                }
            } else {
                println!("Undetermined error"); // this shouldn't happen
            }
        }
    }
}

fn main() {
    show(2.0, 4.0);   // Result: 2.5
    show(0.0, 4.0);   // Zero division error: Division by zero is illegal
    show(2.0, 0.0);   // Zero division error: Division by zero is illegal
    show(100.0, 1.0); // Overflow error: Sum is too large
}

Note that there are crates in the wild which provide more sophisticated way of handling errors. See thiserror and anyhow.

Crystal

class ZeroDivisionError < Exception
end

class Overflow < Exception
end

def div(a : Float, b : Float) : Float
  if b == 0.0
    raise ZeroDivisionError.new("Division by zero is illegal")
  end
  a / b
end

def div_and_add(a : Float, b : Float) : Float
  n = div(a, b)
  m = div(b, a)
  s = m + n
  if s >= 10
    raise Overflow.new("Sum is too large")
  end

  return s
end

def show(a, b)
  begin
    res = div_and_add(a, b)
  rescue exc : ZeroDivisionError
    puts "Zero division error: #{exc}"
  rescue exc : Overflow
    puts "Overflow error: #{exc}"
  rescue exc
    puts "Error: #{exc}"
  else
    puts "Result: #{res}"
  end
end

show 2.0, 4.0   # Result: 2.5
show 0.0, 4.0   # Zero division error: Division by zero is illegal
show 2.0, 0.0   # Zero division error: Division by zero is illegal
show 100.0, 1.0 # Overflow error: Sum is too large

Returning Optionals

Objective: Define fucntions that use Optional to signal successful/unsuccessful operation. Chain them to see how information value is propagated.

Python

from typing import Optional

def div(a: float, b: float) -> Optional[float]:
    if b == 0.0: return None
    return a / b

def div_and_add(a: float, b: float) -> Optional[float]:
    n = div(a, b)
    if n is None: return None

    m = div(b, a)
    if m is None: return None

    s = m + n
    if s >= 10: return None

    return s

print(div_and_add(2.0, 4.0))   # 2.5
print(div_and_add(0.0, 4.0))   # None
print(div_and_add(2.0, 0.0))   # None
print(div_and_add(100.0, 1.0)) # None

Rust

fn div(a: f32, b: f32) -> Option<f32> {
    if b == 0.0 {
        None
    } else {
        Some(a / b)
    }
}

fn div_and_add(a: f32, b: f32) -> Option<f32> {
    let n = div(a, b)?;
    let m = div(b, a)?;
    let s = m + n;

    if s >= 10.0 {
        None
    } else {
        Some(s)
    }
}

fn show(a: f32, b: f32) {
    println!("{:?}", div_and_add(a, b));
}

fn main() {
    show(2.0, 4.0);   // 2.5
    show(0.0, 4.0);   // None
    show(2.0, 0.0);   // None
    show(100.0, 1.0); // None
}

Crystal

def div(a : Float32, b : Float32) : Float32?
  return nil if b == 0.0
  a / b
end

def div_and_add(a : Float32, b : Float32) : Float32?
  div(a, b).try do |n|
    div(b, a).try do |m|
      if (s = m + n) < 10
        s
      end
    end
  end
end

def show(a : Float32, b : Float32)
  puts div_and_add(a, b) || "None"
end

show(2.0, 4.0)   # 2.5
show(0.0, 4.0)   # None
show(2.0, 0.0)   # None
show(100.0, 1.0) # None

RAII

Objective: Allocate resources and call given code in context of those resources. Finally clean-up resources despite of the fact if the code signalled an error or not.

Note: In the examples below 123 represents allocated resources.

Python

class Connection:
    def __init__(self, address):
        self.address = address
    
    def __enter__(self):
        print(f"Connection open to {self.address}")
        return 123
    
    def __exit__(self, ex_type, ex_val, ex_tb):
        print("Connection closed")
    
with Connection("target") as conn:
    print("Doing something well")
    
with Connection("target") as conn:
    raise Exception("Doing something wrong")

Alternatively:

from contextlib import contextmanager
    
@contextmanager
def Connection(address):
    print(f"Connection open to {address}")
    try:
        yield 123
    finally:
        print("Connection closed")

with Connection("target") as conn:
    print("Doing something well")
    
with Connection("target") as conn:
    raise Exception("Doing something wrong")

Rust

fn connection<F: FnOnce(i32) -> ()> (address: String, f: F ) {
    println!("Connection open to {}", address);
    f(123);
    println!("Connection closed");
}


fn main() {

  connection("target".to_string(), |_conn|{
      println!("Doing something well");
  });

  connection("target".to_string(), |_conn|{
      println!("Doing something wrong");
  });
  
  connection("target".to_string(), |_conn|{
      println!("Doing something miserably wrong");
      panic!("Boo"); // connection not closed
  });
  
}

Note: doesn't meet objectives in case of a panic. However many applications are made sure that they never panic.

Crystal

def connection(address)
  puts "Connection open to #{address}"
  begin
    yield 123
  ensure
    puts "Connection closed"
  end
end

connection("target") do |conn|
  puts "Doing something well"
end

connection("target") do |conn|
  raise "Doing something wrong"
end

Dynamic Dispatch

Objective: Create container accepting items of different types. Categorize items in runtime. Dispatch methods dynamically.

Python

from typing import Union, List

class Cat:
    def make_sound(self):
        print("miao")
        
class Cow:
    def make_sound(self):
        print("mooo")

Animal = Union[Cat, Cow]
animals: List[Animal] = []
animals.append( Cat() )
animals.append( Cow() )

for animal in animals:
    animal.make_sound()
    

Alternatively, instead of using Union type, classes Cat and Cow can have common base Animal.

Rust

trait CanMakeSound {
    fn make_sound(&self);
}

struct Cat {
}

impl CanMakeSound for Cat {
    fn make_sound(&self) {
        println!("miao")
    }
}
    
struct Cow {
}

impl CanMakeSound for Cow {
    fn make_sound(&self) {
        println!("mooo")
    }
}

fn main() {

    let mut animals: Vec<&dyn CanMakeSound> = Vec::new();
    animals.push( &Cat{} );
    animals.push( &Cow{} );

    for animal in animals {
    animal.make_sound();
    }
 
}

Alternatively:

fn main() {

    let mut animals: Vec<Box<dyn CanMakeSound>> = Vec::new();
    animals.push( Box::new(Cat{}) );
    animals.push( Box::new(Cow{}) );

    for animal in animals {
    animal.make_sound();
    }

}

Crystal

class Cat
  def make_sound
    puts "miao"
  end
end

class Cow
  def make_sound
    puts "mooo"
  end
end

alias Animal = Cat | Cow
animals = [] of Animal
animals << Cat.new
animals << Cow.new

animals.each &.make_sound()

Alternatively:

animals.each do |animal|
  animal.make_sound
end

Static Dispatch

Objectives: Create container accepting items of any type. Categorize items in runtime. Dispatch methods statically.

Python

Following code works well but I'm unable to give good reasons for doing this in practice. Perhaps performance of this code may be compared with dynamic dispatching. This needs more investigation though.

from typing import Any

class Cat:
    def make_sound(self):
        print("miao")
        
class Cow:
    def make_sound(self):
        print("mooo")

animals: list[Any] = []
animals.append( Cat() )
animals.append( Cow() )

cat_make_sound = Cat.make_sound
cow_make_sound = Cow.make_sound

for animal in animals:    
    match animal:
        case Cat():
            cat_make_sound(animal)
        case Cow():
            cow_make_sound(animal)

CPython interpreters older than 3.10 don't support match, thus alternative would be:

for animal in animals:    
    if isinstance(animal, Cat):
        cat_make_sound(animal)
    elif isinstance(animal, Cow):
        cow_make_sound(animal)

Rust

use std::any::Any;

trait CanMakeSound {
    fn make_sound(&self);
}

struct Cat {
}

impl CanMakeSound for Cat {
    fn make_sound(&self) {
        println!("miao")
    }
}
    
struct Cow {
}

impl CanMakeSound for Cow {
    fn make_sound(&self) {
        println!("mooo")
    }
}

fn main() {
    let mut animals: Vec<Box<dyn Any>> = Vec::new();
    animals.push( Box::new(Cat{}) );
    animals.push( Box::new(Cow{}) );

    for animal in animals {
        if let Some(cat) = animal.downcast_ref::<Cat>() {
            cat.make_sound();
        } else if let Some(cow) = animal.downcast_ref::<Cow>() {
            cow.make_sound();
        }
    }
}

Crystal

#![allow(unused)]
fn main() {
class Cat
  def make_sound
    puts "miao"
  end
end

class Cow
  def make_sound
    puts "mooo"
  end
end

alias Animal = Cat | Cow
alias Any = Pointer(Void)

animals = [] of Any

animals << Box(Animal).box(Cat.new)
animals << Box(Animal).box(Cow.new)

animals.each do |animal|
  unboxed = Box(Animal).unbox(animal)
  if cat = unboxed.as?(Cat)
    cat.make_sound
  elsif cow = unboxed.as?(Cow)
    cow.make_sound
  end
end
}

Distinguish Types of the Arguments

Objective: Define a fucntion that:

  • Returns inverted value for boolean argument.
  • Returns opposite value for integer argument.
  • Returns object with coordinates swapped for argument of Point type.
  • Prints warning and returns value of the argumen for any other type.

Test it where type of the argument is known in advance time and when it determined only in runtime.

Python

from typing import Union
from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

def flip(a: Union[bool, int, Point]):
    
    if isinstance(a, bool):
        return not a
    
    if isinstance(a, int):
        return -a
    
    if isinstance(a, Point):
        return Point(a.y, a.x)

    print(f"Warning: Type {a.__class__.__name__} not flippable");
    return a

# types known in advance
print(f"{flip(False)=}")
print(f"{flip(3)=}")
print(f"{flip(Point(5, 7))=}")
print(f"{flip(33.3)=}")

# types determined in runtime
items = [False, 3,  Point(5, 7), 33.3]
for item in items:
    flipped = flip(item)
    print(f"{item} flipped is {flipped}")

Rust

#![feature(auto_traits)]
#![feature(negative_impls)]
use std::ops::Neg;
use std::any::type_name;

auto trait Flippable {
}

impl !Flippable for bool {}
impl !Flippable for i32 {}
impl !Flippable for i64 {}

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32
}

fn flip_num<T: Neg<Output = T>> (a: T) -> T {
    -a
}

trait Flip {
    fn flip(a: Self) -> Self;
}

impl Flip for bool {
    fn flip(a: bool) -> bool {
        !a
    }
}

impl Flip for i32 {
    fn flip(a: i32) -> i32 {
        flip_num(a)
    }
}

impl Flip for i64 {
    fn flip(a: i64) -> i64 {
        flip_num(a)
    }
}

impl Flip for Point {
    fn flip(a: Point) -> Point {
        Point{x: a.y, y: a.x}
    }
}

impl<T> Flip for T
where
    T: Flippable
{
    fn flip(a: T) -> T {
        println!("Warning: Type {} not flippable", type_name::<T>() );
        a
    }
}

fn flip<T: Flip>(a: T) -> T {
    Flip::flip(a)
}

fn main() {
    // types known in compile time
    dbg!( flip(true));
    dbg!( flip(3 as i32) );
    dbg!( flip(33 as i64) );
    dbg!( flip(Point{x: 5, y: 7}) );
    dbg!( flip(33.3) );
    
    // types determined in runtime
    /* Error: not possible to use it this way
    let items: Vec<&dyn std::any::Any> = vec![&true, &(3 as i32), &(3 as i64), &Point{x: 5, y: 7}, &33.3];
    for item in items {
        flip(*item);
    }
    */
}

Crystal

struct Point
  property x : Int32
  property y : Int32

  def initialize(@x, @y)
  end
end

def flip(a : Bool)
  !a
end

# implementation for all integer types
def flip(a : Int)
  -a
end

def flip(a : Point)
  Point.new(a.y, a.x)
end

# implementation for all remaining types
def flip(a)
  puts "Warning: Type #{a.class} not flippable"
  a
end

# types known in compile time
p! flip false
p! flip 3_i32
p! flip 33_i64
p! flip Point.new(5, 7)
p! flip 33.3

# types determined in runtime
items = [false, 3_i32, 3_i64, Point.new(5, 7), 33.3]
items.each do |item|
  flipped = flip item
  puts "#{item} flipped is #{flipped}"
end

Distinguish Interfaces of the Arguments

Objective: Define a fucntion that:

  • For arguments that implements get_color method, calls this method and prints the result.
  • For remaining arguments prints transparent.

Test it where type of the argument is known in advance time and when it determined only in runtime.

Python

Abstract class can be used to define interface:

from abc import ABC, abstractmethod
from typing import Any

class Colorful(ABC):
    @abstractmethod
    def get_color(self):
        pass

class Apple(Colorful):
    def get_color(self):
        return "green"

class Sky(Colorful):
    def get_color(self):
        return "blue"

def get_color(x: Any):
    if isinstance(x, Colorful):
        return x.get_color()
    else:
        return "transparent"

# types known in advance
print(f"{get_color(Apple())=}")
print(f"{get_color(Sky())=}")
print(f"{get_color(123)=}")
print(f"{get_color('example')=}")

# types determined in runtime
items = [Apple(), Sky(), 123,  "example"]
for item in items:
    color = get_color(item)
    print(f"{item} is {color}")

Alternatively, existence of the methods can be checked in runtime without defining interfaces:

from typing import Any

class Apple:
    def get_color(self):
        return "green"

class Sky:
    def get_color(self):
        return "blue"


def get_color(x: Any):
    if hasattr(x, 'get_color'):
        return x.get_color()
    return "transparent"

Yet another way of implementing get_color:

def get_color(x: Any):
    try:
        get_color_ = x.get_color
    except AttributeError:
        return "transparent"
    else:
        return get_color_()

Rust

#![feature(auto_traits)]
#![feature(negative_impls)]

auto trait Transparent {
}

trait Colorful {
    fn get_color(&self) -> &'static str;
}

struct Apple;

impl !Transparent for Apple {}
impl Colorful for Apple {
  fn get_color(&self) -> &'static str {
    "green"
  }
}
    
struct Sky;

impl !Transparent for Sky {}
impl Colorful for Sky {
  fn get_color(&self) -> &'static str {
    "blue"
  }
}

impl<T> Colorful for T
where
    T: Transparent
{
  fn get_color(&self) -> &'static str {
    "transparent"
  }
}

fn get_color(x: impl Colorful) -> &'static str {
    x.get_color()
}


fn main() {
    // types known in compile time
    dbg!( get_color(Apple{}) );
    dbg!( get_color(Sky{}) );
    dbg!( get_color(123) );
    dbg!( get_color("example") );

    // types determined in runtime
    let items: Vec<&dyn Colorful> = vec![&Apple{}, &Sky{}, &123, &"example"];
    for item in items {
        // Below we print only color of the item. Printing the items themself
        // needs `fmt` to be implemented and is out of the scope of this example.
        println!("item is {}", item.get_color() );
    }
}

Crystal

Abstract struct can be used to define interface:

abstract struct Colorful
  abstract def get_color
end

struct Apple < Colorful
  def get_color
    "green"
  end
end

struct Sky < Colorful
  def get_color
    "blue"
  end
end

def get_color(x : Colorful)
  return x.get_color
end

def get_color(x)
  return "transparent"
end

# types known in compile time
p! get_color Apple.new
p! get_color Sky.new
p! get_color 123
p! get_color "example"

# types determined in runtime
items = [Apple.new, Sky.new, 123, "example"]
items.each do |item|
  color = get_color(item)
  puts "#{item} is #{color}"
end

Alternatively, existence of the methods can be checked in runtime without defining interfaces:

struct Apple
  def get_color
    "green"
  end
end

struct Sky
  def get_color
    "blue"
  end
end

def get_color(x)
  if x.responds_to?(:get_color)
    return x.get_color
  end
  return "transparent"
end

Distinguish Quantity of the Arguments

Objective: Define a fucntion that:

  • Takes required argument, firstname
  • Takes optional argument, surname
  • Prints different messages depending on whether surname is provided or not.

Python

from typing import Optional

def say_hello(firstname: str, surname: Optional[str] = None):
    if surname is None:
        print(f"Hi {firstname}!")
    else:
        print(f"Hello {firstname} {surname}!")

say_hello("John");
say_hello("Tom", "Brown");

Rust

Here is where builder pattern comes in:

#[derive(Default)]
struct SayHello {
    surname: Option<String>,
}

impl SayHello {
    fn default() -> Self {
        Self{
            ..Default::default()
        }
    }
    
    fn surname(mut self, val: &str) -> Self {
        self.surname = Some(val.to_string());
        self
    }
    
    fn call(&self, firstname: &str) {
        if let Some(surname) = &self.surname {
            println!("Hello {} {}!", firstname, surname);
        } else {
            println!("Hi {}!", firstname);                
        }
    }
}

fn main() {
    SayHello::default().call("John");
    SayHello::default().surname("Brown").call("Tom");
}

Presence of surname is checked in compile time.

Crystal

Variant 1: Presence of surname is checked in compile time.

def say_hello(firstname : String)
  puts "Hi #{firstname}!"
end

def say_hello(firstname : String, surname : String)
  puts "Hello #{firstname} #{surname}!"
end

say_hello "John"
say_hello("Tom", "Brown")

Variant 2: Presence of surname is checked in runtime.

def say_hello(firstname : String, surname : String? = nil)
  if surname
    puts "Hello #{firstname} #{surname}!"
  else
    puts "Hi #{firstname}!"
  end
end

say_hello "John"
say_hello("Tom", "Brown")

Operators

Objective: Define class Hundred. Provide operators that allows for adding numeric values to the instances of Hundred (on left and right hand side).

Python

class Hundred:
    def __radd__(self, other):
        return other + 100

    def __add__(self, other):
        return other + self

print(2 + Hundred())   # 102
print(Hundred() + 4)   # 104
print(Hundred() + 4.5) # 104.5

Rust

struct Hundred {
}

impl std::ops::Add<Hundred> for i32 {
    type Output = i32;

    fn add(self, rhs: Hundred) -> i32 {
        rhs + self
    }
}

impl std::ops::Add<i32> for Hundred {
    type Output = i32;

    fn add(self, rhs: i32) -> i32 {
        rhs + 100
    }
}

fn main() {

    dbg!(2 + Hundred{}); // 102
    dbg!(Hundred{} + 4); // 104

    // Other numeric types require separate implementation
    //dbg!(Hundred{} + 4.5 );
}

Note: crate num-traits can help with providing implementation for each numeric type.

Crystal

class Hundred
  def +(other)
    other + 100
  end
end

struct Number
  def +(other : Hundred)
    other + self
  end
end

puts 2 + Hundred.new
puts Hundred.new + 4
puts Hundred.new + 4.5

Type Inference

Objective: Show how types are infered in different types of statements.

Python

Check types by calling: mypy --strict inference.py

def foo(x: int, y: int) -> int:
    return x + y

class C:
    def __init__(self, x: int):
        self.x = x
        
    def get_x(self) -> int:
        return self.x

foo(2, 3)

c = C(123)
print(c.get_x())

Rust

fn foo(x : i32, y : i32) -> i32 {
    x + y
}

struct C {
    x : i32
}

impl C {
    fn new(x : i32) -> Self {
        Self{x}
    }

    fn get_x(&self) -> i32 {
        self.x
    }
}

fn main() {
    println!("{:?}", foo(2, 3));

    let c = C::new(123);
    println!("{:?}", c.get_x());
}

Crystal

# generic function
def foo(x, y)
  x + y
end

class C
  # type needs to be specified
  def initialize(@x : Int32)
  end

  # type is inferred
  def get_x
    @x
  end
end

puts foo(2, 3)             # calls foo<Int32, Int32>:Int32
puts foo("hello", "world") # calls foo<String, String>:String

c = C.new(123)
puts c.get_x

Extending Existing Types

Objective: Add methods to the integer type.

Python

Extending built-in class is not possible. Inharitance helps to workaround the problem.

class Int(int):
    
    @property
    def dots(self):
        return '.' * self

x = Int(9)

# Method used as a property, so parentheses not needed.
print(x.dots)

Rust

Extending built-in types is possible, but how to do it for all integer types at once?

trait Dots {
    fn dots(&self) -> String;
}

impl Dots for i32 {
    fn dots(&self) -> String {
        ".".repeat(*self as usize)
    }
}

fn main() {
    // There are no properties, we need parentheses.
    println!("{}", 9.dots());
}

Crystal

Inheritance is available, but not easy in case of this example. Instead of that, method can be added to the existing type:

struct Int
  def dots
    "." * self
  end
end

# Method can be called with or without parentheses.
puts 9.dots

Mixins

Objective: Create methods that can be injected into classes of our choice. Methods should have access to the fields of the instances. Injection should not involve inheritance.

Python

Python supports multiple-inheritance. It is natural to use it for this use case:

class Greeting:
    def hello(self):
        print(f"Hello, my name is {self.name}. How can I help you?")

class Farewell:
    def bye(self):
        print("Thank you. Bye!")

class ChatBot(Greeting, Farewell):
    def __init__(self, name):
        self.name = name

bot = ChatBot("R2D2")
bot.hello()
bot.bye()

Using inheritance hovewer violates definition of a mixin. If this is important for the design, we can copy methods without affecting MRO:

def mixin(*sources):
    def decorator(target):
        for src in sources:
            for key in dir(src):
                if not (key.startswith('__') and key.endswith('__')):
                    setattr(target, key, getattr(src, key))
        return target
    return decorator

class Greeting:
    def hello(self):
        print(f"Hello, my name is {self.name}. How can I help you?")

class Farewell:
    def bye(self):
        print("Thank you. Bye!")

@mixin(Greeting, Farewell)
class ChatBot:
    def __init__(self, name):
        self.name = name

bot = ChatBot("R2D2")
bot.hello()
bot.bye()

More sophisticated packages like mixin can be used.

Rust

Rust doesn't support inheritance. Traits are natural way of fulfilling objectives.

Note that associated functions can't directly access fields of the structure. For this reason we define helper trait Named.

trait Named {
    fn name(&self) -> &str;
}

trait Greeting : Named {
    fn hello(&self) {
        println!("Hello, my name is {}. How can I help you?", self.name());
    }
}

trait Farewell {
    fn bye(&self) {
        println!("Thank you. Bye!");
    }
}

struct ChatBot {
    name : String
}

impl Greeting for ChatBot {}
impl Farewell for ChatBot {}
impl Named for ChatBot {
    fn name(&self) -> &str {
        &self.name
    }
}

fn main() {
  let bot = ChatBot{name: "R2D2".to_string()};
  bot.hello();
  bot.bye();
}

Crystal

Crystal supports single-inheritance. Mixins can be implemented using modules. Methods in the module can directly access fields of the structure. This is checked at compile time.

module Greeting
  def hello
    puts "Hello, my name is #{@name}. How can I help you?"
  end
end

module Farewell
  def bye
    puts "Thank you. Bye!"
  end
end

class ChatBot
  include Greeting
  include Farewell

  def initialize(@name : String)
  end
end

trx = ChatBot.new "R2D2"
trx.hello
trx.bye

Multithreading

Objectives: Create queue of data to be processed and queue of processing results. Spawn T threads, where each thread is a worker that performs data processing.

Python

CPython 3.8.10 has GIL (global interpreter lock) that prevents threads from running in parallel. However multiple threads can be spawned and run concurently.

from random import randint
from time import sleep
from threading import Thread
from queue import Queue

N = 100
T = 3

def shift(q_in, q_out, offset):
    while True:
        x = q_in.get()
        if x is None:
            break
        q_out.put(x + offset)
        sleep(0.001 * randint(0, 10))

q_in = Queue()
q_out = Queue()
offsets = ((i+1)*N for i in range(T))
threads = [Thread(target=shift, args=(q_in, q_out, offset))
           for offset in offsets]

[t.start() for t in threads]

for x in range(N):
    q_in.put(x)

[q_in.put(None) for _ in threads]

[t.join() for t in threads]

for x in range(N):
    print(q_out.get())

In practice ThreadPoolExecutor can efficiently simplify this code. However, for the sake of this exercise we use Thread in order to explore more of this field.

Rust

Rust supports threads that can be run in parallel. Crate flume has been used as a replacement of std::sync::mpsc. Flume provides multiple-producers-multiple-consumers channels. Additionally crate rand provides random numbers for demonstrational purposes.

use std::time;
use std::thread;
use rand::Rng;

const N: i32 = 100;
const T: i32 = 3;

fn shift(q_in: flume::Receiver<Option<i32>>, q_out: flume::Sender<Option<i32>>, offset: i32) {
    let ms = time::Duration::from_millis(1);
    let mut rng = rand::thread_rng();
    loop {
        let x = q_in.recv().unwrap();
        match x {
            Some(v) => { q_out.send( Some(v + offset) ).unwrap(); }
            None => { break; }
        }
        thread::sleep(rng.gen_range(0..10) * ms);
    }
}

fn main() {
    let (q_in_tx, q_in_rx) = flume::unbounded();
    let (q_out_tx, q_out_rx) = flume::unbounded();

    let mut t = Vec::<thread::JoinHandle<()>>::new();

    for x in 0..T {
        let q_in_rx_c = q_in_rx.clone();
        let q_out_tx_c = q_out_tx.clone();
        t.push(thread::spawn(move || {
            shift(q_in_rx_c, q_out_tx_c, (x + 1) * 100);
        }));
     }
     
    for x in 0..N {
        q_in_tx.send(Some(x)).unwrap();
    }

    for _ in 0..T {
        q_in_tx.send(None).unwrap();
    }

    for _ in 0..T {
        t.pop().unwrap().join().unwrap();
    }
    
    for _ in 0..N {
        if let Some(res) = q_out_rx.recv().unwrap() {
            println!("{:?}", res);
        }
    }
    
}

Crystal

Crystal 1.8.2 doesn't support multithreading. Entire application runs in a single thread, except for garbage collector which runs in a separate thread.

Multiprocessing

Objectives: Create a producer and consumer passing the data from one to another using FIFO.

Python

from random import randint
from time import sleep
from multiprocessing import Queue, Process

N = 100

def producer(fifo):
    for x in range(N):
        sleep(0.001 * randint(0, 10))
        fifo.put(x)
        print(f"Sent {x}")
    fifo.put(None)

def consumer(fifo):
    while True:
        x = fifo.get()
        if x is None:
            break
        sleep(0.001 * randint(0, 10))
        print(f"Received {x}")

fifo = Queue()
producer_proc = Process(target=producer, args=(fifo,))
consumer_proc = Process(target=consumer, args=(fifo,))

consumer_proc.start()
producer_proc.start()

producer_proc.join()
consumer_proc.join()

Rust

Standard library of Rust 1.73 doesn't seem to have good support for multiprocessing. With some effort this can be implemented using platform specific features. For instance fork & waitid syscalls in Unix.

Crystal

Standard library of Crystal 1.8.2 doesn't seem to have good support for multiprocessing. With some effort this can be implemented using platform specific features. For instance fork & waitid syscalls in Unix.

Asynchronous Execution

Objectives: Create vector of data items to be processed. Spawn concurent coroutines, one for each data item. Collect and present results.

Python

from random import randint
import asyncio

N = 10
 
async def shift(x):
    await asyncio.sleep(0.001 * randint(0, 10))
    print(f"Done with {x}")
    return 100 * x
 
async def launch():
    tasks =  [asyncio.create_task(shift(x)) for x in range(N)]
    print("All spawned")
    results = await asyncio.gather(*tasks)
    print(f"Results: {results}")
 
asyncio.run(launch())

Rust

use std::time::Duration;
use async_std::task;
use futures::executor::block_on;
use futures::future::join_all;
use rand::Rng;

const N: i32 = 10;

async fn shift(x: i32) -> i32 {
    let ms = Duration::from_millis(1);
    let random = {
        let mut rng = rand::thread_rng();
        rng.gen_range(0..10)
    };

    task::sleep(random * ms).await;
    println!("Done with {}", x);
    return 100 * x;
}

async fn launch() {
    let tasks: Vec<_> = (0..N).map(|x| task::spawn(shift(x))).collect();
    println!("All spawned");
    let results = join_all(tasks).await;
    println!("Results: {:?}", results.iter().collect::<Vec<_>>());
}

fn main() {
    block_on(launch());
}

Crystal

N = 10

def shift(x)
  sleep(0.001 * rand(10))
  puts "Done with #{x}"
  100 * x
end

alias ResultChannel = Channel(Int32)
result_channels = [] of ResultChannel

N.times do |x|
  result_channel = ResultChannel.new
  result_channels << result_channel
  spawn { result_channel.send(shift(x)) }
end

puts "All spawned"
results = result_channels.map &.receive
puts "Results: #{results}"

System Command

Objectives: Call system command ls. If return code is other than 0, signal error that includes message obtained from stderr. Otherwise read stdout, split lines and present as a list.

Python

import subprocess

target_dir = "/"
status = subprocess.run(["ls", target_dir],
                        stdout=subprocess.PIPE,
                        stderr=subprocess.PIPE)

if status.returncode:
    msg = status.stderr.decode()
    raise Exception(msg)
else:
    list_files = status.stdout.decode().split()
    print(list_files)

Note: in practice, packages like plumbum can be a good alternative.

Rust

use std::process::Command;

fn main() {

    let cmd = Command::new("ls")
            .arg("/")
            .stdout(std::process::Stdio::piped())
            .output()
            .expect("Failed to invoke command");

    if cmd.status.code() == Some(0) { // cmd.status.success() also available
        let stdout = String::from_utf8(cmd.stdout).unwrap();
        let list_files: Vec<&str> = stdout.split("\n").collect();
        println!("{:?}", list_files);
    } else {
        let stderr = String::from_utf8(cmd.stderr).unwrap();
        println!("Error: {}", stderr.trim());
    }

}

Note: unwrap has been used for simplicity. In practice this can be replaced by error propagation.

Crystal

cmd = Process.new("ls", args: ["/"],
  output: Process::Redirect::Pipe,
  error: Process::Redirect::Pipe)

stdout = cmd.output.gets_to_end
stderr = cmd.error.gets_to_end
status = cmd.wait
if status.exit_status == 0
  puts stdout.split
else
  raise stderr
end

Alternative variant below forwards stderr of the subprocess to stderr of the main process. Doesn't meet objectives but is still worth of mentioning.

stdout = `ls /`
if $?.success? # return code != 0 ?
  puts stdout.split
end

Calling libc

Objective: Call getpid from libc. Print PID returned by the function.

Python

from ctypes import cdll
libc = cdll.LoadLibrary("libc.so.6") 
print(libc.getpid())

Similar approach can be used for accessing other shared libraries.

Rust

Rust compiler comes with pre-prepared bindings for libc. As for now, this is only available in unstable versions of the compiler. Compile as:

rustc --edition 2024 -Z unstable-options calllibc.rs
#![feature(rustc_private)]
extern crate libc;
use libc::pid_t;

#[link(name = "c")]
extern {
    fn getpid() -> pid_t;
}

fn main() {
    let pid = unsafe { getpid() };
    println!("{}", pid);
}

Crystal

lib Libc
    alias PidT = Int32
    fun getpid : PidT
end

pid = Libc.getpid
puts(pid)

Similar approach can be used for accessing other shared libraries.

Making syscall

Objective: Invoke getpid syscall. Print returned PID.

Python

There is no dedicated binding for syscall in standard library. However syscall from libc can be utilized:

# inspiredy by: https://stackoverflow.com/a/37032683

import ctypes
from enum import IntEnum

class Syscall(IntEnum):
    GETPID = 39

libc = ctypes.CDLL(None)
# or alternatively:
#libc = ctypes.cdll.LoadLibrary("libc.so.6") 

syscall = libc.syscall
pid = syscall(Syscall.GETPID)
print(pid)

Rust

Example below uses syscalls crate.

use syscalls::{Sysno, syscall};

fn main() {
    match unsafe { syscall!(Sysno::getpid) } {
        Ok(pid) => {
            println!("{}", pid);
        }
        Err(err) => {
            eprintln!("getpid() failed: {}", err);
        }
    }
}

Crystal

Dedicated module Syscall is available. Bindings to individual syscalls must be created manually.

require "syscall"

module OurSysCalls
  Syscall.def_syscall getpid, Int32
end

pid = OurSysCalls.getpid
puts(pid)

Embedding Assembly

Objective: Use assembly code below to invoke getpid syscall. Obtain PID from %eax and print it to stdout.

movl $20, %eax
int $0x80

Note that this should work under most unix platforms running on x86. Alternative solution that works on x86-64 would be:

movl $39, %eax
syscall

Python

Embedding assembly makes little sense in an interpreted language like Python where typical intention is to be platform-agnostic. Hovewer, for the sake of example we can use CFFI module that will compile C code on the fly. C code in turn will embed our assembly:

from cffi import FFI
ffi = FFI()
ffi.set_source("_pidlib", r"""
int get_pid() {
    int pid;
    asm("movl $20, %%eax\n"
        "int $0x80\n"
        "movl %%eax, %0\n" : "=r"(pid));
    return pid;
}
""")
ffi.cdef("int get_pid();")
ffi.compile()

from _pidlib.lib import get_pid

print(get_pid())

Rust

Intel syntax applies.

use std::arch::asm;

fn main() {
    let mut pid: u32;
    unsafe {
        asm!(
            "mov eax, $20",
            "int $0x80",
            "mov {pid:e}, eax",
             pid = out(reg) pid,
        );
    }
    println!("{}", pid);
}

Crystal

AT&T syntax applies.

fun getpid() : Int32
    pid = uninitialized Int32
    
    asm("
    movl $$20, %eax
    int $$0x80
    movl %eax, $0" : "=r"(pid))
    
    return pid
end

puts getpid

Serdes

Objective: Define a simple class and create an instance. Serialize the object into JSON. Finally deserialize JSON into new object.

Python

import json
from dataclasses import dataclass, asdict

@dataclass
class Person:
  name: str
  height: float
 
person_a = Person("Greg", 1.80)
json_text_a = json.dumps(asdict(person_a))
print(json_text_a)

json_text_b = '{"name":"Tom", "height": 1.85}'
person_b = Person(**json.loads(json_text_b))
print(person_b)

Notes regarding deserialization:

  • Types are not checked. Use dacite or pydantic if needed.
  • Extra arguments raise an exception.
  • Missing arguments raise an exception, unless default values are provided.

Rust

Stdlib doesn't support serialization/deserialization to/from JSON. Crate serde has been used.

use serde::{Serialize, Deserialize};

#[derive(Default)]
#[derive(Serialize, Deserialize, Debug)]
struct Person {
    name: String,
    height: f64,
}

fn main() {
    let person_a = Person {name: "Greg".to_string(), height: 1.80};
    let json_text_a = serde_json::to_string(&person_a).unwrap();
    println!("{}", json_text_a);

    let json_text_b = r#"{"name":"Tom", "height": 1.85}"#;
    let person_b: Person = serde_json::from_str(&json_text_b).unwrap();
    println!("{:?}", person_b);
}

Notes regarding deserialization:

  • Incorrect types result in error.
  • Extra arguments are allowed and ignored.
  • Missing arguments result in error.

Crystal

#![allow(unused)]
fn main() {
require "json"

class Person
  include JSON::Serializable
  property name : String
  property height : Float64

  def initialize(@name, @height)
  end
end

person_a = Person.new("Greg", 1.80)
json_text_a = person_a.to_json
puts json_text_a

json_text_b = %({"name":"Tom", "height": 1.85})
person_b = Person.from_json(json_text_b)
p! person_b
}

Notes regarding deserialization:

  • Incorrect types raise an exception.
  • Extra arguments are allowed and ignored.
  • Missing arguments raise an exception, unless default values are provided.

Web Frameworks

Objective: Implement simple web application that demonstrates how to parse URL and how web framework handes errors. Use built-in HTTP servers.

Python

Framework in use: flask.

from flask import Flask

app = Flask(__name__)

@app.route('/', methods=["GET"]) # methods=["GET"] can be skipped, it is default
def hello_world():
    return f"Hello, World!"

@app.route('/bug')
def hello_bug():
    # let's see how it is rendered in debug mode
    raise Exception("Intentional error")

@app.route('/<name>')
def hello(name):
    name = name or "World"
    return f"Hello, {name}!"

if __name__ == "__main__":
    app.run(port=3000, debug=True)

Rust

Framework in use: actix-web.

use actix_web::{get, web, App, HttpServer, Responder};

#[get("/")]
async fn hello_world() -> impl Responder {
    "Hello World!"
}

#[get("/bug")]
async fn hello_bug() -> actix_web::Result<String> {
    Err(actix_web::error::ErrorNotImplemented("Intentional error"))
}

#[get("/{name}")]
async fn hello(name: web::Path<String>) -> impl Responder {
    format!("Hello {}!", &name)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new()
        .service(hello_world)
        .service(hello_bug)
        .service(hello))
        .bind(("127.0.0.1", 3000))?
        .run()
        .await
}

Crystal

Framework in use: kemal.

require "kemal"

get "/" do
  "Hello World!"
end

get "/bug" do
  raise "Intentional error"
end

get "/:name" do |env|
  name = env.params.url["name"]
  "Hello #{name}!"
end

Kemal.run

ORM

Objective: Create a PostgreSQL database that preserves state of a stock. Insert a few items. Then select some of them and present the results.

Python

ORM in use: sqlalchemy. Migration tool: alembic.

# DB in a container:
# docker run --name productsdb -e POSTGRES_PASSWORD=pass -p 5432:5432 -d postgres:15.3

import sqlalchemy as db
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy.orm import MappedAsDataclass, Session

class Base(MappedAsDataclass, DeclarativeBase):
#    metadata = db.MetaData()
    pass

class StockItem(Base):
    __tablename__ = "stock"
    name: Mapped[str] = mapped_column(primary_key=True)
    vendor: Mapped[str]
    quantity: Mapped[int]

engine = db.create_engine("postgresql+psycopg2://postgres:pass@127.0.0.1/postgres",)
#Base.metadata.create_all(engine)

with Session(engine) as session:
    session.add_all([
        StockItem(name="Toothbrush", vendor="Limo", quantity=1497),
        StockItem(name="Comb", vendor="Takoon", quantity=210),
        StockItem(name="Towel", vendor="Beana", quantity=362),
        ])

    session.commit()    

    query = db.select(StockItem).where(StockItem.quantity >= 300)
    for user in session.scalars(query):
        print(user)


Restore commented lines to Initialize database.

Rust

ORM in use: diesel. Diesel comes with built-in migration tool.

// models.rs
use crate::schema::stock;
use diesel::prelude::*;

#[derive(Debug)]
#[derive(Insertable)]
#[derive(Queryable, Selectable)]
#[diesel(table_name = stock)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct StockItem {
    pub name: String,
    pub vendor: String,
    pub quantity: i32,
}
// schema.rs
// @generated automatically by Diesel CLI.

diesel::table! {
    stock (name) {
        name -> Varchar,
        vendor -> Varchar,
        quantity -> Int4,
    }
}
// main.rs
mod models;
mod schema;

use diesel::pg::PgConnection;
use diesel::prelude::*;
use self::schema::stock::dsl::*;
use self::models::*;

pub fn establish_connection() -> PgConnection {
     let database_url = "postgres://postgres:pass@127.0.0.1/postgres";
     PgConnection::establish(&database_url)
         .unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
}

fn main() {
    let connection = &mut establish_connection();
    
    let new_item1 = StockItem { name: "Toothbrush".to_string(), vendor: "Limo".to_string(), quantity: 1497 };
    let new_item2 = StockItem { name: "Comb".to_string(), vendor: "Takoon".to_string(), quantity: 210 };
    let new_item3 = StockItem { name: "Towel".to_string(), vendor: "Beana".to_string(), quantity: 362 };
    
    diesel::insert_into(self::schema::stock::table)
        .values([new_item1, new_item2, new_item3])
        .returning(StockItem::as_returning())
        .get_results(connection)
        .expect("Error saving new stock item");
    
    let results = stock
        .filter(quantity.ge(300))
        .select(StockItem::as_select())
        .load(connection)
        .expect("Error loading stock items");

    for item in results {
        println!("{:?}", item);
    }
}

Crystal

ORM in use: granite. Migration tool: micrate. These are part of amber framework.

require "granite/adapter/pg"

Granite::Connections << Granite::Adapter::Pg.new(name: "testdb", url: "postgres://postgres:pass@127.0.0.1/postgres")

class Stock < Granite::Base
  connection testdb
  table stock

  column name : String, primary: true, auto: false
  column vendor : String
  column quantity : Int32
end

Stock.new(name: "Toothbrush", vendor: "Limo", quantity: 1497).save
Stock.new(name: "Comb", vendor: "Takoon", quantity: 210).save
Stock.new(name: "Towel", vendor: "Beana", quantity: 362).save

Stock.where(:quantity, :gteq, 300).select.each do |item|
  # puts item.name # accessing fields also possible
  puts item.to_json
end
  • Initialize DB by calling: shards run micrate -- up
  • Uninitialize DB by calling: shards run micrate -- down

Debug Print

Objective: Print value of a variable along with its name.

Python

x = 123
print(f"{x=}")

Rust

fn main() {
    let x = 123;
    dbg!(x);
}

Crystal

x = 123
p! x

Inspection

Objective: Extend class by a method that will print names and types of the instance variables.

Python

Based on type hints:

class Inspectable:
    def show_vars(self):
        for field_name, field_type in self.__annotations__.items():
            print(f"{field_name!r} : {field_type.__name__}")

class Foo(Inspectable):
    bar: int
    baz: str

foo = Foo()
foo.show_vars()

Based the values of variables:

class Inspectable:
    def show_vars(self):
        for field_name in dir(self):
            if not (field_name.startswith('__') and field_name.endswith('__')):
                field_value = getattr(self, field_name)
                if not callable(field_value):
                    print(f"{field_name!r} : {field_value.__class__.__name__}")

class Foo(Inspectable):
    def __init__(self):
        self.bar = 123
        self.baz = "qwx"

foo = Foo()
foo.show_vars()

Note that there are more corner cases to be considered in Python. For instance how to differentiate instance variables and methods. Whether dunder variables should be considered or not, etc.

Rust

// inspired by: https://stackoverflow.com/a/56389650

macro_rules! generate_struct {
    ($name:ident {$($field_name:ident : $field_type:ty),+}) => {
        struct $name { $($field_name: $field_type),+ }
        impl $name {
            fn show_vars(&self) {
            $(
            let field_name = stringify!($field_name);
            let field_type = stringify!($field_type);
               println!("{:?} : {:?}", field_name, field_type);
            )*
            }
        }
    };
}

generate_struct! { Foo { bar: i32, baz: String } }

fn main() {
    let foo = Foo{bar: 123, baz: "abc".to_string()};
    foo.show_vars();
}

Crystal

class Object
  def vars
    {{ @type.instance_vars.map {|v| v.name.stringify + " : " + v.type.stringify} }}
  end
  
  def show_vars
    self.vars.each {|v| puts v}
  end
end

class Foo
  def initialize(@bar : Int32, @baz : String)
  end
end

foo = Foo.new 123, "abc"
foo.show_vars()

RegEx

Objective: Use regex to pick the name from the sentence: "Hello John!"

Python

import re

text = "Hello John!"

if m := re.search("(\w+)!$", text):
    name = m.groups()[0]
    print(f"Name: {name}")

Rust

There is no regex in stdlib. Crate regex has been used.

use regex::Regex;

fn main() {
    let text = "Hello John!";
    let re = Regex::new(r"(\w+)\!$").unwrap();
    
    if let Some(m) = re.captures(text) {
        println!("Name: {}", &m[1]);
    }
}

Crystal

text = "Hello John!"

/(\w+)\!$/.match(text).try do |x|
  name = x[1]
  puts "Name: #{name}"
end

Rust - thiserror

This chapter presents usage of thiserror crate.

Objectives

  • Implement a chain of functions where one calls another.
  • Return an error from the innermost function.
  • Add context to the errors on each subsequent level.
  • Handle the error on the top level by displaying all the causes.
  • Consider std::io::Error as well as user-defined error as a root cause.

Implementation

// config.rs
use std::fmt::Debug;

#[derive(thiserror::Error, Debug)]
pub enum ConfigError {
    #[error("Failed to read config file")]
    FileReadError(#[from] std::io::Error),
    
    #[error("Failed to parse config file: {0}")]
    ParseConfigError(String),
}

pub fn load_config() -> Result<(), ConfigError> {
    let _text = std::fs::read_to_string("myconfig")?;
    Err(ConfigError::ParseConfigError("Unknown key 'foo'".to_string()))?;
    return Ok(());
}
// setup.rs
use std::fmt::Debug;

use crate::config;

#[derive(thiserror::Error, Debug)]
pub enum SetupError {
    #[error("Failed to configure")]
    LoadConfigError(#[from] config::ConfigError),
}

pub fn setup_app() -> Result<(), SetupError> {
    Ok(config::load_config()?)
}
// launcher.rs
use std::fmt::Debug;
use std::error::Error;

use crate::setup;

#[derive(thiserror::Error)]
pub enum LaunchError {
    #[error("Failed to setup application")]
    SetupAppError(#[from] setup::SetupError),
}

impl Debug for LaunchError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self)?;
        
        let mut err_obj : &dyn Error = self;
        while let Some(source) = err_obj.source() {
            write!(f, "\n  Because: {}", source)?;
            err_obj = source;
        }
        Ok(())
    }
}

pub fn launch_app() -> Result<(), LaunchError> {
    Ok(setup::setup_app()?)
}
// main.rs
mod config;
mod setup;
mod launcher;

fn show_err_stack(top_err: &dyn std::error::Error) {
    eprintln!("{}", top_err);
    #[cfg(debug_assertions)]
    {
        let mut err_obj : &dyn std::error::Error = top_err;
        while let Some(source) = err_obj.source() {
            eprintln!("  Because: {}", source);
            err_obj = source;
        }
    }
}

fn main() {

    if let Err(e) = launcher::launch_app() {
        //println!("{}", e);   // show only top err message
        //println!("{:?}", e); // show all the errors
        show_err_stack(&e);  // show all the errors (customized)
        std::process::exit(1);
    }
   
}

Testing

$ cargo build
$ ./target/debug/thiserror_demo
Failed to setup application
  Because: Failed to configure
  Because: Failed to read config file
  Because: No such file or directory (os error 2)

$ touch myconfig
$ ./target/debug/thiserror_demo
Failed to setup application
  Because: Failed to configure
  Because: Failed to parse config file: Unknown key 'foo'

Open Topics

  • add impl Debug for T for each error that derive(DebugStack)
  • add config file path to FileReadError

Rust - anyhow

This chapter presents usage of anyhow crate.

Objectives

  • Implement a chain of functions where one calls another.
  • Return an error from the innermost function.
  • Add context to the errors on each subsequent level.
  • Handle the error on the top level by displaying all the causes.
  • Consider std::io::Error as well as user-defined error as a root cause.

Implementation

// config.rs
use anyhow::{Result, Context, bail};

pub fn load_config() -> Result<()> {
    let path = "myconfig";  
    let _text = std::fs::read_to_string(path)
        .context(format!("Failed to read config file: {}", path))?;
        
    let key = "foo";
    bail!("Unknown key '{}'", key);
}
// setup.rs
use anyhow::{Result, Context};

use crate::config;

pub fn setup_app() -> Result<()> {
    Ok(config::load_config().context("Failed to configure")?)
}
// launcher.rs
use anyhow::{Result, Context};

use crate::setup;

pub fn launch_app() -> Result<()> {
    Ok(setup::setup_app().context("Failed to setup application")?)
}
// main.rs
use anyhow::Result;
mod config;
mod setup;
mod launcher;

fn main() -> Result<()> {
    launcher::launch_app()
}

Testing

$ cargo build
$ ./target/debug/anyhow_demo
Error: Failed to setup application

Caused by:
    0: Failed to configure
    1: Failed to read config file: myconfig
    2: No such file or directory (os error 2)

$ touch myconfig
$ ./target/debug/anyhow_demo
Error: Failed to setup application

Caused by:
    0: Failed to configure
    1: Unknown key 'foo'