Software architecture in pure languages (Haskell): logic-driven vs. interaction-driven - haskell

I'm trying to learn best practices on how to structure a program in a pure functional language (Haskell), but I'm having trouble with an architecture which I find natural but cannot replicate easily in the "pure world".
Let me describe a simple model case: a two-player guessing game.
Game rules
A secret random integer number in [0, 100] is generated.
Players take turns trying to guess the secret number.
If the guess is correct, the player wins.
Otherwise the game tells whether the secret number is larger or smaller than the guess and the possible range for the unknown secret is updated.
My question concerns the implementation of this game where a human player plays against the computer.
I propose two implementations:
logic driven
interaction driven
Logic-driven
In the logic-driven implementation, the execution is driven by the game logic. A Game has a State and some participating Players. The Game::run function makes the players play in turns and updates the game state until completion. The players receive a state in Player::play and return the move they decide to play.
The benefits that I can see of this approach are:
the architecture is very clean and natural;
it abstracts the nature of a Player: a HumanPlayer and a ComputerPlayer are interchangeable, even if the former has to deal with IO, whereas the latter represents a pure computation (you can verify this by putting Game::new(ComputerPlayer::new("CPU-1"), ComputerPlayer::new("CPU-2")) in the main function and watching the computer battle).
Here is a possible implementation in Rust:
use rand::Rng;
use std::io::{self, Write};
fn min<T: Ord>(x: T, y: T) -> T { if x <= y { x } else { y } }
fn max<T: Ord>(x: T, y: T) -> T { if x >= y { x } else { y } }
struct Game<P1, P2> {
secret: i32,
state: State,
p1: P1,
p2: P2,
}
#[derive(Clone, Copy, Debug)]
struct State {
lower: i32,
upper: i32,
}
struct Move(i32);
trait Player {
fn name(&self) -> &str;
fn play(&mut self, st: State) -> Move;
}
struct HumanPlayer {
name: String,
}
struct ComputerPlayer {
name: String,
}
impl HumanPlayer {
fn new(name: &str) -> Self {
Self {
name: String::from(name),
}
}
}
impl ComputerPlayer {
fn new(name: &str) -> Self {
Self {
name: String::from(name),
}
}
}
impl Player for HumanPlayer {
fn name(&self) -> &str {
&self.name
}
fn play(&mut self, _st: State) -> Move {
let mut s = String::new();
print!("Please enter your guess: ");
let _ = io::stdout().flush();
io::stdin().read_line(&mut s).expect("Error reading input");
let guess = s.trim().parse().expect("Error parsing number");
println!("{} guessing {}", self.name, guess);
Move(guess)
}
}
impl Player for ComputerPlayer {
fn name(&self) -> &str {
&self.name
}
fn play(&mut self, st: State) -> Move {
let mut rng = rand::thread_rng();
let guess = rng.gen_range(st.lower, st.upper + 1);
println!("{} guessing {}", self.name, guess);
Move(guess)
}
}
impl<P1, P2> Game<P1, P2>
where
P1: Player,
P2: Player,
{
fn new(p1: P1, p2: P2) -> Self {
let mut rng = rand::thread_rng();
Game {
secret: rng.gen_range(0, 101),
state: State {
lower: 0,
upper: 100,
},
p1,
p2,
}
}
fn run(&mut self) {
loop {
// Player 1's turn
self.report();
let m1 = self.p1.play(self.state);
if self.update(m1) {
println!("{} wins!", self.p1.name());
break;
}
// Player 2's turn
self.report();
let m2 = self.p2.play(self.state);
if self.update(m2) {
println!("{} wins!", self.p2.name());
break;
}
}
}
fn update(&mut self, mv: Move) -> bool {
let Move(m) = mv;
if m < self.secret {
self.state.lower = max(self.state.lower, m + 1);
false
} else if m > self.secret {
self.state.upper = min(self.state.upper, m - 1);
false
} else {
true
}
}
fn report(&self) {
println!("Current state = {:?}", self.state);
}
}
fn main() {
let mut game = Game::new(HumanPlayer::new("Human"), ComputerPlayer::new("CPU"));
game.run();
}
Interaction-driven
In the interaction-driven implementation, all the functionalities related to the game, including the decisions taken by the computer players, must be pure functions without side effects. HumanPlayer becomes then the interface through which the real person sitting in front of the computer interacts with the game. In a certain sense, the game becomes a function mapping user input to an updated state.
This is the approach that, to my eyes, seems to be forced by a pure language, because all the logic of the game becomes a pure computation, free of side effects, simply transforming an old state to a new state.
I kind of like also this point of view (separating input -> (state transformation) -> output): it definitely has some merits, but I feel that, as can be easily seen in this example, it breaks some other good properties of the program, such as the symmetry between human player and computer player. From the point of view of the game logic, its doesn't matter whether the decision for the next move comes from a pure computation performed by the computer or a user interaction involving IO.
I provide here a reference implementation, again in Rust:
use rand::Rng;
use std::io::{self, Write};
fn min<T: Ord>(x: T, y: T) -> T { if x <= y { x } else { y } }
fn max<T: Ord>(x: T, y: T) -> T { if x >= y { x } else { y } }
struct Game {
secret: i32,
state: State,
computer: ComputerPlayer,
}
#[derive(Clone, Copy, Debug)]
struct State {
lower: i32,
upper: i32,
}
struct Move(i32);
struct HumanPlayer {
name: String,
game: Game,
}
struct ComputerPlayer {
name: String,
}
impl HumanPlayer {
fn new(name: &str, game: Game) -> Self {
Self {
name: String::from(name),
game,
}
}
fn name(&self) -> &str {
&self.name
}
fn ask_user(&self) -> Move {
let mut s = String::new();
print!("Please enter your guess: ");
let _ = io::stdout().flush();
io::stdin().read_line(&mut s).expect("Error reading input");
let guess = s.trim().parse().expect("Error parsing number");
println!("{} guessing {}", self.name, guess);
Move(guess)
}
fn process_human_player_turn(&mut self) -> bool {
self.game.report();
let m = self.ask_user();
if self.game.update(m) {
println!("{} wins!", self.name());
return false;
}
self.game.report();
let m = self.game.computer.play(self.game.state);
if self.game.update(m) {
println!("{} wins!", self.game.computer.name());
return false;
}
true
}
fn run_game(&mut self) {
while self.process_human_player_turn() {}
}
}
impl ComputerPlayer {
fn new(name: &str) -> Self {
Self {
name: String::from(name),
}
}
fn name(&self) -> &str {
&self.name
}
fn play(&mut self, st: State) -> Move {
let mut rng = rand::thread_rng();
let guess = rng.gen_range(st.lower, st.upper + 1);
println!("{} guessing {}", self.name, guess);
Move(guess)
}
}
impl Game {
fn new(computer: ComputerPlayer) -> Self {
let mut rng = rand::thread_rng();
Game {
secret: rng.gen_range(0, 101),
state: State {
lower: 0,
upper: 100,
},
computer,
}
}
fn update(&mut self, mv: Move) -> bool {
let Move(m) = mv;
if m < self.secret {
self.state.lower = max(self.state.lower, m + 1);
false
} else if m > self.secret {
self.state.upper = min(self.state.upper, m - 1);
false
} else {
true
}
}
fn report(&self) {
println!("Current state = {:?}", self.state);
}
}
fn main() {
let mut p = HumanPlayer::new("Human", Game::new(ComputerPlayer::new("CPU")));
p.run_game();
}
Conclusion
An effectful language (such as Rust) gives us the flexibility to choose the approach based on our priorities: either symmetry between human and computer players or sharp separation between pure computation (state transformation) and IO (user interaction).
Given my current knowledge of Haskell, I cannot say the same about the pure world: I feel forced to adopt the second approach, because the first one would be littered with IO everywhere. In particular, I would like the hear some words from some functional programming gurus on how to implement in Haskell the logic-driven approach and what are their opinions/comments on the subject.
I'm prepared to learn a lot of insight from this.

I suppose the Haskell equivalent of your Logic-Driven implementation (with error handling omitted) is something like this:
{-# OPTIONS_GHC -Wall #-}
{-# LANGUAGE FlexibleContexts #-}
import System.Random
import Control.Monad
import Control.Monad.State
import Control.Monad.Trans.Maybe
data Game = Game Int Range Player Player
type Range = (Int, Int)
data Player = Player String (Range -> IO Int)
humanPlayer, computerPlayer :: String -> Player
humanPlayer nam = Player nam strategy
where strategy _ = do
putStrLn "Please enter your guess:"
readLn
computerPlayer nam = Player nam strategy
where strategy s = do
x <- randomRIO s
putStrLn $ nam ++ " guessing " ++ show x
return x
newGame :: Player -> Player -> IO Game
newGame p1 p2 = do
let s = (0,100)
x <- randomRIO s
return $ Game x s p1 p2
run :: Game -> IO ()
run (Game x s0 p1 p2)
= do Nothing <- runMaybeT $ evalStateT game s0
return ()
where
game = mapM_ runPlayer $ cycle [p1, p2]
runPlayer (Player nam strat) = do
s#(lo,hi) <- get
liftIO . putStrLn $ "Current state = " ++ show s
guess <- liftIO $ strat s
put =<< case compare x guess of
LT -> return (lo, guess-1)
GT -> return (guess+1, hi)
EQ -> do
liftIO . putStrLn $ nam ++ " wins!"
mzero
main :: IO ()
main = run =<< newGame (humanPlayer "Human") (computerPlayer "Computer")
We could tear our hair, rend our clothes, and moan pitifully that we had to use liftIOs in a few places and that the monad transformer stack hurts our tummies, but it mostly looks like idiomatic Haskell code to me, except perhaps that using MaybeT to end a loop is a little ugly. (Like many Haskell programmers who should know better, I got swept up in the pleasing form of the minor expression mapM_ runPlayer $ cycle [p1, p2] and wrote a silly monad stack to accommodate it.)
I guess I find it a little disingenuous to argue that, on the one hand, in an effectful language like Rust we can effortlessly mix pure and impure code because the pure code is actually written in an impure language, but for some reason if we try to write the same code in Haskell, we are bound by God and Country to write our pure code outside the IO monad and then bemoan the fact that we can't do IO there and so our implementation choices are suddenly limited. Writing "pure" code in an impure context isn't a capital offense, in Haskell or any other language.
In particular, you can easily write an obviously pure player for this implementation:
purePlayer :: String -> Player
purePlayer nam = Player nam (return . pureStrategy)
where pureStrategy :: Range -> Int
pureStrategy (lo, hi) = (lo + hi) `div` 2
or, if you're a zealot, you could make the above code polymorphic in the base monad, and use IO for the human but Identity for the purePlayer. Ta da, your pure player is now truly pure, and you can tell all your dimwitted, effectful Rust friends that you're so much better than they are while boasting to your genius, pure Haskell friends that you just figured out why they invented UnliftIO.

Related

What is an efficient way of parsing a string of single-character commands, each optionally followed by an integer repeat count?

I am implementing a robot that takes orders like L (turn left), R (turn right) and M (move forward). These orders may be augmented with a quantifier like M3LMR2 (move 3 steps, turn left, move one step, turn face). This is the equivalent of MMMLMRR.
I coded the robot structure that can understand the following enum:
pub enum Message {
TurnLeft(i8),
TurnRight(i8),
MoveForward(i8),
}
Robot::execute(&mut self, orders: Vec<Message>) is doing its job correctly.
Now, I am struggling to write something decent for the string parsing, juggling with &str, String, char and unsafe slicings because tokens can be 1 or more characters.
I have tried regular expression matching (almost worked), but I really want to tokenize the string:
fn capture(orders: &String, start: &usize, end: &usize) -> Message {
unsafe {
let order = orders.get_unchecked(start..end);
// …
};
Message::TurnLeft(1) // temporary
}
pub fn parse_orders(orders: String) -> Result<Vec<Message>, String> {
let mut messages = vec![];
let mut start: usize = 0;
let mut end: usize = 0;
while end < orders.len() && end != start {
end += 1;
match orders.get(end) {
Some('0'...'9') => continue,
_ => {
messages.push(capture(&orders, &start, &end));
start = end;
}
}
}
Ok(messages)
}
This doesn't compile and is clumsy.
The idea is to write a parser that turn the order string into a vector of Message:
let messages = parse_order("M3LMR2");
println!("Messages => {:?}", messages);
// would print
// [Message::MoveForward(3), Message::TurnLeft(1), Message::MoveForward(1), Message::TurnRight(2)]
What would be the efficient/elegant way for doing that?
You can do this very simply with an iterator, using parse and some basic String processing:
#[derive(Debug, PartialEq, Clone)]
enum Message {
TurnLeft(u8),
TurnRight(u8),
MoveForward(u8),
}
struct RobotOrders(String);
impl RobotOrders {
fn new(source: impl Into<String>) -> Self {
RobotOrders(source.into())
}
}
impl Iterator for RobotOrders {
type Item = Message;
fn next(&mut self) -> Option<Message> {
self.0.chars().next()?;
let order = self.0.remove(0);
let n_digits = self.0.chars().take_while(char::is_ascii_digit).count();
let mut number = self.0.clone();
self.0 = number.split_off(n_digits);
let number = number.parse().unwrap_or(1);
Some(match order {
'L' => Message::TurnLeft(number),
'R' => Message::TurnRight(number),
'M' => Message::MoveForward(number),
_ => unimplemented!(),
})
}
}
fn main() {
use Message::*;
let orders = RobotOrders::new("M3LMR2");
let should_be = [MoveForward(3), TurnLeft(1), MoveForward(1), TurnRight(2)];
assert!(orders.eq(should_be.iter().cloned()));
}

Is it possible to compose a chain of functions at runtime?

I have 3 functions:
fn f1() -> u64 {
println!("Hello world: 1");
2
}
fn f2(i: u64) -> Box<FnMut()> {
println!("Hello world: {}", i);
Box::new(|| println!("Hello world: {}", 3))
}
fn f3(mut f: Box<FnMut()>) {
f()
}
One functional technique is chaining — connecting the output of function A to the input of function B:
fn main() {
f3(f2(f1()));
}
This may help in Rust because this method is purely functional and the functions can be pure functions; they don't touch global variables, only work with its arguments which were moved (which is awesome).
How can I do this chain at runtime? If I have function f4 which accepts the input of f2 but does not use it as f3. We can also use it for further chaining by adding a return type to it:
fn f4(_: Box<FnMut()>) -> bool {
println!("Hello world: 4");
true
}
fn main() {
f4(f2(f1())) // returns f4's result (true)
}
I want to be able to decide how to chain my functions at runtime. The example would be this Lua code (sorry for this):
function f1()
print("Hello world: 1")
return 2
end
function f2(args)
print("Hello world: " .. args)
return function()
print("Hello world: " .. args + 1)
end
end
function f3(args)
args()
end
function f4()
print("Hello world: 4")
end
function run_chain(list)
local args
for _, v in ipairs(list) do
args = v(args)
end
end
local list = {}
list[#list + 1] = f1
list[#list + 1] = f2
list[#list + 1] = f3
run_chain(list)
list[#list] = f4
run_chain(list)
This is a big plus of dynamic typing of scripting languages, but as far as I know Rust alleges that it is much more functional than C++ for example. Is it possible to chain the functions in such a way?
Here's how you can do the simple chaining issue. Converting it from free functions to a builder or operator style is left as an exercise. It also uses the "impl Trait" feature introduced in Rust 1.26 to make it nicer.
fn f1(_: ()) -> u64 {
println!("Hello world: 1");
2
}
fn f2(i: u64) -> Box<FnMut()> {
println!("Hello world: {}", i);
Box::new(|| println!("Hello world: {}", 3))
}
fn f3(mut f: Box<FnMut()>) {
f()
}
fn f4(_: Box<FnMut()>) -> bool {
println!("Hello world: 4");
true
}
fn dot<I, X, O, F1, F2>(mut f1: F1, mut f2: F2) -> impl FnMut(I) -> O
where
F1: FnMut(I) -> X,
F2: FnMut(X) -> O,
{
move |i| f2(f1(i))
}
fn main() {
let mut c = dot(dot(f1, f2), f3);
c(());
let mut c2 = dot(dot(f1, f2), f4);
c2(());
}
Playground
Gluing two functions together is not very hard, but you may run into lifetime issues if your types are more complex. In particular, if the input parameter to a function in the chain is a reference to the type that the previous function returns, this code will not compile. I believe that some more parameters and generic bounds can solve this issue, but you would have to experiment a bit.
See also the tool crate (compose is pretty much what I just posted) and the rustz crate, both of which add more functional idioms to Rust.
Edit: This answer is for the question as I originally understood it. The chaining comment at the end makes this answer not ideally; that's a different beast. The answer to that is, yes, it's possible, but like any metaprogramming not easy.
Not well, but this has nothing to do with being functional or not. It's about typing.
You can do this in Rust:
struct Chain {
f1: Box<FnMut() -> u64>,
f2: Box<FnMut(u64) -> Box<FnMut()>>,
f3: Box<FnMut(Box<FnMut()>)>,
}
impl Chain {
fn run(&self) {
f3(f2(f1()));
}
}
fn f1() -> u64 {
println!("Hello world: 1");
2
}
fn f2(i: u64) -> Box<FnMut()> {
println!("Hello world: {}", i);
Box::new(|| println!("Hello world: {}", 3))
}
fn f3(mut f: Box<FnMut()>) {
f()
}
fn main() {
let chain = Chain {
f1: Box::new(f1),
f2: Box::new(f2),
f3: Box::new(f3),
};
chain.run();
}
But you can't append arbitrary functions to this chain, nor can you substitute f4 for f3:
error[E0271]: type mismatch resolving `<fn(std::boxed::Box<std::ops::FnMut() + 'static>) -> bool {f4} as std::ops::FnOnce<(std::boxed::Box<std::ops::FnMut() + 'static>,)>>::Output == ()`
--> src/main.rs:36:13
|
36 | f3: Box::new(f4),
| ^^^^^^^^^^^^ expected bool, found ()
|
= note: expected type `bool`
found type `()`
= note: required for the cast to the object type `std::ops::FnMut(std::boxed::Box<std::ops::FnMut() + 'static>)`
Because Rust is strictly typed, the functions in the chain have to be of known types, and those types have to fit together.
That said, anything a dynamic language can do, Rust can emulate if you just implement enough of the dynamic typing machinery yourself. You can make a struct that contains a Vec<Box<FnMut(&Any) -> Any>> and an add_func generic method that takes some function and adds a wrapper that does the necessary unwrapping, checking and rewrapping to the Vec. The run method then calls these functions in order.
I have actually done this I think. This code may need some review, but I think everything can be implement in the following way:
Define the functions you wanna call:
fn f1() -> u64 {
println!("Hello world: 1");
2
}
fn f2(i: u64) -> Box<FnMut()> {
println!("Hello world: {}", i);
Box::new(|| println!("Hello world: {}", 3))
}
fn f3(mut f: Box<FnMut()>) {
f()
}
fn f4(_: Box<FnMut()>) -> bool {
println!("Hello world: 4");
true
}
Take the advantage of having Any type in Rust type system as same as closures:
use std::any::Any;
struct Bridge<'a> {
function: &'a mut FnMut(Box<Any>) -> Box<Any>,
}
The closures can then be used for type erasure of called functions. However, we would still need some work in function checking arguments:
fn run_chain(chain: &mut [Bridge]) -> Box<Any> {
if chain.is_empty() {
return Box::new(false)
}
let mut args;
{
let function = &mut chain.first_mut().unwrap().function;
args = function(Box::new(0));
}
for c in chain.iter_mut().skip(1) {
let res = (c.function)(args);
args = res;
}
args
}
fn main() {
let mut f1 = |_: Box<Any>| { let res = f1(); Box::new(res) as Box<Any> };
let mut f2 = |args: Box<Any>| { let res = f2(*args.downcast::<u64>().unwrap()); Box::new(res) as Box<Any> };
let mut f3 = |args: Box<Any>| { let res = f3(*args.downcast::<Box<FnMut()>>().unwrap()); Box::new(res) as Box<Any> };
let mut f4 = |args: Box<Any>| { let res = f4(*args.downcast::<Box<FnMut()>>().unwrap()); Box::new(res) as Box<Any> };
let mut fns: Vec<Bridge> = Vec::new();
fns.push(Bridge { function: &mut f1 });
fns.push(Bridge { function: &mut f2 });
fns.push(Bridge { function: &mut f3 });
let _ = run_chain(&mut fns);
fns.pop();
fns.push(Bridge { function: &mut f4 });
let res = run_chain(&mut fns);
println!("Result: {:?}", res.downcast::<bool>().unwrap());
}
So basically all we do is writing a closure wrapper with the same interface. The type checking can be done right inside the closure before passing the argument further and it can be checked so that it will not lead to a crash.

How to express integers other than zero and one in generic code using the num crate?

The num crate in Rust provides a way of representing zeros and ones via T::zero() and T::one(). Is there a way of representing other integers, such as two, three, etc.?
Consider the following (artificial) example:
extern crate num;
trait IsTwo {
fn is_two(self) -> bool;
}
impl<T: num::Integer> IsTwo for T {
fn is_two(self) -> bool {
self == (T::one() + T::one())
}
}
Is there a better way of representing T::one() + T::one() as 2?
One way of representing arbitrary integers in generic code is to use the num::NumCast trait:
impl<T: num::Integer + num::NumCast> IsTwo for T {
fn is_two(self) -> bool {
self == T::from(2).unwrap()
}
}
A related way is to use the num::FromPrimitive trait:
impl<T: num::Integer + num::FromPrimitive> IsTwo for T {
fn is_two(self) -> bool {
self == T::from_i32(2).unwrap()
}
}
Related questions and answers: [1, 2].
You can write a function:
fn two<T>() -> T
where T: num::Integer,
{
let mut v = T::zero();
for _ in 0..2 {
v = v + T::one();
}
v
}
I've chosen this form because it's easily made into a macro, which can be reused for any set of values:
num_constant!(two, 2);
num_constant!(forty_two, 42);
I hear the concerns now... "but that's a loop and inefficient!". That's what optimizing compilers are for. Here's the LLVM IR for two when compiled in release mode:
; Function Attrs: noinline readnone uwtable
define internal fastcc i32 #_ZN10playground3two17hbef99995c3606e93E() unnamed_addr #3 personality i32 (i32, i32, i64, %"unwind::libunwind::_Unwind_Exception"*, %"unwind::libunwind::_Unwind_Context"*)* #rust_eh_personality {
bb3:
br label %bb8
bb8: ; preds = %bb3
ret i32 2
}
That's right - it's been optimized to the value 2. No loops.
It's relatively simple to forge any number from 0 and 1:
you need to create 2, which is hardly difficult
you then proceed in converting your number to base 2, which takes O(log2(N)) operations
The algorithm is dead simple:
fn convert<T: Integer>(n: usize) -> T {
let two = T::one() + T::one();
let mut n = n;
let mut acc = T::one();
let mut result = T::zero();
while n > 0 {
if n % 2 != 0 {
result += acc;
}
acc *= two;
n /= 2;
}
result
}
And will be efficient both in Debug (O(log2(N)) iterations) and Release (the compiler optimizes it out completely).
For those who wish to see it in action, here on the playground we can see that convert::<i32>(12345) is optimized to 12345 as expected.
As an exercise to the reader, implement a generic version of convert which takes any Integer parameter, there's not much operations required on n after all.

A pattern for finite (game) state machine in Rust with changing behavior?

I'm trying to write a turn-based game in Rust and I'm running up against a wall in the language (unless I'm not understanding something quite right – I'm new to the language). Basically, I'd like to change states in my game where each state has different behavior. For example I have something like:
struct Game {
state: [ Some GameState implementer ],
}
impl Game {
fn handle(&mut self, event: Event) {
let new_state = self.handle(event);
self.state = new_state;
}
}
struct ChooseAttackerPhase {
// ...
}
struct ResolveAttacks {
// ...
}
impl ResolveAttacks {
fn resolve(&self) {
// does some stuff
}
}
trait GameState {
fn handle(&self, event: Event) -> [ A New GateState implementer ]
}
impl GameState for ChooseAttackerPhase {
fn handle(&self, event: Event) -> [ A New GameState implementer ] {
// ...
}
}
impl GameState for ResolveAttacks {
fn handle(&self, event: Event) -> [ A New GameState implementer ] {
// ...
}
}
This was my original plan. I want handle to be a pure function that returns a new GameState instance. But as I understand it, this is not currently possible in Rust. So I tried using enums with tuples, each with their respective handler, that ended up being a dead end since I would have to match for every state.
Anyways, the code is not from my original project. Its just an example. My question is: is there a pattern for doing this in Rust that I'm missing? I'd like to be able to separate the logic for things I need to do in each state that are unique to each state and avoid writing lengthy pattern matching statements.
Let me know if I need to clarify my question a bit more.
A finite state machine (FSM) can be directly modeled using two enums, one representing all the states and another representing all the transitions:
#[derive(Debug)]
enum Event {
Coin,
Push,
}
#[derive(Debug)]
enum Turnstyle {
Locked,
Unlocked,
}
impl Turnstyle {
fn next(self, event: Event) -> Turnstyle {
use Event::*;
use Turnstyle::*;
match self {
Locked => {
match event {
Coin => Unlocked,
_ => self,
}
},
Unlocked => {
match event {
Push => Locked,
_ => self,
}
}
}
}
}
fn main() {
let t = Turnstyle::Locked;
let t = t.next(Event::Push);
println!("{:?}", t);
let t = t.next(Event::Coin);
println!("{:?}", t);
let t = t.next(Event::Coin);
println!("{:?}", t);
let t = t.next(Event::Push);
println!("{:?}", t);
}
The biggest downside is that one method ends up becoming very cluttered with all the state / transition pairs. You can sometimes neaten up the match a bit by matching on the pairs:
match (self, event) {
(Locked, Coin) => Unlocked,
(Unlocked, Push) => Locked,
(prev, _) => prev,
}
avoid writing lengthy pattern matching statements.
Each match arm can be a function that you call for every unique action you'd like to do. Above, Unlocked could be replaced with a function called unlocked that does whatever it needs to.
using enums [...] ended up being a dead end since I would have to match for every state.
Note that you can use the _ to match any pattern.
A downside to the enum is that it is not open for other people to add to it. Maybe you'd like to have an extensible system for your game where mods can add new concepts. In that case, you can use traits:
#[derive(Debug)]
enum Event {
Damage,
Healing,
Poison,
Esuna,
}
#[derive(Debug)]
struct Player {
state: Box<PlayerState>,
}
impl Player {
fn handle(&mut self, event: Event) {
let new_state = self.state.handle(event);
self.state = new_state;
}
}
trait PlayerState: std::fmt::Debug {
fn handle(&self, event: Event) -> Box<PlayerState>;
}
#[derive(Debug)]
struct Healthy;
#[derive(Debug)]
struct Poisoned;
impl PlayerState for Healthy {
fn handle(&self, event: Event) -> Box<PlayerState> {
match event {
Event::Poison => Box::new(Poisoned),
_ => Box::new(Healthy),
}
}
}
impl PlayerState for Poisoned {
fn handle(&self, event: Event) -> Box<PlayerState> {
match event {
Event::Esuna => Box::new(Healthy),
_ => Box::new(Poisoned),
}
}
}
fn main() {
let mut player = Player { state: Box::new(Healthy) };
println!("{:?}", player);
player.handle(Event::Damage);
println!("{:?}", player);
player.handle(Event::Healing);
println!("{:?}", player);
player.handle(Event::Poison);
println!("{:?}", player);
player.handle(Event::Esuna);
println!("{:?}", player);
}
Now, you can implement whatever states you'd like.
I want handle to be a pure function that returns a new GameState instance.
You cannot return a GameState instance because the compiler needs to know how much space each value requires. If you could return a struct that took up 4 bytes in one call or 8 bytes from another, the compiler wouldn't have any idea how much space the call you actually make needs.
The trade-off you have to make is to always return a newly allocated trait object. This allocation is required to give a homogenous size to every possible variant of PlayerState that might arise.
In the future, there might be support for saying that a function returns a trait (fn things() -> impl Iterator for example). This is basically hiding the fact that there is a value with a known size that the programmer doesn't / cannot write. If I understand correctly, it would not help in this case because the ambiguity of size would not be determinable at compile time.
In the extremely rare case that your states don't have any actual state, you could create a shared, immutable, global instance of each state:
trait PlayerState: std::fmt::Debug {
fn handle(&self, event: Event) -> &'static PlayerState;
}
static HEALTHY: Healthy = Healthy;
static POISONED: Poisoned = Poisoned;
impl PlayerState for Healthy {
fn handle(&self, event: Event) -> &'static PlayerState {
match event {
Event::Poison => &POISONED,
_ => &HEALTHY,
}
}
}
impl PlayerState for Poisoned {
fn handle(&self, event: Event) -> &'static PlayerState {
match event {
Event::Esuna => &HEALTHY,
_ => &POISONED,
}
}
}
This will avoid the overhead (whatever it may be) of the allocation. I wouldn't try this until you know there's no state and there's lots of time spent in the allocation.
I'm experimenting with encoding the FSM into the type model. This requires each state and each event to have it's type but I guess it's just bytes underneath and the explicit types allow me to break the transitions apart. Here's a playground with a tourniquet example.
We start with the simplest assumptions. Machine is represented by it's states and transitions. An event transits the machine in one step to a new state, consuming old state. This allows for the machine to be encoded in immutable state and event structs. States implement this generic Machine trait to add transitions:
pub trait Machine<TEvent> {
type State;
fn step(self, event: TEvent) -> Self::State;
}
That's all the framework for this pattern really. The rest is application and implementation. You cannot make a transition that is not defined and there's no unpredictable state. It looks very readable. For instance:
enum State {
Go(Open),
Wait(Locked),
}
struct Locked {
price: u8,
credit: u8,
}
struct Open {
price: u8,
credit: u8,
}
struct Coin {
value: u8,
}
impl Machine<Coin> for Locked {
type State = State;
fn step(self, coin: Coin) -> Self::State {
let credit = self.credit + coin.value;
if credit >= self.price {
println!("Thanks, you've got enough: {}", credit);
State::Go(Open {
credit: credit,
price: self.price,
})
} else {
println!("Thanks, {} is still missing", self.price - credit);
State::Wait(Locked {
credit: credit,
price: self.price,
})
}
}
}
And the client code is pretty semantic, too:
let locked = Locked {
price: 25,
credit: 0,
};
match locked.step(Coin { value: 5 }) {
State::Go(open) => {println!("Weeeeeeeeeeeeee!");},
State::Wait(locked) => {panic!("Oooops");},
}
I was much inspired by Andrew Hobben's Pretty State Machine Pattern.
There is a (synthetic) example of Music Player implementation via the State design pattern in Rust here: https://github.com/fadeevab/design-patterns-rust/tree/main/behavioral/state
The State design pattern is described in detail in The Rust Book: https://doc.rust-lang.org/book/ch17-03-oo-design-patterns.html
The State pattern actually does what you need: changing states [in the game] where each state has different behavior (read, "different implementation of the trait State").
You'd need to define trait State as follows:
pub trait State {
fn event1(self: Box<Self>, game: &mut Game) -> Box<dyn State>;
fn event2(self: Box<Self>, game: &mut Game) -> Box<dyn State>;
}
Then you define a behavior of the each state:
struct AttackState;
impl State for AttackState{
fn event1(self: Box<Self>, game: &mut Game) -> Box<dyn State> {
game.do_one_thing();
// Transition to another state: Attack -> Resolve
Box::new(ResolveState)
}
}
struct ResolveState;
impl State for ResolveState {
fn event1(self: Box<Self>, game: &mut Game) -> Box<dyn State> {
game.do_another_thing();
// No state transition
self
}
}
Each state implements different actions over the game object.

Is there an easy way to cast entire tuples of scalar values at once?

I want to cast a (u16, u16) to a (f32, f32). This is what I tried:
let tuple1 = (5u16, 8u16);
let tuple2 = tuple1 as (f32, f32);
Ideally, I would like to avoid writing
let tuple2 = (tuple1.0 as f32, tuple1.1 as f32);
There's no built-in way to do this, but one can do it with a macro:
macro_rules! tuple_as {
($t: expr, ($($ty: ident),*)) => {
{
let ($($ty,)*) = $t;
($($ty as $ty,)*)
}
}
}
fn main() {
let t: (u8, char, isize) = (97, 'a', -1);
let other = tuple_as!(t, (char, i32, i8));
println!("{:?}", other);
}
Prints ('a', 97, -1).
The macro only works for casting between types with names that are a single identifier (that's what the : ident refers to), since it reuses those names for binding to the elements of the source tuple to be able to cast them. All primitive types are valid single identifiers, so it works well for those.
No, you cannot. This is roughly equivalent to "can I cast all the fields in a struct to different types all at once?".
You can write a generic extension trait which can do this conversion for you, the only problem is that I don't believe there's any existing generic "conversion" trait which also has a u16 -> f32 implementation defined.
If you really want a function that does this, here is an as-minimal-as-I-could-make-it skeleton you can build on:
trait TupleCast<T> {
type Output;
fn tuple_cast(self) -> <Self as TupleCast<T>>::Output;
}
impl<T> TupleCast<T> for () {
type Output = ();
fn tuple_cast(self) -> <() as TupleCast<T>>::Output {
()
}
}
impl<S, T> TupleCast<T> for (S,) where S: CustomAs<T> {
type Output = (T,);
fn tuple_cast(self) -> <(S,) as TupleCast<T>>::Output {
(self.0.custom_as(),)
}
}
impl<S, T> TupleCast<T> for (S, S) where S: CustomAs<T> {
type Output = (T, T);
fn tuple_cast(self) -> <(S, S) as TupleCast<T>>::Output {
(self.0.custom_as(), self.1.custom_as())
}
}
// You would probably have more impls, up to some size limit.
// We can't use std::convert::From, because it isn't defined for the same
// basic types as the `as` operator is... which kinda sucks. So, we have
// to implement the desired conversions ourselves.
//
// Since this would be hideously tedious, we can use a macro to speed things
// up a little.
trait CustomAs<T> {
fn custom_as(self) -> T;
}
macro_rules! custom_as_impl {
($src:ty:) => {};
($src:ty: $dst:ty) => {
impl CustomAs<$dst> for $src {
fn custom_as(self) -> $dst {
self as $dst
}
}
};
($src:ty: $dst:ty, $($rest:ty),*) => {
custom_as_impl! { $src: $dst }
custom_as_impl! { $src: $($rest),* }
};
}
// You could obviously list others, or do manual impls.
custom_as_impl! { u16: u16, u32, u64, i32, i64, f32, f64 }
fn main() {
let x: (u16, u16) = (1, 2);
let y: (f32, f32) = x.tuple_cast();
println!("{:?}", y);
}
No,
there
is
not.
this version handles a few more cases Playground Example
original source: https://stackoverflow.com/a/29981602/5979634
because of matching rules, for single type casts just use as_tuple!(expr, T) or as_tuple!(expr, (T))
the rest works as in the original answer
macro_rules! tuple_as {
($t: expr, $ty: ident) => {{
let (a, b) = $t;
let a = a as $ty;
let b = b as $ty;
(a, b)
}};
($t: expr, ($ty: ident)) => {{
let (a, b) = $t;
let a = a as $ty;
let b = b as $ty;
(a, b)
}};
($t: expr, ($($ty: ident),*)) => {{
let ($($ty,)*) = $t;
($($ty as $ty,)*)
}}}

Resources