Cogs and Levers A blog full of technical stuff

Revisiting SPR in Haskell

Introduction

Quite some time ago, I wrote a post about a very simple Scissors, Paper, Rock implementation using Haskell. In today’s post, I’d like to revisit that code and clean it up with some tests now that I know a little more.

Avoiding so much do

One point is to avoid the use of do notation, when it’s not needed.

-- Map string to Move
str2Move :: String -> Move
str2Move "s" = Scissors
str2Move "p" = Paper
str2Move "r" = Rock
str2Move _   = Unknown

-- Determine the move that beats the given move
getWinner :: Move -> Move
getWinner Scissors = Rock
getWinner Rock     = Paper
getWinner Paper    = Scissors
getWinner Unknown  = Unknown

These functions were previously do notated, can be simplified back to these translations. The usage of pattern matching here also improves the readability of the code.

Improved randomness

What was being used before getStdGen has now been replaced with newStdGen, which gives us a new random generator per game, improving the randomness.

main :: IO ()
main = do
   gen <- newStdGen

Tests

To verify our game logic, some tests have been added using Hspec.

-- MainSpec.hs
module MainSpec where

import Test.Hspec
import System.Random (mkStdGen)
import Main  -- Import your module here

main :: IO ()
main = hspec $ do
    describe "str2Move" $ do
        it "converts 's' to Scissors" $
            str2Move "s" `shouldBe` Scissors
        it "converts 'p' to Paper" $
            str2Move "p" `shouldBe` Paper
        it "converts 'r' to Rock" $
            str2Move "r" `shouldBe` Rock
        it "returns Unknown for invalid input" $
            str2Move "x" `shouldBe` Unknown

    describe "getWinner" $ do
        it "Rock beats Scissors" $
            getWinner Scissors `shouldBe` Rock
        it "Paper beats Rock" $
            getWinner Rock `shouldBe` Paper
        it "Scissors beat Paper" $
            getWinner Paper `shouldBe` Scissors
        it "Unknown returns Unknown" $
            getWinner Unknown `shouldBe` Unknown

    describe "getOutcome" $ do
        it "returns Draw when both moves are the same" $
            getOutcome Rock Rock `shouldBe` Draw
        it "returns Winner when player beats CPU" $
            getOutcome Rock Scissors `shouldBe` Winner
        it "returns Loser when CPU beats player" $
            getOutcome Scissors Rock `shouldBe` Loser
        it "returns ND for Unknown player move" $
            getOutcome Unknown Rock `shouldBe` ND
        it "returns ND for Unknown CPU move" $
            getOutcome Rock Unknown `shouldBe` ND

    describe "getCpuMove" $ do
        it "returns Rock for seed 1" $
            getCpuMove (mkStdGen 1) `shouldBe` Rock
        it "returns Scissors for seed 2" $
            getCpuMove (mkStdGen 2) `shouldBe` Scissors
        it "returns Paper for seed 3" $
            getCpuMove (mkStdGen 3) `shouldBe` Paper

Here is the full code listing:

module Main where

import System.IO
import System.Random

data Move = Scissors | Paper | Rock | Unknown deriving (Eq, Show)
data Outcome = Winner | Draw | Loser | ND deriving (Show)

-- Map string to Move
str2Move :: String -> Move
str2Move "s" = Scissors
str2Move "p" = Paper
str2Move "r" = Rock
str2Move _   = Unknown

-- Determine the move that beats the given move
getWinner :: Move -> Move
getWinner Scissors = Rock
getWinner Rock     = Paper
getWinner Paper    = Scissors
getWinner Unknown  = Unknown

-- Calculate the outcome based on player and CPU moves
getOutcome :: Move -> Move -> Outcome
getOutcome player cpu
   | player == Unknown || cpu == Unknown = ND
   | player == cpu = Draw
   | cpu == getWinner player = Loser
   | otherwise = Winner

-- Generate a CPU move based on random number
getCpuMove :: StdGen -> Move
getCpuMove gen = case fst (randomR (1, 3) gen :: (Int, StdGen)) of
   1 -> Rock
   2 -> Scissors
   3 -> Paper
   _ -> Unknown  -- This case is unreachable but keeps pattern exhaustive

main :: IO ()
main = do
   gen <- newStdGen  -- Get a new generator each round for more randomness
   putStr "Enter your choice (s, p, or r): "
   hFlush stdout
   line <- getLine

   let player = str2Move line
   if player == Unknown
      then putStrLn "Invalid input! Please enter 's', 'p', or 'r'."
      else do
         let cpu = getCpuMove gen
         let outcome = getOutcome player cpu

         putStrLn $ "Player Chose: " ++ show player
         putStrLn $ "CPU Chose   : " ++ show cpu
         putStrLn $ "Outcome     : " ++ show outcome

Learning Rust

Introduction

In an attempt to put myself on an accelerated learning course about Rust, I’ve produced a number of articles in a “Learn Rust” series.

Part 1-3: Core Concepts

These articles introduce you to Rust’s syntax, ownership model, and error handling, forming the essential foundation of Rust programming.

Part 4-9: Advanced Language Features

Learn about Rust’s advanced features that make it unique, including concurrency, macros, generics, and more. These tools help you write safe, efficient, and reusable code.

Part 10-13: Development Tools and Web Programming

This section explores Rust’s ecosystem for testing, package management, and web development, providing tools to help you maintain, extend, and deploy Rust applications.

Part 14-16: Specialized Topics

Dive into Rust’s applications in secure programming, systems development, and interoperability with other languages. These areas highlight Rust’s unique strengths and its use in performance-critical applications.

Dive in, start with Part 1

Learning Rust Part 9 - Files and I/O

Introduction

Rust’s I/O capabilities provide a range of options for efficiently handling files, streams, and standard input/output. Rust’s std::fs module offers synchronous file handling, while libraries like tokio and async-std add support for asynchronous I/O, enabling non-blocking operations. In this post, we’ll explore Rust’s key I/O operations, including file reading, writing, metadata, streaming, and error handling.

Standard Input and Output

Rust provides convenient tools for interacting with the console, allowing programs to communicate with users or other processes.

Standard Output

Rust’s print!, println!, and eprintln! macros are used to display messages. println! sends output to standard output, while eprintln! sends output to standard error.

fn main() {
    println!("Hello, world!");      // Standard output
    eprintln!("This is an error");   // Standard error
}

Standard Input

To read user input, std::io::stdin provides a read_line method that stores console input into a String.

use std::io;

fn main() {
    let mut input = String::new();
    println!("Enter your name:");
    io::stdin().read_line(&mut input).expect("Failed to read input");
    println!("Hello, {}", input.trim());
}

Reading and Writing Files

Rust’s std::fs module makes file reading and writing straightforward, offering methods like File::open for reading and File::create for writing.

Reading Files

The read_to_string method reads the entire contents of a file into a String.

use std::fs;

fn main() -> std::io::Result<()> {
    let content = fs::read_to_string("example.txt")?;
    println!("{}", content);
    Ok(())
}

Writing Files

To write to a file, use File::create to open or create the file, and write_all to write bytes to it.

use std::fs::File;
use std::io::Write;

fn main() -> std::io::Result<()> {
    let mut file = File::create("output.txt")?;
    file.write_all(b"Hello, world!")?;
    Ok(())
}

Streaming I/O

Streaming I/O is efficient for reading or writing large files in chunks, especially when loading the entire file into memory is impractical. BufReader and BufWriter provide buffering for improved performance.

Buffered Reading

BufReader reads data in chunks, storing it in a buffer for efficient access.

use std::fs::File;
use std::io::{self, BufRead, BufReader};

fn main() -> io::Result<()> {
    let file = File::open("example.txt")?;
    let reader = BufReader::new(file);

    for line in reader.lines() {
        println!("{}", line?);
    }
    Ok(())
}

Buffered Writing

BufWriter buffers output, which is particularly useful when writing multiple small pieces of data.

use std::fs::File;
use std::io::{self, BufWriter, Write};

fn main() -> io::Result<()> {
    let file = File::create("buffered_output.txt")?;
    let mut writer = BufWriter::new(file);

    writer.write_all(b"Hello, world!")?;
    writer.flush()?; // Ensures all data is written
    Ok(())
}

File Metadata and Permissions

Rust allows access to and modification of file metadata, including permissions and timestamps, via the metadata method and the Permissions struct.

Retrieving Metadata

The metadata function provides details such as file size and permissions.

use std::fs;

fn main() -> std::io::Result<()> {
    let metadata = fs::metadata("example.txt")?;
    println!("File size: {}", metadata.len());
    println!("Is read-only: {}", metadata.permissions().readonly());
    Ok(())
}

Changing Permissions

You can modify file permissions with set_permissions, which can be particularly useful for restricting access to sensitive files.

use std::fs;
use std::os::unix::fs::PermissionsExt;

fn main() -> std::io::Result<()> {
    let mut perms = fs::metadata("example.txt")?.permissions();
    perms.set_readonly(true);
    fs::set_permissions("example.txt", perms)?;
    Ok(())
}

Asynchronous I/O

For non-blocking I/O, Rust offers asynchronous support through libraries like tokio and async-std. These libraries allow file and network operations to run without blocking the main thread, making them ideal for scalable applications.

Using Tokio for Async I/O

The tokio::fs module provides async counterparts to common file operations, like reading and writing.

use tokio::fs::File;
use tokio::io::AsyncWriteExt;

#[tokio::main]
async fn main() -> tokio::io::Result<()> {
    let mut file = File::create("async_output.txt").await?;
    file.write_all(b"Hello, async world!").await?;
    Ok(())
}

Async Streaming with Tokio

BufReader and BufWriter are also available in asynchronous forms with Tokio, enabling efficient non-blocking I/O.

use tokio::fs::File;
use tokio::io::{self, AsyncBufReadExt, BufReader};

#[tokio::main]
async fn main() -> io::Result<()> {
    let file = File::open("example.txt").await?;
    let reader = BufReader::new(file);

    let mut lines = reader.lines();
    while let Some(line) = lines.next_line().await? {
        println!("{}", line);
    }
    Ok(())
}

Error Handling in I/O Operations

Error handling is essential in I/O operations, as access to files can fail due to permissions, missing files, or storage limitations. Rust’s Result type and the ? operator streamline error handling in I/O tasks.

Using Result and ? for Concise Error Handling

Most I/O functions return Result, enabling explicit error handling or propagation with ?. We covered this syntax in part 3 of this series.

use std::fs;

fn read_file() -> std::io::Result<String> {
    let content = fs::read_to_string("example.txt")?;
    Ok(content)
}

fn main() {
    match read_file() {
        Ok(content) => println!("File content: {}", content),
        Err(e) => eprintln!("Error reading file: {}", e),
    }
}

Summary

Rust provides comprehensive tools for file handling and I/O, from basic read/write operations to asynchronous streaming and metadata management. With built-in error handling and async capabilities, Rust’s I/O tools allow for efficient, flexible, and reliable code, making it well-suited for building high-performance applications that handle complex I/O tasks with ease.

Learning Rust Part 8 - Unsafe

Introduction

Rust is known for its strong safety guarantees, particularly around memory safety, achieved through strict ownership and borrowing rules. However, certain low-level operations—like raw pointer manipulation and foreign function interfaces—require bypassing these safety checks.

Rust’s unsafe code capabilities provide the necessary control for these cases, but they come with potential risks. In this post, we’ll explore unsafe Rust’s capabilities, including raw pointers, unsafe blocks, and FFI (Foreign Function Interface), and offer best practices for safe usage.

Raw Pointers

In Rust, raw pointers (*const T for immutable and *mut T for mutable) enable low-level memory manipulation similar to pointers in C. Unlike Rust references (& and &mut), raw pointers:

  • Don’t enforce Rust’s borrowing rules.
  • Can be null or dangling.
  • Are not automatically dereferenced.

Creating Raw Pointers

Raw pointers are created using the as keyword for casting or by using Box::into_raw for heap allocations.

fn main() {
    let x = 42;
    let r1 = &x as *const i32; // Immutable raw pointer
    let r2 = &x as *mut i32;   // Mutable raw pointer
}

Dereferencing and Pointer Arithmetic

With raw pointers, you can dereference or manipulate memory addresses directly. However, Rust requires an unsafe block to perform these actions due to the inherent risks.

Dereferencing Raw Pointers

Dereferencing a raw pointer retrieves the value it points to. This operation is only allowed in an unsafe block, as dereferencing invalid pointers can lead to crashes or undefined behavior.

fn main() {
    let x = 42;
    let r = &x as *const i32;

    unsafe {
        println!("Value at pointer: {}", *r); // Unsafe dereference
    }
}

Pointer Arithmetic

Raw pointers also support pointer arithmetic, allowing you to manually navigate memory addresses. This is especially useful for low-level data manipulation.

fn main() {
    let arr = [10, 20, 30];
    let ptr = arr.as_ptr();

    unsafe {
        println!("Second element: {}", *ptr.add(1)); // Accesses arr[1]
    }
}

Unsafe Blocks

Rust confines certain risky operations to unsafe blocks to help isolate potentially unsafe code from safe parts of the program. This provides flexibility while containing risks.

Operations Allowed in Unsafe Blocks

Only the following operations are allowed in unsafe blocks:

  • Dereferencing raw pointers.
  • Calling unsafe functions.
  • Accessing or modifying mutable static variables.
  • Implementing unsafe traits.
  • Accessing union fields.
unsafe fn dangerous() {
    println!("This function is unsafe to call.");
}

fn main() {
    unsafe {
        dangerous();
    }
}

FFI (Foreign Function Interface)

Rust’s Foreign Function Interface (FFI) lets you call functions from other languages, such as C, making it valuable for systems programming or integrating existing C libraries.

Declaring an External Function

To call a C function from Rust, use extern "C", which specifies the C calling convention. Here’s an example of calling C’s abs function to find the absolute value.

extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("Absolute value: {}", abs(-5));
    }
}

Defining Functions for C

You can also use extern "C" to make Rust functions callable from C by adding the #[no_mangle] attribute, which prevents Rust from renaming the function during compilation.

#[no_mangle]
pub extern "C" fn my_function() {
    println!("Called from C code!");
}

Working with C Libraries

To use external libraries, add the #[link] attribute, which specifies the library’s name. For example, here’s how to link to the math library (libm) for advanced mathematical functions.

Using C Libraries (Linking and Calling)

The following example demonstrates calling sqrt from the math library.

#[link(name = "m")]
extern "C" {
    fn sqrt(x: f64) -> f64;
}

fn main() {
    unsafe {
        println!("Square root of 9.0 is {}", sqrt(9.0));
    }
}

Note: You may need to configure linking in your Cargo.toml to include the library during compilation.

Undefined Behavior and Safety

Unsafe Rust allows for operations that, if misused, can lead to undefined behavior. Common causes of undefined behavior include:

  • Dereferencing null or dangling pointers.
  • Breaking Rust’s aliasing rules (e.g., multiple mutable references).
  • Accessing memory out of bounds.
  • Using uninitialized data.

Safety Tips for Using Unsafe Code

To minimize risks when using unsafe code, follow these best practices:

  • Limit unsafe code to small, well-defined sections to make it easier to review and understand.
  • Wrap unsafe code in safe abstractions to prevent direct access to risky operations.
  • Thoroughly review unsafe code, especially around pointer dereferencing and FFI calls.

By isolating and encapsulating unsafe operations within safe APIs, you can maintain Rust’s safety guarantees while still taking advantage of low-level control when necessary.

Summary

Unsafe Rust provides tools like raw pointers, unsafe blocks, and FFI to extend Rust’s capabilities in low-level programming, where direct memory access and foreign function calls are required. While these features offer powerful flexibility, they should be used sparingly and with caution to avoid undefined behavior. With proper handling, unsafe code can be an invaluable tool, enabling Rust developers to push the boundaries of Rust’s memory safety model without sacrificing control or performance.

Learning Rust Part 7 - Macros and Metaprogramming

Introduction

Rust’s macros offer powerful metaprogramming tools, enabling code generation, compile-time optimizations, and even domain-specific languages (DSLs). Unlike functions, macros operate at compile time, which makes them flexible and efficient but also requires careful usage. Rust’s macro system includes two primary types: declarative macros and procedural macros. In this post, we’ll explore both types and look at some practical examples.

Declarative Macros (macro_rules!)

Declarative macros, created with macro_rules!, use pattern matching to expand code at compile time. These macros are ideal for handling repetitive code patterns and for defining custom DSLs.

Defining a Declarative Macro

Here’s an example of a logging macro that can handle multiple arguments. The macro uses pattern matching to determine how to expand the code.

macro_rules! log {
    ($msg:expr) => {
        println!("[LOG]: {}", $msg);
    };
    ($fmt:expr, $($arg:tt)*) => {
        println!("[LOG]: {}", format!($fmt, $($arg)*));
    };
}

fn main() {
    log!("Starting application");
    log!("Hello, {}", "world");
}

This log! macro can be called with either a single expression or a format string with additional arguments.

Repeaters ($()*)

Macros can use repeaters like $(...)* to handle variable numbers of arguments. Here’s a macro that generates a Vec<String> from a list of string literals:

macro_rules! vec_of_strings {
    ($($x:expr),*) => {
        vec![$(String::from($x)),*]
    };
}

fn main() {
    let v = vec_of_strings!["apple", "banana", "cherry"];
    println!("{:?}", v);
}

This macro makes it easy to create a vector of strings without repeating String::from for each element.

Metavariables

In Rust macros, metavariables specify the kinds of expressions a macro can match. Here’s a look at the most commonly used types, with examples to help clarify each one.

expr: Expressions

The expr metavariable type represents any valid Rust expression. This includes literals, function calls, arithmetic operations, and more.

macro_rules! log {
    ($msg:expr) => { 
        println!("[LOG]: {}", $msg); 
    };
}

fn main() {
    log!(42);                // 42 is a literal expression
    log!(5 + 3);             // 5 + 3 is an arithmetic expression
    log!("Hello, world!");   // "Hello, world!" is a string literal expression
}

tt: Token Tree

The tt metavariable stands for token tree and is the most flexible type, accepting any valid Rust token or group of tokens. This includes literals, expressions, blocks, or even entire function bodies. tt is often used for parameters with variable length, as in $($arg:tt)*.

In the example, ($($arg:tt)*) allows the macro to accept a variable number of arguments, each matching the tt pattern.

macro_rules! log {
    ($fmt:expr, $($arg:tt)*) => {
        println!("[LOG]: {}", format!($fmt, $($arg)*));
    };
}

fn main() {
    log!("Hello, {}", "world");    // "Hello, {}" is matched as $fmt, "world" as $arg
    log!("Values: {} and {}", 1, 2); // Two arguments matched as $arg
}

In this case, ($fmt:expr, $($arg:tt)*):

  • $fmt:expr matches a single format string.
  • $($arg:tt)* matches a sequence of additional arguments, like "world" or 1, 2.

Other Common Metavariable Types

Rust macros support additional metavariable types, each providing a different kind of flexibility. Here are some other commonly used types:

  • ident: Matches an identifier (variable, function, or type name).
macro_rules! make_var {
    ($name:ident) => {
        let $name = 10;
    };
}

fn main() {
    make_var!(x); // Expands to: let x = 10;
    println!("{}", x);
}
  • ty: Matches a type (like i32 or String).
macro_rules! make_vec {
    ($type:ty) => {
        Vec::<$type>::new()
    };
}

fn main() {
    let v: Vec<i32> = make_vec!(i32); // Expands to Vec::<i32>::new()
}
  • pat: Matches a pattern, often used in match arms.
macro_rules! match_num {
    ($num:pat) => {
        match $num {
            1 => println!("One"),
            _ => println!("Not one"),
        }
    };
}

fn main() {
    match_num!(1);
}
  • literal: Matches literal values like numbers, characters, or strings. Useful when you need to capture only literal values.
macro_rules! print_literal {
    ($x:literal) => {
        println!("Literal: {}", $x);
    };
}

fn main() {
    print_literal!(42);       // Works, as 42 is a literal
    // print_literal!(5 + 5); // Error: 5 + 5 is not a literal
}

Metavariable types

Here’s a quick reference of metavariable types commonly used in Rust macros:

Metavariable Matches Example
expr Any valid Rust expression 5 + 3, hello, foo()
tt Any token tree 1, { 1 + 2 }, foo, bar
ident Identifiers my_var, TypeName
ty Types i32, String
pat Patterns _, Some(x), 1..=10
literal Literals 42, 'a', "text"

Procedural Macros

Procedural macros allow more advanced metaprogramming by directly manipulating Rust’s syntax. They operate on tokens (the syntactic elements of code) rather than strings, offering greater control over code generation. Procedural macros are defined as separate functions, usually in a dedicated crate.

Types of Procedural Macros

Rust supports three main types of procedural macros:

  • Function-like macros: Called like functions but with macro-level flexibility.
  • Attribute macros: Add custom behavior to items like functions and structs.
  • Derive macros: Automatically implement traits for structs or enums.

Creating a Function-like Macro

A function-like macro uses the proc_macro crate to manipulate tokens directly. Here’s an example that generates a function called hello that prints a greeting:

use proc_macro;

#[proc_macro]
pub fn hello_macro(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input_str = input.to_string();
    format!("fn hello() {{ println!(\"Hello, {}!\"); }}", input_str).parse().unwrap()
}

This macro generates a hello function that prints a customized message. It would typically be used by adding hello_macro!("Rust"); to the main code, and would output Hello, Rust!.

Attribute Macros

Attribute macros attach custom attributes to items, making them useful for adding behaviors to functions, structs, or enums. For instance, an attribute macro can automatically log messages when entering and exiting a function.

use proc_macro::TokenStream;

#[proc_macro_attribute]
pub fn log(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let input = item.to_string();
    let output = format!(
        "fn main() {{
            println!(\"Entering function\");
            {}
            println!(\"Exiting function\");
        }}", input
    );
    output.parse().unwrap()
}

When applied to main, this macro logs messages before and after function execution, helping with function-level tracing.

Derive Macros

Derive macros are a powerful feature in Rust, enabling automatic trait implementation for custom data types. Commonly used for traits like Debug, Clone, and PartialEq, derive macros simplify code by eliminating the need for manual trait implementation.

Implementing a Derive Macro

Suppose we want to implement a custom Hello trait that prints a greeting. We can create a derive macro to automatically implement Hello for any struct annotated with #[derive(Hello)].

First, define the Hello trait:

pub trait Hello {
    fn say_hello(&self);
}

Then, implement the derive macro in a procedural macro crate:

use proc_macro::TokenStream;
use quote::quote;

#[proc_macro_derive(Hello)]
pub fn hello_derive(input: TokenStream) -> TokenStream {
    let ast: syn::DeriveInput = syn::parse(input).unwrap();
    let name = &ast.ident;

    let gen = quote! {
        impl Hello for #name {
            fn say_hello(&self) {
                println!("Hello from {}", stringify!(#name));
            }
        }
    };
    gen.into()
}

Now, any struct tagged with #[derive(Hello)] will automatically implement the Hello trait, making the code more modular and concise.

Domain-Specific Languages (DSLs) with Macros

Rust’s macros can be used to create DSLs, enabling specialized, readable syntax for specific tasks.

Example: Creating a Simple DSL

Here’s an example of a DSL for building SQL-like queries. The query! macro translates the input syntax into a formatted SQL query string.

macro_rules! query {
    ($table:expr => $($col:expr),*) => {
        format!("SELECT {} FROM {}", stringify!($($col),*), $table)
    };
}

fn main() {
    let sql = query!("users" => "id", "name", "email");
    println!("{}", sql); // Outputs: SELECT id, name, email FROM users
}

This example uses macro_rules! to create a custom query builder, transforming macro input into SQL syntax in a natural format.

Summary

Rust’s macros and metaprogramming features provide versatile tools for code generation, manipulation, and optimization. With declarative macros for straightforward pattern matching, procedural macros for syntax manipulation, and derive macros for auto-implementing traits, Rust enables developers to write efficient, flexible, and concise code. Macros can help create DSLs or extend functionality in powerful ways, making Rust an excellent choice for both performance and code expressiveness.