纯语言的软件体系结构(Haskell):逻辑驱动与交互驱动

问题描述 投票:1回答:1

[我正在尝试学习有关如何以纯函数式语言(Haskell)构造程序的最佳实践,但是我遇到了一种架构问题,虽然我觉得这种架构很自然,但却无法在“纯净世界”中轻易复制。] >

让我描述一个简单的模型案例:一个两人猜谜游戏。

游戏规则
  1. 生成[0,100]中的秘密随机整数。

  • 玩家轮流尝试猜测密码。
  • 如果猜测正确,则玩家获胜。
  • 否则,游戏会告诉您机密号是大于还是小于猜测值,并且更新了未知机密的可能范围。
  • 我的问题与该游戏的实现有关,在该游戏中,人类玩家与计算机对战。

    我提出了两个实现:

    • 逻辑驱动
    • 互动驱动
    • 逻辑驱动

    在逻辑驱动的实现中,执行是由游戏逻辑驱动的。 Game有一个State和一些参与的PlayerGame::run功能使玩家轮流玩游戏并更新游戏状态,直到完成为止。玩家在Player::play中收到状态,并返回他们决定打的棋。

    我可以看到的这种方法的好处是:

    1. 建筑非常干净自然;
    2. 它抽象了Player的性质:HumanPlayerComputerPlayer是可互换的,即使前者必须处理IO,而后者则代表纯计算(您可以通过将[C0 ]在Game::new(ComputerPlayer::new("CPU-1"), ComputerPlayer::new("CPU-2"))功能中并观看计算机战斗)。
    3. 这是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)构造程序的最佳实践,但是我遇到了一种架构,该架构我觉得很自然,但是无法轻松复制...

    haskell io architecture software-design purely-functional
    1个回答
    0
    投票
    我想您的Logic-Driven实现的Haskell等效项(省略了错误处理)是这样的:
    © www.soinside.com 2019 - 2024. All rights reserved.