函数式编程优点_函数式编程的优点

本文探讨了函数式编程在国际象棋AI和游戏开发中的应用,对比了Objective-C和Elm的性能。通过举例展示了Elm如何通过不变状态和递归实现高效代码,强调了函数式编程在保持代码整洁、避免副作用和提高可读性方面的优势。
摘要由CSDN通过智能技术生成

函数式编程优点

本系列的第一部分中 ,我讨论了函数式编程的核心概念,并给出了一些例子说明它们如何发挥作用。 函数编程的核心概念列表(再次从第一部分开始)如下:

  1. 使用功能作为其他功能的输入和输出,
    高阶函数
  2. 使用mapfilterreduce类型函数而不是循环
  3. 不变的状态
  4. 递归代替循环
  5. 从其他功能组合功能
  6. 将“纯”功能与具有副作用的功能区分开

我给出了使用前四个项目的一些示例,因此让我们更深入地研究这些项目,并添加第5个项目。

与Elm建立高效的国际象棋AI

但是首先,有关国际象棋编程的一些信息:

为类似象棋的游戏编写AI是一件很有趣的事情。 真正的国际象棋AI必须非常复杂。 电脑下象棋的方式比人类下得多。 他们尝试查看每个可能的移动顺序,并为结果位置分配一个值。 这种蛮力的方法需要大量的工作,以至于如果不进行重大的优化,AI将需要几天的时间才能移动,并且还需要大量的内存。

优化国际象棋AI代码的挑战已经产生了一些很棒的算法,例如alpha-beta修剪和min-max搜索。 击败世界冠军Garry Kasparov的AI Deep Blue内置了专门的“象棋”芯片,使其速度比纯软件解决方案快了许多倍。 此外,国际象棋公开赛百科全书包含了所有流行公开赛的最佳棋步,并且这些棋子作为每个游戏的前十到二十步不需要AI进行工作的数据,都被硬编码到国际象棋程序中。

相比之下,我的宠物项目Wounds(请参阅本系列的第一部分 )的AI非常原始。 在评估董事会中的给定职位时,它将比较军队(包括受伤的部分)的实力,并将其各自的机动性与每个团队可以进行多少法律行动的原始计数进行比较。

我们将看到在不耗尽某些资源(包括人的耐心)的情况下,我们可以展望未来的许多举措。

为简单起见,让我们假设在每个连续回合中合法移动的次数保持不变(不是;它通常随着游戏第一部分中的每个移动而增加)。 第一位在Wounds中是Red的玩家在游戏开始时进行了大约30次合法移动。 AI会尝试将每个动作作为第1层,然后使Blue团队的每个响应(第2层)为30乘以30,即900。您将看到结果。 接下来将考虑Red的27,000次举动(第3页)和Blue的810,000次举动(第4页)。 当您开始看远一点时,很容易耗尽时间和内存。

比较Objective-C和Elm

当我在Objective-C中为此编写代码时,在遇到函数编程之前,我看到递归将使我能够为此编写干净的代码。 当我实现递归代码时,我发现深度为5层时,代码将使我的iPad崩溃。 这样做的原因有两个:一,在董事会上进行下一步行动之前,需要保存每个董事会职位的副本;二,未针对递归对Objective-C进行优化,因此会产生额外的开销。 递归调用(没有优化)会占用堆栈空间,因此通常是最弱的链接。

让我们希望这在Elm中效果更好。 看看这个:

makeAIMove : Player -> Board -> Board
makeAIMove player board =
  let
    moveList = getAIMoves board player
    scoredMobility = List.map (\move -> scoreMoveMobility board player move) moveList
    scoredMoves = 
    List.map (\move -> scoreMoveMaterial board player move) scoredMobility
    sortedMoves = List.sortBy .score scoredMoves |> List.reverse 
    maybeMove = List.head sortedMoves
  in
    case maybeMove of
      Just maybeMove ->
        let _ = Debug.log "score " maybeMove.score
        in
          makeMove board maybeMove
      _ ->
        board

在这里,我们看到了对List.map的几个调用,它使用一个函数作为输入。 如果需要,传入的函数还会获取参数。 最后,列表通过。 该函数将在列表中的每个项目上调用。 List.map将返回结果列表。 在这种情况下,我要传递动作列表。 首先是所有合法举动,然后是那些举动的得分,然后加上材料的得分,然后进行排序,这样我们就可以抓住得分最高的列表的头。

请注意,每个后续调用的结果如何放入其自己的变量中,该变量将被传递给下一个函数。 这是我的下一个尝试,其中我利用链接消除了这些临时变量:

makeAIMove : Player -> Board -> Board
makeAIMove player board =
  let
    maybeMove =
    getAIMoves board player
    |> List.map (\move -> scoreMoveMobility board player move)
    |> List.map (\move -> scoreMoveMaterial board player move)
    |> List.sortBy .score
    |> List.reverse
    |> List.head
  in
    case maybeMove of
      Just maybeMove ->
        makeMove board maybeMove
      _ ->
        board

在Elm中, |>运算符将其前面的函数的输出作为最后一个参数传递给其后面的函数。 如果比较上面的代码段,您将看到第二个代码段中每行末尾的参数丢失。 在语言之间,该参数传递顺序未标准化。

例如,Elixir中的链接函数将输出参数作为第一个输入参数发送给后续函数,而不是最后一个参数。 这看起来类似于使用用于连接UNIX命令行实用程序的管道运算符。 但是可以这么说,还有一些更强大的事情正在发生。

部分应用的功能

当我说一个功能的输出传递给下一个功能时,我撒了点谎。 实际传递的称为部分应用函数 。 将这些信息传递出去的行为称为currying

Currying是为了向组合逻辑的先驱Haskell Brooks Curry致敬。 动词“咖喱”不仅指代他和他的作品,而且还以他的名字命名了三种函数式编程语言:Haskell,Brooks和Curry。 在这三者中,只有Haskell有大量追随者。

像这样传递部分应用的函数是命名函数编程的另一个原因。 这个名字恰当的另一个原因是函数式程序员对所谓的副作用非常谨慎。

在面向对象的编程中,对象包含内部状态及其方法既可以读取状态又可以写入状态是很平常的事。 代码和数据交织在一起。 如果没有精心设计,可能会出现各种难以预料的副作用。

函数式程序员喜欢对这些副作用要非常小心。 给定相同的输入,“纯”功能始终返回相同的输出。 此外,它不会影响应用程序状态。 由于很少有没有状态的应用程序可以工作,因此需要以某种方式解决。 因此,仍然存在状态,但是必须非常谨慎和认真地处理它。

例如,在React / Redux中,您可以通过编码称为“ reducers”的Redux函数来处理状态。 然后创建“动作”。 当Redux收到一个动作时,它会小心地应用任何中间件,然后调用所有的reducer。 减速器可以对自己感兴趣的动作采取行动,或者可以说是推卸责任。 React / Redux并不是纯功能性的工具链,但是作者说,它们已经受到Elm架构的严重影响。

不变状态的优雅

让我们再次讨论不可变状态,这一次更深入地介绍了在代码中使用它的优雅之处。 “纯”功能性编程工具强制执行不可变状态。 半功能性工具(如React(与Redux)一起使用)强烈建议并假设您将坚持不变的状态设计模式。 例如ReactJS,您可以调用

this.state = ‘new state’;

constructor()但是应该使用后续的状态更改

this.setState(‘new state’);

这似乎没有被强制执行,因此我可以想象自己会忘记此规则而引入错误。 有点像当我输入=而不是== 。 我敢打赌,这从未发生在你身上;)。

诸如Elixir和Elm之类的功能语言将返回数据的更新副本,而不是修改数据本身。 直观地讲,这在处理和内存方面似乎都很昂贵,但是实际上这些工具已经过优化,因此实际上效率更高而不是更低。

这是我(为简单起见而编辑)的Objective-C递归代码,用于搜索良好的AI动作:

- (Move *) pickBestMove: (Board *) board forPlayer: (Player *) player depth: (int) depth
{
    Move *bestMove = nil;
    NSMutableArray *moves = [self generateLegalMoves:board forPlayer:player];
    if(moves.count > 0)
    {
        bestMove = moves[0];
        BOOL alphaBeta = depth % 2 == 1;
        for(Move *move in moves)
        {
            move.score = 0;
            int interimValueScore = 0;
            if(move.defending_man != nil)
            {
                interimValueScore += move.defending_man.value;
            }
            if(self.searchDepth > depth)
            {
                if(move.defending_man != nil)
                {
                    if(depth < 4)
                    {
                        Board *tempBoard = [board copy]; 
                        [tempBoard makeMove:move];
                        Player *nextPlayer = [self getNextPlayer:player];
                        // here's the recursive call
                        Move *nextPlyBestMove = 
                [self pickBestMove:tempBoard forPlayer:nextPlayer 
                    depth: depth + 1];
                        if(nextPlyBestMove != nil)
                        {
                            interimValueScore += nextPlyBestMove.score;
                        }
                        [tempBoard unMakeMove:move];
                    }
                }
            }
            if(alphaBeta)
            {
                move.score += interimValueScore;
                if(move.score > bestMove.score)
                {
                    bestMove = move;
                }
            }
            else
            {
                move.score -= interimValueScore;
                if(move.score < bestMove.score)
                {
                    bestMove = move;
                }
            }
        }
    }
    return bestMove;
}

每次我搜索一个更深的层时,都会制作一个新的木板副本,然后在该木板的副本上进行每个动作,并为得到的位置打分。 该代码很难调试,需要各种仪器代码才能在特定深度和板上位置停止。 即使在我的iMac上,它也非常慢,这意味着它玩的不是很强大。

现在让我们回到Elm,它要求状态是不可变的。 我在Elm制作的makeAIMove第一个版本在为移动性评分时向前看了一个动作,而在给材质评分时却没有任何动作。 这非常快,但是不太聪明。 它打得非常激进,没有考虑对手的React。 这是一个更智能的版本,它使用递归来搜索由输入参数控制的深度:

makeAIMove : Player -> Int -> Board -> Board
makeAIMove player searchDepth board =
  let
    maybeMove = pickBestMove player 1 4 board
  in
    case maybeMove of
      Just maybeMove ->
        maybeMove
       |> makeMove board
      _ ->
        board

pickBestMove函数对其进行递归调用,直到达到由limit参数指定的搜索深度为止:

pickBestMove : Player -> Int -> Int -> Board -> Maybe Move
pickBestMove player depth limit board =
  let
    legalMoves = getAIMoves board player
    alphaBeta = (rem depth 2) /= 1

    setMoveScore move player depth limit board =
      if depth < limit then
        let
          nextBoard = makeMove board move
          nextPlayer = (Player.getNextPlayer player)
          -- here's the recursive call
          nextPlyBestMove =
            pickBestMove nextPlayer (depth + 1) limit nextBoard
        in
          case nextPlyBestMove of
            Just nextPlyBestMove ->
              if alphaBeta then
                {move | score = move.score + (scoreBoard nextPlayer nextBoard)}
              else
                {move | score = move.score - (scoreBoard nextPlayer nextBoard)}
            _ ->
              move
      else
        {move | score = makeMove board move |> scoreBoard player}
  in
    List.map (\move -> setMoveScore move player depth limit board) legalMoves
    |> List.sortBy .score
    |> List.reverse
    |> List.head

在顶部的let部分中,我定义了一个本地函数setMoveScore ,该函数setMoveScore legalMoves列表中的每个移动而legalMovessetMoveScore递归调用外部函数pickBestMove 。 本地/内部函数递归调用外部函数是一种有趣的模式。 可以将setMoveScore视为代码块而不是谨慎的功能。

模式{move | score = makeMove board move |> scoreBoard player} {move | score = makeMove board move |> scoreBoard player}显示了Elm如何响应状态变化。 此摘要返回更新了分数的新的move副本。 这与List.map函数调用非常匹配, List.map通过将函数应用于旧列表中的每个项目来创建新列表。

同样,函数调用nextBoard = makeMove board move返回位置更改的新板。 Player.getPlayer工作方式相同。 这非常有用,因为我们希望能够在此功能的最后一行中使用原始的boardplayer

我们不需要在每个层深度管理这些临时板,并确保我们撤消所做的移动。 这就是Objective-C示例的工作方式。 那在很大程度上取决于可变状态。

不变状态版本不易出错。 最终,它也变得更容易理解。 刚开始围绕FP概念进行研究时,我发现比面向对象的代码更难理解。 现在,我变得更加熟练了,我看到了这里的吸引力:一种令人上瘾的品质,对问题的正确解决方案具有优雅的感觉。 在那一刻,一切都点击了。

接下来

本系列的下一篇文章将深入研究React和Redux。

翻译自: https://www.javacodegeeks.com/2017/10/advantages-functional-programming.html

函数式编程优点

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值