[我正在尝试学习有关如何以纯函数式语言(Haskell)构造程序的最佳实践,但是我遇到了一种架构问题,虽然我觉得这种架构很自然,但却无法在“纯净世界”中轻易复制。] >
让我描述一个简单的模型案例:一个两人猜谜游戏。
游戏规则
- 生成[0,100]中的秘密随机整数。
玩家轮流尝试猜测密码。 如果猜测正确,则玩家获胜。 否则,游戏会告诉您机密号是大于还是小于猜测值,并且更新了未知机密的可能范围。 我的问题与该游戏的实现有关,在该游戏中,人类玩家与计算机对战。
我提出了两个实现:
在逻辑驱动的实现中,执行是由游戏逻辑驱动的。 Game
有一个State
和一些参与的Player
。 Game::run
功能使玩家轮流玩游戏并更新游戏状态,直到完成为止。玩家在Player::play
中收到状态,并返回他们决定打的棋。
我可以看到的这种方法的好处是:
Player
的性质:HumanPlayer
和ComputerPlayer
是可互换的,即使前者必须处理IO,而后者则代表纯计算(您可以通过将[C0 ]在Game::new(ComputerPlayer::new("CPU-1"), ComputerPlayer::new("CPU-2"))
功能中并观看计算机战斗)。这是Rust中可能的实现:
main
互动驱动
在交互驱动的实现中,与游戏相关的所有功能,包括计算机玩家做出的决定,必须是无副作用的纯功能。然后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();
}
成为界面,坐在电脑前的真实人与游戏进行交互。从某种意义上说,游戏成为将用户输入映射到更新状态的功能。
在我看来,这种方法似乎是纯语言所强制的,因为游戏的所有逻辑都变成了纯计算,没有副作用,只是将旧状态转换为新状态。
我也很喜欢这种观点(分离HumanPlayer
):它肯定有一些优点,但是我觉得,如在此示例中可以轻易看出的那样,它破坏了程序的其他一些良好特性,例如人类玩家和计算机玩家之间的对称性。从游戏逻辑的角度来看,下一步决定是否来自计算机执行的纯计算还是涉及IO的用户交互都无关紧要。
我再次在Rust中提供一个参考实现:
input -> (state transformation) -> output
结论
[一种有效的语言(例如Rust)使我们可以根据优先级灵活地选择方法:人机交互或计算机参与者之间的对称性,或者纯计算(状态转换)与IO(用户交互)之间的明显分离。
考虑到我目前对Haskell的了解,我不能对纯净的世界说同样的话:我被迫采用第二种方法,因为第一种方法到处都是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();
}
。特别是,我希望听到一些函数式编程专家的话,以谈论如何在Haskell中实施逻辑驱动的方法,以及他们对此主题有何看法/意见。
我准备从中学到很多见识。
[我正在尝试学习有关如何以纯函数式语言(Haskell)构造程序的最佳实践,但是我遇到了一种架构,该架构我觉得很自然,但是无法轻松复制...