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 Movestr2Move::String->Movestr2Move"s"=Scissorsstr2Move"p"=Paperstr2Move"r"=Rockstr2Move_=Unknown-- Determine the move that beats the given movegetWinner::Move->MovegetWinnerScissors=RockgetWinnerRock=PapergetWinnerPaper=ScissorsgetWinnerUnknown=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=dogen<-newStdGen
Tests
To verify our game logic, some tests have been added using Hspec.
-- MainSpec.hsmoduleMainSpecwhereimportTest.HspecimportSystem.Random(mkStdGen)importMain-- Import your module heremain::IO()main=hspec$dodescribe"str2Move"$doit"converts 's' to Scissors"$str2Move"s"`shouldBe`Scissorsit"converts 'p' to Paper"$str2Move"p"`shouldBe`Paperit"converts 'r' to Rock"$str2Move"r"`shouldBe`Rockit"returns Unknown for invalid input"$str2Move"x"`shouldBe`Unknowndescribe"getWinner"$doit"Rock beats Scissors"$getWinnerScissors`shouldBe`Rockit"Paper beats Rock"$getWinnerRock`shouldBe`Paperit"Scissors beat Paper"$getWinnerPaper`shouldBe`Scissorsit"Unknown returns Unknown"$getWinnerUnknown`shouldBe`Unknowndescribe"getOutcome"$doit"returns Draw when both moves are the same"$getOutcomeRockRock`shouldBe`Drawit"returns Winner when player beats CPU"$getOutcomeRockScissors`shouldBe`Winnerit"returns Loser when CPU beats player"$getOutcomeScissorsRock`shouldBe`Loserit"returns ND for Unknown player move"$getOutcomeUnknownRock`shouldBe`NDit"returns ND for Unknown CPU move"$getOutcomeRockUnknown`shouldBe`NDdescribe"getCpuMove"$doit"returns Rock for seed 1"$getCpuMove(mkStdGen1)`shouldBe`Rockit"returns Scissors for seed 2"$getCpuMove(mkStdGen2)`shouldBe`Scissorsit"returns Paper for seed 3"$getCpuMove(mkStdGen3)`shouldBe`Paper
Here is the full code listing:
moduleMainwhereimportSystem.IOimportSystem.RandomdataMove=Scissors|Paper|Rock|Unknownderiving(Eq,Show)dataOutcome=Winner|Draw|Loser|NDderiving(Show)-- Map string to Movestr2Move::String->Movestr2Move"s"=Scissorsstr2Move"p"=Paperstr2Move"r"=Rockstr2Move_=Unknown-- Determine the move that beats the given movegetWinner::Move->MovegetWinnerScissors=RockgetWinnerRock=PapergetWinnerPaper=ScissorsgetWinnerUnknown=Unknown-- Calculate the outcome based on player and CPU movesgetOutcome::Move->Move->OutcomegetOutcomeplayercpu|player==Unknown||cpu==Unknown=ND|player==cpu=Draw|cpu==getWinnerplayer=Loser|otherwise=Winner-- Generate a CPU move based on random numbergetCpuMove::StdGen->MovegetCpuMovegen=casefst(randomR(1,3)gen::(Int,StdGen))of1->Rock2->Scissors3->Paper_->Unknown-- This case is unreachable but keeps pattern exhaustivemain::IO()main=dogen<-newStdGen-- Get a new generator each round for more randomnessputStr"Enter your choice (s, p, or r): "hFlushstdoutline<-getLineletplayer=str2Movelineifplayer==UnknownthenputStrLn"Invalid input! Please enter 's', 'p', or 'r'."elsedoletcpu=getCpuMovegenletoutcome=getOutcomeplayercpuputStrLn$"Player Chose: "++showplayerputStrLn$"CPU Chose : "++showcpuputStrLn$"Outcome : "++showoutcome
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.
This section explores Rust’s ecosystem for testing, package management, and web development, providing tools to help you maintain, extend, and deploy Rust applications.
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.
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.
fnmain(){println!("Hello, world!");// Standard outputeprintln!("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.
usestd::io;fnmain(){letmutinput=String::new();println!("Enter your name:");io::stdin().read_line(&mutinput).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.
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.
BufWriter buffers output, which is particularly useful when writing multiple small pieces of data.
usestd::fs::File;usestd::io::{self,BufWriter,Write};fnmain()->io::Result<()>{letfile=File::create("buffered_output.txt")?;letmutwriter=BufWriter::new(file);writer.write_all(b"Hello, world!")?;writer.flush()?;// Ensures all data is writtenOk(())}
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.
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.
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.
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.
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.
fnmain(){letx=42;letr1=&xas*consti32;// Immutable raw pointerletr2=&xas*muti32;// 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.
fnmain(){letx=42;letr=&xas*consti32;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.
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.
unsafefndangerous(){println!("This function is unsafe to call.");}fnmain(){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.
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]pubextern"C"fnmy_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"{fnsqrt(x:f64)->f64;}fnmain(){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:
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.
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.
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:
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);};}fnmain(){log!(42);// 42 is a literal expressionlog!(5+3);// 5 + 3 is an arithmetic expressionlog!("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)*));};}fnmain(){log!("Hello, {}","world");// "Hello, {}" is matched as $fmt, "world" as $arglog!("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;};}fnmain(){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()};}fnmain(){letv:Vec<i32>=make_vec!(i32);// Expands to Vec::<i32>::new()}
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);};}fnmain(){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:
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.
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:
pubtraitHello{fnsay_hello(&self);}
Then, implement the derive macro in a procedural macro crate:
useproc_macro::TokenStream;usequote::quote;#[proc_macro_derive(Hello)]pubfnhello_derive(input:TokenStream)->TokenStream{letast:syn::DeriveInput=syn::parse(input).unwrap();letname=&ast.ident;letgen=quote!{implHellofor #name{fnsay_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)};}fnmain(){letsql=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.