首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >纯语言的软件体系结构(Haskell):逻辑驱动与交互驱动

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

Stack Overflow用户
提问于 2019-11-28 16:15:41
回答 1查看 162关注 0票数 1

我正在尝试学习如何用纯函数语言(Haskell)构建程序的最佳实践,但我在架构方面遇到了麻烦,我发现这种体系结构是自然的,但在“纯世界”中很难复制。

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

博弈规则

一个在0,100中的秘密随机整数是generated.

  • Players轮流试图猜出这个秘密数。如果猜测是正确的,那么玩家wins.

  • Otherwise游戏就会告诉这个秘密数是大还是小,并更新未知秘密的可能范围。

我的问题是关于这个游戏的实现,在这个游戏中,一个人类玩家在电脑上玩。

我提议两种实现:

driven

  • interaction驱动
  • 逻辑

逻辑驱动

在逻辑驱动的实现中,执行由博弈逻辑驱动.Game有一个State和一些参与的PlayerGame::run函数让玩家轮流玩,并更新游戏状态直到完成。玩家在Player::play中得到一个状态,并返回他们决定要玩的动作。

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

  1. 架构非常简洁和自然;
  2. 抽象了Player的本质:HumanPlayerComputerPlayer是可互换的,即使前者必须处理IO,而后者则代表纯粹的计算(您可以通过在main函数中放置Game::new(ComputerPlayer::new("CPU-1"), ComputerPlayer::new("CPU-2"))来验证这一点,并观察计算机大战)。

以下是在Rust中的一个可能的实现:

代码语言:javascript
复制
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变成了一个界面,坐在电脑前面的真人通过这个界面与游戏进行交互。在某种意义上,游戏变成了将用户输入映射到更新状态的函数。

在我看来,这种方法似乎是被一种纯粹的语言所迫,因为游戏的所有逻辑都变成了纯粹的计算,没有副作用,只是将旧的状态转化为新的状态。

我也有点喜欢这个观点(分离input -> (state transformation) -> output):它肯定有一些优点,但是我觉得,正如在这个例子中很容易看到的,它破坏了程序的其他一些优点,比如人类播放器和计算机播放器之间的对称性。从游戏逻辑的角度来看,下一步的决定是否来自计算机执行的纯计算,还是涉及IO的用户交互并不重要。

我在这里提供了一个参考实现,同样是在Rust中:

代码语言:javascript
复制
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();
}

结论

一种有效的语言(如Rust)为我们提供了根据优先级选择方法的灵活性:人和计算机参与者之间的对称性或纯计算(状态转换)和IO (用户交互)之间的尖锐分离。

鉴于我目前对Haskell的了解,我不能对纯粹的世界说同样的话:我感到不得不采用第二种方法,因为第一种方法到处都是IO。特别是,我想听听一些函数式编程大师关于如何在Haskell中实现逻辑驱动的方法,以及他们对这一主题的看法/评论。

我准备从中学到很多洞察力。

EN

回答 1

Stack Overflow用户

发布于 2019-11-28 22:57:35

我想Haskell相当于您的逻辑驱动实现(省略了错误处理)如下所示:

代码语言:javascript
复制
{-# 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")

我们可以撕下头发,撕裂衣服,可怜地抱怨说,我们不得不在几个地方使用liftIOs,单台变压器堆栈会伤害我们的肚子,但在我看来,它主要像是惯用的Haskell代码,但也许使用MaybeT结束循环有点难看。(和许多应该更了解的Haskell程序员一样,我使用了mapM_ runPlayer $ cycle [p1, p2]这一令人愉快的小表达式,并编写了一个愚蠢的monad堆栈来适应它。)

我想我觉得这样的说法有点不诚实,一方面,在一种有效的语言中,比如Rust,我们可以毫不费力地混合纯代码和不纯代码,因为纯代码实际上是用一种不纯的语言编写的,但出于某种原因,如果我们试图在Haskell中编写相同的代码,我们就必须受上帝和国家的约束,在IO monad之外编写我们的纯代码,然后哀叹我们不能在那里执行IO,因此我们的实现选择突然受到限制。用Haskell或任何其他语言,在不纯的上下文中编写“纯”代码并不是一种死罪。

特别是,您可以轻松地为该实现编写一个明显的纯播放器:

代码语言:javascript
复制
purePlayer :: String -> Player
purePlayer nam = Player nam (return . pureStrategy)
  where pureStrategy :: Range -> Int
        pureStrategy (lo, hi) = (lo + hi) `div` 2

或者,如果您是一个狂热者,您可以在基本的monad中使上面的代码多态,并将IO用于人工,而Identity用于purePlayer。塔达,你的纯玩家现在是真正的纯粹了,你可以告诉你所有愚蠢的,有效的铁锈朋友,你比他们更好的同时,吹嘘你的天才,纯粹的哈斯克尔朋友,你刚刚知道为什么他们发明了UnliftIO

票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/59092892

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档