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
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 Some
can 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'