📝 Notes
Things I want to remember.
2023
#learn-rust What is the question mark (?) operator?
The question mark operator provides an alternative and concise approach to handle errors in functions that return the Result
or Option
type. By using the ?
operator, we can reduce the amount of code needed to handle returning Err
or None
from Result<T, Err>
or Option<T>
types.
Here’s a quick example:
Instead of using a match
statement to check the success of a function do_something()
that returns a Result<T, Err>
type, we can achieve the same result with the following shorter syntax:
let a = match do_something() {
Ok(a) => a,
Err(e) => return Err(e),
}
While this is quite expressive, it can be shortened to:
let a = do_something()?;
This operator unwraps valid values and propagates erroneous values to the calling function.
Now, the question arises: should we always use ?
? Well, as is often the case, it depends.
The ?
operator simplifies code and enhances readability by reducing boilerplate. However, it can only be used in functions that return Result
or Option
types. Therefore, changing the return type of a function is necessary to utilize this operator, which may not always be feasible or desirable. Furthermore, the ?
operator is not available in main()
functions.
Additionally, while the ?
operator is thorough and concise, it lacks the ability to provide custom behavior. There might be situations where you prefer to:
- Invoke custom behavior, such as error recovery
- Create a custom error
- Even panic
In such cases, the match statement is still required to fulfill these specific needs.
Let’s consider another example that demonstrates error propagation across multiple function calls:
use std::io;
fn get_user_age() -> Result<u8, io::Error> {
// Simulating an error condition
Err(io::Error::new(std::io::ErrorKind::Other, "could not get age!"))
}
fn get_user_name() -> Result<String, io::Error> {
// Simulating a successful operation
Ok("Alice".to_string())
}
fn process_user_data() -> Result<(), io::Error> {
let _ = get_user_name()?;
let _ = get_user_age()?;
// Processing user data...
Ok(())
}
fn main() {
match process_user_data() {
Ok(()) => println!("User data processed successfully"),
Err(e) => println!("Error: {}", e),
}
}
Since get_user_age()
encounters an error, the program will terminate and display the following message:
Error: could not get age!
Well, this is all I know about the ?
operator. I hope you found this useful.
References
Justfile with polyglot support
I just discovered that the just command runner supports running commands in multiple languages! As I will probably forget this, I will write it down here.
py:
#!/usr/bin/env python3
name = "Python"
print(f'Greetings from {name}!')
js:
#!/usr/bin/env node
const name = "JavaScript";
console.log(`Greetings from ${name}!`)
go:
#!/Users/c.voigt/go/bin/gorun
package main
import "fmt"
func main() {
var name = "Go"
fmt.Printf("Greetings from %s!\n", name)
}
sh:
#!/usr/bin/env sh
name="Shell"
echo "Greetings from ${name}!"
rhai:
#!/Users/c.voigt/.cargo/bin/rhai-run
let answer = "Rhai";
print(`Greetings from ${answer}`);
To run go code I installed gorun. Admittedly this is a bit hacky, but certainly does the job.
To use Rhai install it with cargo: cargo install rhai
and check which rhai-run
to get the path to the executable.
#learn-rust: Rust enums
Defining an enum is not very spectacular
enum Color {
Yellow,
Red,
Green,
Blue,
}
Let’s pattern-match against the enum, forgetting to match one of the variants. The compiler will warn us about this.
fn print_color(color: Color) {
match color {
Color::Yellow => println!("Yellow"),
Color::Red => println!("Red"),
Color::Green => println!("Green"),
// Color::Blue => println!("Blue"),
}
}
Attaching methods to enums is also possible 🤯
impl Color {
fn green_part(&self) -> bool {
match self {
Color::Yellow => true,
Color::Blue => true,
_ => false,
}
}
fn is_green(&self) -> bool {
// pattern matching self on the if-statement
if let Color::Green = self {
return true;
}
return false
}
}
#learn-rust: retrieving optional values
match statement #learn-rust
let fruits = vec!["banana", "apple", "coconut", "orange", "strawberry"];
for &index in [0, 2, 99].iter() {
match fruits.get(index) {
// note the "&" to get the Option<&&str>
Some(&"coconut") => println!("Coconuts are awesome!!!"),
Some(fruit_name) => println!("It's a delicious {}!", fruit_name),
None => println!("There is no fruit! :("),
}
}
Whenever you use the match expression, keep the following rules in mind:
match
arms are evaluated from top to bottom. Specific cases must be defined earlier than generic cases or they’ll never be matched and evaluated.match
arms must cover every possible value that the input type could have. You’ll get a compiler error if you try to match against a non-exhaustive pattern list.
Use pattern matching for convenience#
If we expect a specific Some
value in an Option
, we could use match (and drop anything else):
let a_number: Option = Some(7);
match a_number {
Some(7) => println!("That's my lucky number!"),
_ => {},
}
Or use a shorter, equivalent version:
let a_number: Option = Some(7);
if let Some(7) = a_number {
println!("That's my lucky number!");
}
Use unwrap
and expect
carefully#
unwrap
and expect
can be used, but will panic if Option
is None
let gift = Some("candy");
assert_eq!(gift.unwrap(), "candy");
let empty_gift: Option<&str> = None;
assert_eq!(empty_gift.unwrap(), "candy"); // This will panic!
This will panic like this:
thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', src/main.rs:6:27
With expect
we can set a custom panic message:
let a = Some("value");
assert_eq!(a.expect("fruits are healthy"), "value");
let b: Option<&str> = None;
b.expect("fruits are healthy"); // panics with `fruits are healthy`
thread 'main' panicked at 'fruits are healthy', src/main.rs:6:7
As both, unwrap
and expect
can panic it is recommended to avoid them and use either of following options instead:
- Use pattern matching and handle the
None
case explicitly. - Call similar non-panicking methods, such as
unwrap_or
, which returns a default value if the variant isNone
or the inner value if the variant isSome(value)
.
assert_eq!(Some("dog").unwrap_or("cat"), "dog");
assert_eq!(None.unwrap_or("cat"), "cat");
#learn-rust: generics
Syntax of a generic struct
:
struct Point {
x: T,
y: T,
}
fn main() {
let boolean = Point { x: true, y: false };
let integer = Point { x: 1, y: 9 };
let float = Point { x: 1.7, y: 4.3 };
let string_slice = Point { x: "high", y: "low" };
}
Note, that multiple different types are possible:
struct Point {
x: T,
y: U,
}
fn main() {
let integer_and_boolean = Point { x: 5, y: false };
let float_and_string = Point { x: 1.0, y: "hey" };
let integer_and_float = Point { x: 5, y: 4.0 };
let both_integer = Point { x: 10, y: 30 };
let both_boolean = Point { x: true, y: true };
}
#learn-rust: tests
Syntax to write tests in Rust:
fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod add_function_tests {
use super::*;
#[test]
fn add_works() {
assert_eq!(add(1, 2), 3);
assert_eq!(add(10, 12), 22);
assert_eq!(add(5, -2), 3);
}
#[test]
#[should_panic]
fn add_fails() {
assert_eq!(add(2, 2), 7);
}
#[test]
#[ignore]
fn add_negatives() {
assert_eq!(add(-2, -2), -4)
}
}
The cfg
attribute controls conditional compilation and will only compile the thing it’s attached to if the predicate is true
. The test
compilation flag is issued automatically by Cargo whenever we execute the command $ cargo test
, so it will always be true when we run our tests.
The use super::*;
declaration is necessary for the code inside the add_function_tests
module to access the add
in the outer module.
2022
unable to find zig installation directory: FileNotFound
If zig refused to build because it is “unable to find zig installation directory”, you need to ensure the lib directory resides at the same relative path as the zig binary.
I played around and wanted to create a container image to streamline my Zig build experience. Doing so, I used the official tarball and copied over the zig
binary, put it into /usr/local/bin
, and assumed this was going to work. zig version
even proved me correct:
$ zig version
0.10.0-dev.2431+0e6285c8f
However, once I wanted to build, I got this error message:
error: unable to find zig installation directory '/usr/local/bin/zig': FileNotFound
As usual, Zig error messages aren’t that helpful (yet). Only coincidentally, I found an old dump of zigs freenode IRC channel pointing me in the right direction:
21:25
the zig
binary needs to stay at the same relative path to thelib
dir
So the solution is not only to copy the binary but also the lib directory from the tarball.
Hopefully, this will be picked up by the search engines, so next time someone encounters the issue, it’s a little bit less of a riddle.
2019
Following logs of multiple Kubernetes Pods
Following your logs is a crucial part of application and operation debugging. tail -f
is every sysadmins best friend - easy, fast, and grep
able!
While centralized logging solutions such as ELK/EFK, Splunk or Grafana Loki are almost common sense, they are some times overkill. Also, they are not accessible from the command line - which is usually the center of your debugging operations.
To stream logs of Kubernetes workloads we are already used to:
$ kubctl -n <namespace> logs -f <pod-id>
But what if you want to read log interactions of different workloads? What if you want to see the logs of all replicas of that same pod?
For this purpose Kubernetes allows log streaming based on label selectors:
$ kubectl -n namespace logs -l app=myapp
This will stream logs of all Pods with the label app=myapp
to your console.
As already mentioned, this does not only affect pods of the same type but all pods sharing the same label. This might include Replicasets and Jobs as well!
Of course, grep
ing and other magic is possible as well ❤.