如何在 Haskell 中制作打勾游戏的图案?

2024-05-15

实现有 2 个参数的函数 ticktick。第一个参数是自然数元组,定义游戏场地的行数和列数。第二个列表包含由玩家“x”和玩家“o”轮流玩的坐标给出的井字游戏比赛的记录。打印游戏的实际状态,其中游戏区域将由字符“-”和“|”界定,空方块“ ”以及字符“x”和“o”将位于玩家玩过的方块上。

ticktack::(Int,Int) -> [(Int,Int)] -> Result


我已经尝试过这样的事情:

type Result = [String]
pp :: Result -> IO ()
pp x = putStr (concat (map (++"\n") x))

ticktack::(Int,Int) -> [(Int,Int)] -> Result
ticktack (0,0) (x:xs) = []
ticktack (a,b) [] = []
ticktack (a,b) (x:xs) =[ [if a == fst x && b == snd x then 'x' else ' ' | b <- [1..b]]| a <- [1..a]] ++ ticktack (a,b) xs 


但它只返回 N [String] 和 1 个结果,所以我需要将这些结果合并为一个 [String]。


正如@luqui 在对该问题的评论中所说:

您可以合并输出...或者您可以为每个空间搜索一次历史记录 董事会。 ...

之前的解决方案是在附近的问题中描述 https://stackoverflow.com/a/58685116. The "chess"问题已经 解决了,与你的只是表面上的不同“圈和叉”问题,所以应该 适应解决方案不要太难。然而:

  • 在这种情况下,电路板尺寸是固定的并且很小,所以我们不担心效率低下 成对合并板。

  • 在这种情况下,电路板尺寸是可变的,因此后一种方法的解决方案可能值得一试。

为了使算法更加高效,而不是在棋盘上滚动并搜索 匹配每个单元格的动作,我们将滚动动作并将值分配给棋盘 表示为可变数组。可变数组可以被认为是「先进技术」在 函数式编程,因此对于中级 Haskeller 来说这也是一个很好的练习。我只是 以前用过它们一两次,所以让我们看看我是否能解决这个问题!

这将如何运作?

该程序的核心是一个字节矩形数组。数组有两种类型: 可变的和"frozen"。虽然冻结数组无法更改,但可变数组是一条规则 可能只存在于单子上下文中,因此我们只能在数组被冻结时自由地传递它。 如果这看起来过于复杂,我只能请读者相信额外的 安全保证是值得的。

不管怎样,有以下几种类型:

type Position = (Int, Int)

type Field s = STUArray s Position Char

type FrozenField = UArray Position Char

我们将创建一个函数“适用”移动到数组的列表,将其解冻和冻结为 需要。

type Move = (Char, Position)

applyMoves :: FrozenField -> [Move] -> FrozenField

(这个想法Move是在板上做个标记就足够了,不需要知道 轮到谁了。)

应用于适当大小的空字段,该函数将解决我们的问题 - 我们将 只需要调整输入和输出的格式。

empty :: Position -> FrozenField

positionsToMoves :: [Position] -> [Move]

arrayToLists :: FrozenField -> [[Char]]

我们的最终程序将如下所示:

tictac :: Position -> [Position] -> IO ()
tictac corner = pp . arrayToLists . applyMoves (empty corner) . positionsToMoves

我希望它看起来很明智?尽管我们还没有编写任何有形的代码。

我们可以写代码吗?

Yes.

首先,我们需要一些进口。没有人喜欢进口,但由于某种原因,目前还没有 自动化。所以在这里:

import Data.Foldable (traverse_)
import Data.Array.Unboxed
import Data.Array.ST
import GHC.ST (ST)

使用数组可以做的最简单的事情就是创建一个空数组。让我们尝试一下:

empty :: Position -> FrozenField
empty corner = runSTUArray (newArray ((1, 1), corner) ' ')

这个想法是newArray声明内存中的一个区域并用空格填充它,并且runSTUArray冻结它,以便可以安全地将其传输到程序的另一部分。我们可以改为"inline"创建数组并赢得一些速度,但我们只需要做一次,我 希望保持它的可组合性——我认为这样程序会更简单。

另一件容易做的事情是写"glue"调整输入输出格式的代码:

positionsToMoves :: [Position] -> [Move]
positionsToMoves = zip (cycle ['x', 'o'])

arrayToLists :: FrozenField -> [[Char]]
arrayToLists u =
  let ((minHeight, minWidth), (maxHeight, maxWidth)) = bounds u
  in [ [ u ! (row, column) | column <- [minWidth.. maxWidth] ] | row <- [minHeight.. maxHeight] ]

这里没有什么不寻常的,普通的列表处理。

最后,最困难的部分 - 将任意数量的移动应用于给定冻结数组的代码:

applyMoves :: FrozenField -> [Move] -> FrozenField
applyMoves start xs = runSTUArray (foldST applyMove (thaw start) xs)
  where

    foldST :: (a -> b -> ST s ()) -> ST s a -> [b] -> ST s a
    foldST f start' moves = do
        u <- start'
        traverse_ (f u) moves
        return u

    applyMove :: Field s -> Move -> ST s ()
    applyMove u (x, i) = writeArray u i x

模式与函数中的模式相同empty:修改一个数组,然后冻结它——以及所有 修改必须发生在STmonad,为了安全。foldST包含所有的“至关重要的” “内循环”我们的计划。

(P.S.)这实际上是如何运作的?

让我们打开包装UArray and STUArray首先类型。它们是什么以及有什么区别?

UArray means “拆箱数组”,也就是说一个值数组,而不是一个数组 指针。我们例子中的值实际上是一个 Unicode 字符,而不是 C"byte" char,所以它不是一个字节,而是一个变量 大小实体。当它以未装箱的形式存储时,它会转换为Int32然后隐形地回来 对我们来说。一个Int32对于我们存储 3 个不同值的卑微目的来说当然太多了, 所以这里还有改进的空间。要了解有关未装箱值的更多信息,我邀请您 查看 1991 年介绍它们的文章,“作为一等公民的未装箱价值观 非严格的函数式语言” https://www.microsoft.com/en-us/research/wp-content/uploads/1991/01/unboxed-values.pdf.

这些值未装箱并不意味着您可以更改它们。 Haskell 中的纯值 总是不可变的。因此,如果您更改数组中的单个值,整个数组将是 抄袭——贵!这是哪里STUArray进来。ST代表State Thread,还有什么STUArray是 是 一个“解冻”数组,您可以在其中覆盖单个值而无需复制 整个东西。为了确保安全,它只能生活在一个 monad 中,在本例中是ST monad. (注意如何STUArray值永远不会出现在 a 之外ST s wrap.)你可以想象一个ST计算作为一个小的命令式进程,拥有自己的内存,与外部分离 世界。据说他们发明了ST首先,然后发现他们可以获得IO从 它,所以IO实际上是ST伪装的。有关如何操作的更多详细信息ST有效,查看 1994年的原创文章:“惰性功能状态线程” https://www.microsoft.com/en-us/research/wp-content/uploads/1994/06/lazy-functional-state-threads.pdf..

现在让我们更仔细地看看foldST。我们看到的是功能上, 它不是 合理。首先我们绑定的值start' to u,然后我们返回u- 相同 多变的。从功能的角度来看,这与写法是一样的:

u <- start'
return u

— 这相当于u由单子定律。诀窍在于中间发生的事情:

traverse_ (f u) moves

让我们检查一下类型。

λ :type traverse_
traverse_ :: (Foldable t, Applicative f) => (a -> f b) -> t a -> f ()

因此,正在调用某个函数u作为论证,但结果是无用的()类型。 在功能设置中,这条线毫无意义。但在 monad 中,绑定值可能appear改变。f是一个可以改变 monad 状态的函数,因此可以改变 返回时的绑定名称。 C 中的类似代码有点像这样:

char* foldST(void f(char*, Move), int n_start, char start[], int n_moves, Move moves[])
{
    // u <- start
    char* u = malloc(sizeof(char) * n_start);
    memcpy(u, start, sizeof(char) * n_start);

    // traverse_ (f u) moves
    for (int i = 0; i < n_moves; i++)
    {
        f(u, moves[i]);
    }

    // return u
    return u;
}

在 Haskell 中,指针算术被抽象掉了,但本质上traverse_ in ST作品 像这样。我不太熟悉 C 也不熟悉 C 的内部工作原理ST抽象化,所以 这只是一个类比,而不是试图精确再现。尽管如此,我希望它能帮助读者观察两者之间的相似之处ST和普通的命令式 C 代码。

任务完成!

它运行得相当快。只需片刻即可在百万尺寸上绘制百万步比赛 木板。我希望它也解释得足够清楚。如果有问题或不清楚,请随时发表评论。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何在 Haskell 中制作打勾游戏的图案? 的相关文章

随机推荐

  • 是否可以在 Visual Studio 中重命名项目,使其文件夹名称也重命名?

    假设我们正在开发一个名为 MyProject 的项目 我希望能够将其名称更改为 MyProject2 并将其文件夹名称也重命名为 MyProject2 这可以从 Visual Studio 中实现吗 如果不是 如何让这种情况发生在 外部 呢
  • preg_match 所有以@开头的单词?

    我对正则表达式不太确定 所以我不得不问你 如何用 PHP 判断字符串中是否包含以 开头的单词 例如我有一个像 This is for codeworxx 这样的字符串 我很抱歉 但我没有任何起点 希望你能帮忙 谢谢 萨沙 好的 谢谢你的结果
  • 如何从 BottomNavigationBar 中删除图标?

    我只需要 BottomNavigationBarItem 中的标签 但我找不到删除它们的方法 您可以隐藏标签showSelectedLabels and showUnselectedLabels设置为 false 但图标没有等效项 构造函数
  • 如何使用 Python 3 正确显示倒计时日期

    我正在尝试获取将显示的倒计时 基本上就像一个世界末日时钟哈哈 有人可以帮忙吗 import os import sys import time import datetime def timer endTime datetime datet
  • 无法从 JSON 请求获取数据,尽管我知道它已返回

    我试图获取从 getJSON 返回的数据 但我无法让它工作 我已经在 search twitter API 上尝试了相同的代码 效果很好 但它不适用于其他网站 我知道数据已返回 因为我在使用检查器时可以找到它 我通过检查器找到的值是 id
  • Java RMI - 客户端超时

    我正在使用 Java RMI 构建分布式系统 它必须支持服务器丢失 如果我的客户端使用 RMI 连接到服务器 如果该服务器出现故障 例如电缆问题 我的客户端应该会收到异常 以便它可以连接到其他服务器 但是当服务器出现故障时 我的客户端什么也
  • 更改 Windows Phone 系统托盘颜色

    有没有办法将 Windows Phone 上的系统托盘颜色从黑色更改为白色 我的应用程序有白色背景 所以我希望系统托盘也是白色的 您可以在页面 XAML 中执行此操作
  • C++ Streambuf 方法可以抛出异常吗?

    我正在尝试找到一种方法来获取读取或写入流的字符数 即使存在错误并且读 写结束时间较短 该方法也是可靠的 我正在做这样的事情 return stream rdbuf gt sputn buffer buffer size 但如果streamb
  • 创建IOT设备边缘Python Sdk

    如何创建物联网边缘设备 实际上我使用azure sdk服务物联网并创建了普通设备 但我不知道如何使用azure服务客户端sdk创建物联网边缘设备 这与创建普通设备的方式类似 唯一的区别是您需要添加一个capabilities财产 devic
  • 如何检测当前的 JSF 版本?

    我正在开发 jsf webapp 现在我需要知道我正在使用什么 JSF 版本 我在哪里可以查到这个 提前致谢 您的意思是 以编程方式 你可以从Package getImplementationVersion http docs oracle
  • 批量插入不适用于 NULL 数据

    当我从 CSV 文件将批量数据插入到表中时 它不起作用 显示错误 第 2 行第 9 列的批量加载数据转换错误 类型不匹配或指定代码页的字符无效 csv 文件中的第 9 列值为空 我该如何处理这个问题 根据这些信息 我认为目标表的特定字段被定
  • 抛出 Java 异常时是否会生成堆栈跟踪?

    这是假设我们不调用 printstacktrace 方法 只是抛出和捕获 我们正在考虑这样做是为了解决一些性能瓶颈 不 堆栈跟踪是在构造异常对象时生成的 而不是在抛出异常对象时生成的 Throwable 构造函数调用 fillInStack
  • 如何在 DropDownList 中保留空格 - ASP.net MVC Razor 视图

    我在视图中通过以下方式绑定我的模型 问题是我的项目文本是格式化文本 单词之间有空格 如下所示 123 First 234 00 123 AnotherItem 234 00 123 Second 234 00 我想保留此项目文本中的空格 即
  • PHP:是否可以从文件内容(字符串)创建 SplFileObject 对象?

    例如 contents file get contents image png 是否可以从 contents 创建 SplFileObject 对象 Thanks php 有一些特殊的流包装器 http www php net manual
  • 矩阵到数组 C#

    这将是转换方阵的最有效方法 例如 1 2 3 4 5 6 7 8 9 into 1 2 3 4 5 6 7 8 9 in c 我在做 int array2D new int 1 2 3 4 5 6 7 8 9 int array1D new
  • iOS 13 检查 CLLocationManager 的临时授权状态

    根据 WWDC 视频 https developer apple com videos play wwdc2019 705 https developer apple com videos play wwdc2019 705 当你要求 Al
  • Erlang:如何将原子转换为字符串?

    我想从原子转换为字符串 Input hello world Output hello world 我该如何实现这一目标 Use atom to list http erlang org doc man erlang html atom to
  • 使用 SERVER_NAME 时出现 Flask 404

    在我的 Flask 配置中 我将 SERVER NAME 设置为 app example com 之类的域 我这样做是因为我需要使用url for with external网址 如果未设置 SERVER NAME Flask 会认为服务器
  • 在简单注入器中解析具有自定义参数的类

    我正在使用以下命令创建 WPF MVVM 应用程序简易注射器作为 DI 容器 现在 当我尝试从简单注入器解析视图时遇到一些问题 因为我需要在构造时将参数传递到构造函数中 而不是在将视图注册到容器时 因此这不是适用的 简单注入器将值传递到构造
  • 如何在 Haskell 中制作打勾游戏的图案?

    实现有 2 个参数的函数 ticktick 第一个参数是自然数元组 定义游戏场地的行数和列数 第二个列表包含由玩家 x 和玩家 o 轮流玩的坐标给出的井字游戏比赛的记录 打印游戏的实际状态 其中游戏区域将由字符 和 界定 空方块 以及字符