Как сделать шаблон игры в Ticktack в Haskell? - PullRequest
2 голосов
/ 09 ноября 2019

Реализовать функцию ticktack, которая имеет 2 аргумента. Первый аргумент - это кортеж натуральных чисел, который определяет количество строк и столбцов игрового поля. Второй список содержит запись совпадения игры в тикетах, заданную координатами, по которым играли по очереди игрок '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 [Strings] с 1 результатом,поэтому мне нужно, чтобы эти результаты были объединены в одну [String].

1 Ответ

0 голосов
/ 09 ноября 2019

Как говорит @luqui в комментарии к вопросу:

Вы можете объединить выходные данные ... или вы можете искать в истории один раз для каждого пробела на доске. ...

Первое решение описано в соседнем вопросе . Решенная здесь проблема "шахматы" лишь поверхностно отличается от вашей проблемы "крестики-нолики" , поэтому адаптировать решение не должно быть слишком сложно. Однако:

  • В этом случае размер платы фиксирован и мал, поэтому нас не беспокоит неэффективность объединения досок попарно.

  • В этом случае размер платы является переменным, поэтому стоит попробовать решение последним методом.

Чтобы сделать алгоритм еще более эффективным, вместо прокрутки поBoard и в поисках подходящих ходов в каждой ячейке мы прокручиваем ходы и назначаем значения доске, представленной в виде изменяемого массива. Изменяемые массивы можно считать «продвинутой техникой» в функциональном программировании, поэтому это также может быть хорошим упражнением для промежуточного Хаскеллера. Я использовал их только один или два раза, поэтому давайте посмотрим, смогу ли я это выяснить!

Как это будет работать?

В основе программы будет прямоугольный массивбайтов. Массив представлен в двух вариантах: изменяемый и «замороженный» . Хотя замороженный массив не может быть изменен, существует правило, что изменяемый массив может существовать только в монадическом контексте, поэтому мы можем свободно передавать массив только тогда, когда он заморожен. Если это кажется слишком сложным, я могу только попросить читателя поверить, что дополнительные гарантии безопасности стоят этого усложнения.

В любом случае, вот типы:

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

Надеюсь, это выглядит разумно? Хотя мы еще не написали никакого осязаемого кода.

Можем ли мы написать код?

Да.

Во-первых, нам понадобится некоторый импорт. Никто не любит импорт, но по какой-то причине он еще не автоматизирован. Итак, вот:

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" создать массив и выиграть некоторую скорость, но нам нужно сделать это только один раз, и я хотел, чтобы он был компонуемым - я думаю, что программа будет проще в этом смысле.

Еще одна простая вещь - это написать «клей» код, который регулирует формат ввода и вывода:

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: измените массив, затем заморозьте его - и все изменения должны произойти в монаде ST, для безопасности. foldST содержит все "императив" "внутренний цикл" нашей программы.

(PS) Как это на самом деле работает?

Давайте сначала развернем типы UArray и STUArray. Каковы они и в чем разница?

UArray означает «нераспакованный массив» , то есть массив значений, в отличие от массива указателей,Значение в нашем случае на самом деле является символом Unicode, а не C "байтом" char, так что это не байт, а сущность переменного размера. Когда он хранится в распакованном виде, он преобразуется в Int32 и возвращается нам незаметно. Int32, конечно, слишком много для нашей скромной цели хранения 3 различных значений, поэтому здесь есть место для улучшения. Чтобы узнать больше о распакованных значениях, я предлагаю вам ознакомиться со статьей, в которой они были введены еще в 1991 году, «Распакованные значения как граждан первого класса на неограниченном функциональном языке» .

То, что значения распакованы, не означает, что вы можете изменить их. Чистая ценность в Хаскеле всегда неизменна. Итак, если вы измените одно значение в массиве, весь массив будет скопирован - дорого! Вот где приходит STUArray. ST обозначает State Thread, а STUArray представляет собой «незамерзающий» массив , где вы можете перезаписывать отдельные значения, не копируя все это. Для обеспечения безопасности он может жить только в монаде, в данном случае в монаде ST. (Обратите внимание, что значение STUArray никогда не появляется за пределами ST s.) Вы можете представить вычисление ST как небольшой императивный процесс со своей собственной памятью, отделенной от внешнего мира. История гласит, что сначала они изобрели ST, а затем выяснили, что могут извлечь из него IO, поэтому IO на самом деле скрыто ST. Для получения более подробной информации о том, как работает ST, ознакомьтесь с оригинальной статьей 1994 года: "Lazy Functional State Threads" .

Давайте теперь рассмотрим более внимательнопосмотрите на foldST. Мы видим, что функционально , это не имеет смысла. Сначала мы связываем значение start' с 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 в качестве аргумента, но результатбесполезный тип (). В функциональной обстановке эта строка ничего не значит. Но в монаде связанные значения могут казаться меняться. f - это функция, которая может изменять состояние монады и, следовательно, может изменять значение связанных имен, когда они возвращаются. Аналогичный код в 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_ в ST работает следующим образом. Я не очень знаком ни с Си, ни с внутренней работой абстракции ST, так что это всего лишь аналогия, а не попытка точного воспроизведения. Тем не менее, я надеюсь, что это поможет читателю заметить сходство между ST и обычным императивным кодом C.

Миссия выполнена!

Она работает достаточно быстро. Требуется всего лишь мгновение, чтобы нарисовать матч на миллион шагов на доске размером в миллион. Я надеюсь, что это также объясняется достаточно четко. Не стесняйтесь комментировать, если что-то не так или неясно.

...