Как бы то ни было, я тоже бывший юрист по кодированию. Это обновление отличной статьи @coding_lawyer. Если у вас есть вопросы, лучше спросите его, хотя я постараюсь ответить, если вы спросите меня. В основном зависимости и синтаксис были обновлены до reason-react@0.4.1. Развлекайся!
Возможно, вы слышали о Reason раньше. Это синтаксис поверх OCaml, который компилируется как в читаемый код JavaScript, так и в собственный и байт-код.
Это означает, что вы потенциально можете написать одно приложение, используя синтаксис Reason, и иметь возможность запускать его в браузере, а также на телефонах Android и iOS.
Это одна из причин, почему разум (ой, каламбур) становится все более популярным. Это особенно актуально для сообщества JavaScript из-за сходства синтаксиса.
Если бы вы были разработчиком JavaScript до появления Reason и хотели изучить язык функционального программирования (FP), вам также пришлось бы изучить совершенно новый синтаксис и набор правил. Это могло обескуражить многих.
Используя Reason, вам в основном необходимо понимать принципы FP, на которых он основан, такие как неизменяемость, каррирование, композиция и функции высшего порядка.
До того, как я открыл для себя Reason, я пытался максимально использовать принципы FP в JavaScript. Однако в этом смысле JavaScript ограничен, поскольку он не предназначен для использования в качестве языка программирования FP. Чтобы эффективно использовать эти принципы, вам нужно использовать набор библиотек, которые создают сложные абстракции, которые скрыты от вас.
Reason, с другой стороны, открывает всю область FP для всех заинтересованных разработчиков JavaScript. Это дает нам возможность использовать все эти классные функции OCaml, используя синтаксис, который мы хорошо знаем.
И последнее, но не менее важное: мы можем писать наши приложения React или React Native с помощью Reason.
Почему вам стоит попробовать Reason?
Я надеюсь, что вы откроете для себя ответ к тому времени, когда закончите читать этот пост.
По мере прохождения исходного кода классической игры Tic Tac Toe, написанной на Reason с использованием React, я объясню основные особенности языка. Вы увидите преимущества строгой системы типов, неизменяемости, сопоставления с образцом, функциональной композиции с использованием конвейера и т. Д. В отличие от JavaScript, эти функции являются неотъемлемой частью самого Reason.
Разогрев
Прежде чем запачкать руки, вам необходимо установить Reason на вашу машину, следуя этому руководству.
После этого вам нужно настроить приложение. Для этого вы можете либо клонировать мой репозиторий, содержащий код нашего приложения, либо настроить свой собственный проект, используя ReasonScripts и код.
Чтобы просмотреть свое приложение в браузере, вам необходимо сначала скомпилировать файлы Reason в файлы JavaScript. Об этом позаботится компилятор BuckleScript.
Другими словами, когда вы запускаете npm start (в проекте ReasonScripts), ваш код Reason компилируется в JavaScript. Затем результат компиляции отображается в браузере. Вы можете сами убедиться, насколько читабелен скомпилированный код, проверив папку lib внутри вашего приложения.
Наш первый компонент
Как мы уже упоминали, наше приложение Tic Tac Toe написано с использованием библиотеки ReasonReact. Это делает Reason доступным для разработчиков JavaScript, и многие новички приходят из этого сообщества.
Наше приложение имеет классическую структуру компонентов, как и любое другое приложение React. Мы рассмотрим компоненты сверху вниз, когда говорим о пользовательском интерфейсе, и снизу вверх при описании их логики.
Давайте начнем с рассмотрения компонента верхнего уровня приложения.
let component = ReasonReact.statelessComponent("App");
let make = _children => { ...component, render: _self => <div> <div className="title"> (ReasonReact.string("Tic Tac Toe")) </div> <Game /> </div>, };
Компонент создается, когда вы вызываете ReasonReact.statelessComponent и передаете ему имя компонента. Вам не нужны классовые ключевые слова, как в React, поскольку в Reason их вообще нет.
Компонент не является ни классом, ни функцией - это так называемая запись. record - это одна из структур данных Reason, которая похожа на объект JavaScript. Однако, в отличие от последнего, запись неизменна.
Наш новый компонент записи содержит различные свойства по умолчанию, такие как начальное состояние, методы жизненного цикла и рендеринг. Чтобы настроить компонент в соответствии с нашими потребностями, нам нужно переопределить некоторые из этих свойств. Мы можем сделать это внутри функции make, которая возвращает наш компонент.
Поскольку запись неизменна, мы не можем изменить ее свойства путем изменения. Вместо этого нам нужно вернуть новую запись. Для этого нам нужно распространить наш компонент и переопределить свойства, которые мы хотим изменить. Это очень похоже на оператор распространения объекта JavaScript.
Поскольку приложение представляет собой довольно простой компонент, мы хотим переопределить только метод рендеринга по умолчанию, чтобы мы могли отображать наши элементы на экране. Метод render принимает единственный аргумент self, который дает нам доступ к состоянию и редукторам, как мы увидим позже.
Поскольку ReasonReact поддерживает JSX, наша функция рендеринга может возвращать элементы JSX. Элемент без заглавной буквы будет распознан как элемент DOM - div. Элемент с заглавной буквы будет распознан как компонент - Игра.
Из-за строгой системы типов Reason вы не можете просто передать строку элементу, чтобы отобразить его, как в классическом React.
Вместо этого вам нужно передать такую строку во вспомогательную функцию ReasonReact.string, которая преобразует ее в responseElement, который можно отобразить.
Поскольку это немного многословно и мы будем использовать этот помощник довольно часто, давайте сохраним его в переменной toString. В Reason для этого можно использовать только ключевое слово let.
let toString = ReasonReact.string;
Прежде чем двигаться дальше, давайте немного поговорим об аргументах функции make. Поскольку мы не передаем никаких свойств компоненту приложения, он принимает только аргумент дочерних элементов по умолчанию.
Однако мы этим не пользуемся. Мы можем сделать это явным, написав перед ним символ подчеркивания. Если мы этого не сделали, компилятор выдаст нам предупреждение о том, что аргумент не используется. Мы делаем то же самое с аргументом self в методе рендеринга.
Понятные сообщения об ошибках и предупреждения - еще одна полезная функция, которая по сравнению с JavaScript может улучшить ваш опыт разработчика.
Настройка типов вариантов
Прежде чем углубляться в само приложение, мы сначала определим наши типы.
Reason - это язык со статической типизацией. Это означает, что он оценивает типы наших значений во время компиляции. Другими словами, вам не нужно запускать приложение, чтобы проверить правильность ваших типов. Это также означает, что ваш редактор может предоставить вам полезную поддержку редактирования.
Однако наличие системы типов не означает, что вам нужно явно определять типы для всех значений. Если вы решите не делать этого, Reason определит (выведет) типы за вас.
Мы воспользуемся преимуществами системы типов, чтобы определить типы, которые мы будем использовать в нашем приложении. Это заставит нас задуматься о структуре нашего приложения перед его написанием, а в качестве бонуса мы получим документацию по коду.
Если у вас есть опыт работы с TypeScript или Flow, типы Reason будут вам знакомы. Однако, в отличие от этих двух библиотек, вам вообще не нужна никакая предыдущая конфигурация (я смотрю на вас, Typescript). Типы доступны из коробки.
В Reason мы можем различать типы и вариантные типы (краткие варианты). Типы: например, bool, string и int. С другой стороны, варианты более сложные. Думайте о них как о перечислимых наборах значений или, точнее, о конструкторах. Как мы увидим позже, варианты можно обрабатывать с помощью сопоставления с образцом.
type player = | Cross | Circle;
type field = | Empty | Marked(player);
Здесь мы определяем варианты "player" и "field". При определении варианта вам необходимо использовать ключевое слово `type`.
Поскольку мы создаем игру «Крестики-нолики», нам понадобятся два игрока. Итак, у типа игрока будет два возможных конструктора - «Крест» и «Круг».
Если мы думаем об игровом поле, мы знаем, что каждый тип поля может иметь два возможных конструктора - либо «Пустое», либо «Отмеченное» одним из игроков.
Если вы посмотрите на конструктор Marked, то увидите, что мы используем его как структуру данных. Мы используем вариант для хранения другой части данных. В нашем случае мы передаем ему вариант player. Это довольно мощное поведение, поскольку оно позволяет нам комбинировать различные варианты и типы вместе для создания более сложных типов.
Итак, у нас есть вариант "поле". Однако нам нужно определить все игровое поле, состоящее из рядов полей.
type row = list(field);
type board = list(row);
Каждая «строка» представляет собой список «полей», а игровое поле состоит из списка «строк».
Список представляет собой одну из структур данных Reason, аналогичную массиву JavaScript. Разница в том, что она неизменна. Reason также имеет массив в виде изменяемого списка фиксированной длины. Мы вернемся к этим структурам позже.
type gameState =
| Playing(player)
| Winner(player)
| Draw;
Другой вариант, который нам нужно определить, - это `gameState`. В игре может быть три возможных состояния. Один из игроков может быть «Играющим», «Победителем» или у нас может быть «Ничья».
Теперь у нас есть все типы, необходимые для составления состояния нашей игры.
type state = {
board,
gameState,
};
Состояние нашего компонента - это запись, состоящая из доски и gameState.
Прежде чем двигаться дальше, я хотел бы поговорить о модулях. В Reason файлы - это модули. Например, мы сохранили все наши варианты в файле SharedTypes.re. Этот код автоматически помещается внутрь модуля следующим образом:
module SharedTypes { // variant types code }
Если мы хотим получить доступ к этому модулю в другом файле, нам не нужно ключевое слово import. Мы можем легко получить доступ к нашим модулям в любом месте нашего приложения, используя точечную нотацию - например, `SharedTypes.gameState`.
Поскольку мы используем наши варианты довольно часто, мы можем сделать их более краткими, написав `open SharedTypes;` в верхней части файла, в котором мы хотим получить доступ к нашему модулю. Это позволяет нам отказаться от записи через точку, поскольку мы можем использовать наш модуль в области видимости нашего файла.
Создание государства
Поскольку мы знаем, как будет выглядеть состояние нашего приложения, мы можем приступить к созданию самой игры.
Мы видели, что наш компонент App отображает компонент Game. Это место, где начинается все самое интересное. Я покажу вам код шаг за шагом.
Приложение было компонентом без сохранения состояния, похожим на функциональный компонент в React. С другой стороны, Game имеет состояние, что означает, что она может содержать состояние и редукторы. Редукторы в Reason основаны на тех же принципах, что и те, что вы знаете по Redux. Вы вызываете действие, и редуктор его поймает и соответствующим образом обновит состояние.
Чтобы увидеть, что происходит в компоненте Game, давайте проверим функцию make (код сокращен).
let component = ReasonReact.reducerComponent("Game");
let make = _children => { ...component, initialState: () => initialState,
reducer: (action: action, state: state) => ...,
render: ({state, send}) => ..., };
В компоненте App мы переопределили только метод render. Здесь мы также переопределяем свойства reducer и initialState. О редукторах поговорим позже.
initialState - это функция, которая (что удивительно) возвращает начальное состояние, которое мы сохранили в переменной.
let initialState = {
board: [
[Empty, Empty, Empty],
[Empty, Empty, Empty],
[Empty, Empty, Empty],
],
gameState: Playing(Cross),
};
Если вы немного прокрутите вверх и проверьте наш тип состояния, вы увидите, что у `initialState` такая же структура. Он состоит из "доски", состоящей из "ряда" полей. В начале игры все поля пустые.
Однако их статус может измениться по ходу игры. Другая часть состояния - это `gameState`, которая изначально устанавливается для игрока` Cross`, который играет первым.
Доска для рендеринга
Давайте посмотрим на метод рендеринга нашего компонента Game.
render: ({state, send}) =>
<div className="game">
<Board state onRestart=(_evt => send(Restart))
onMark=(id => send(ClickSquare(id)))
/>
</div>,
Мы уже знали, что он принимает аргумент self. Здесь мы используем деструктурирование для доступа к `state` и функции` send`. Это работает так же, как в JavaScript.
Метод render возвращает компонент Board и передает ему state и два обработчика состояния в качестве свойств. Первый отвечает за перезапуск приложения, а второй срабатывает, когда игрок отмечает поле.
Вы могли заметить, что мы не пишем state = state при передаче свойства state. В Reason, если мы не меняем имя свойства, мы можем передать свойство, используя этот упрощенный синтаксис, называемый каламбуром.
Теперь мы можем взглянуть на компонент Board. На данный момент я пропустил большую часть метода рендеринга.
let component = ReasonReact.statelessComponent("Board");
let make = (~state: state, ~onMark, ~onRestart, _children) => {
...component,
render: (_) => <div className="game-board"> // ... </div>,
};
Совет - это компонент без гражданства. Как вы могли заметить, функция make теперь принимает несколько аргументов. Это свойства, которые мы передали от родительского компонента Game.
Символ ~ означает, что аргумент помечен. При вызове функции с таким аргументом нам нужно явно указать имя аргумента при вызове этой функции (компонента). И это то, что мы сделали, когда передали ему реквизиты в компоненте Game.
Вы также могли заметить, что мы делаем еще одну вещь с одним из аргументов - ~ state: state. В предыдущем разделе мы определили наш тип состояния. Здесь мы сообщаем компилятору, что структура этого аргумента должна быть такой же, как у типа состояния. Возможно, вы знаете этот паттерн из Flow.
Вернемся к методу рендеринга компонента Board.
Поскольку здесь мы имеем дело со списками, мы поговорим о них немного подробнее, прежде чем исследовать остальную часть метода рендеринга.
Экскурсия I: список и массив
В Reason у нас есть две структуры данных, напоминающие массивы JavaScript - список и массив. Список неизменяемый и изменяемый размер, тогда как массив изменяемый и имеет фиксированную длину. Мы используем список из-за его гибкости и эффективности, которые действительно проявляются, когда мы используем его рекурсивно.
Чтобы отобразить список, вы можете использовать метод List.map, который получает два аргумента - функцию и список. Функция берет элемент из списка и отображает его. Это работает почти так же, как JavaScript Array.map. Вот простой пример:
let numbers = [1, 5, 8, 9, 15]; let increasedNumbers = List.map((num) => num + 2, numbers); Js.log(increasedNumbers); // [3,[7,[10,[11,[17,0]]]]]
Какие? Вы хотите сказать, что результат печати выглядит странно? Это потому, что списки в Reason связаны.
Печать списков в вашем коде может сбивать с толку. К счастью, вы можете преобразовать его в массив с помощью метода Array.of_list.
Js.log(Array.of_list(increasedNumbers)); // [3,7,10,11,17]
Вернемся к нашему приложению и вспомним, как выглядит наше состояние.
let initialState = { board: [ [Empty, Empty, Empty], [Empty, Empty, Empty], [Empty, Empty, Empty], ], gameState: Playing(Cross), };
Внутри метода рендеринга Board мы сначала отображаем доску, которая состоит из списка строк. Итак, сопоставив его, мы получим доступ к строкам. Затем мы визуализируем компонент BoardRow.
let component = ReasonReact.statelessComponent("Board"); let make = (~state: state, ~onMark, ~onRestart, _children) => { ...component, render: (_) => <div className="game-board"> ( ReasonReact.array( Array.of_list( List.mapi( (index: int, row: row) => <BoardRow key=(string_of_int(index)) gameState=state.gameState row onMark index />, state.board, ), ), ) ) // ...
Мы используем метод List.mapi, который предоставляет нам аргумент индекса, который нам нужен для однозначного определения наших идентификаторов.
При сопоставлении списка с элементами JSX нам нужно сделать еще две вещи.
Во-первых, нам нужно преобразовать его в массив с помощью Array.of_list. Во-вторых, нам нужно преобразовать результат в responseElement с помощью ReasonReact.array, поскольку мы (как уже упоминалось) не можем просто передать строку в элемент JSX, как в React.
Чтобы перейти к значениям полей, нам также необходимо отобразить каждую строку. Мы делаем это внутри компонента BoardRow. Здесь каждый элемент из строки затем сопоставляется с компонентом Square.
let component = ReasonReact.statelessComponent("BoardRow"); let make = (~gameState: gameState, ~row: row, ~onMark, ~index: int, _children) => { ...component, render: (_) => <div className="board-row"> (ReasonReact.array( Array.of_list( List.mapi( (ind: int, value: field) => { let id = string_of_int(index) ++ string_of_int(ind); <Square key=id value onMark=(() => onMark(id)) gameState />; }, row, ), ), )) </div>, };
Используя эти два сопоставления, наша доска визуализируется. Вы согласитесь со мной, что читабельность этого кода не так хороша из-за всех оберток функций.
Чтобы улучшить его, мы можем использовать оператор конвейера, который берет данные нашего списка и передает их через наши функции. Вот второй пример сопоставления - на этот раз с использованием конвейера.
let component = ReasonReact.statelessComponent("BoardRow"); let make = (~gameState: gameState, ~row: row, ~onMark, ~index: int, _children) => { ...component, render: (_) => <div className="board-row"> ( row |> List.mapi((ind: int, value: field) => { let id = string_of_int(index) ++ string_of_int(ind <Square key=id value onMark=(() => onMark(id)) gameState />; }) |> Array.of_list |> ReasonReact.array ) </div>, };
Это делает наш код более читабельным, не так ли? Сначала мы берем строку и передаем ее методу сопоставления. Затем мы конвертируем наш результат в массив. Наконец, мы преобразовываем его в reactElement.
Сопоставляя нашу доску, мы визуализируем на экране кучу компонентов Square и тем самым создаем целую игровую доску.
Переносим на Площадь парочку реквизита. Поскольку мы хотим, чтобы наш идентификатор был уникальным, мы создаем его, комбинируя индексы из обоих сопоставлений. Мы также передаем значение, которое содержит тип поля, который может быть пустым или отмеченным.
Наконец, мы передаем gameState и обработчик onMark, который будет вызываться при нажатии на конкретный Square.
Ввод полей
let component = ReasonReact.statelessComponent("Square"); let make = (~value: field, ~gameState: gameState, ~onMark, _children) => { ...component, render: _self => <button className=(getClass(gameState, value)) disabled=(gameState |> isFinished) onClick=(_evt => onMark())> (value |> toValue |> toString) </button>, };
Компонент Square отображает кнопку и передает ей некоторые свойства. Здесь мы используем несколько вспомогательных функций, но я не буду подробно останавливаться на них. Их все можно найти в репо.
Класс кнопки вычисляется с помощью вспомогательной функции getClass, которая окрашивает квадрат в зеленый цвет, когда один из игроков выигрывает. Когда это произойдет, все квадраты также будут отключены.
Чтобы отобразить значение кнопки, мы используем два помощника.
let toValue = (field: field) => switch (field) { | Marked(Cross) => "X" | Marked(Circle) => "O" | Empty => "" };
toValue преобразует тип поля в строку, используя сопоставление с образцом. Мы поговорим о сопоставлении с образцом позже. На данный момент вам нужно знать, что мы сопоставляем данные поля с нашими тремя шаблонами. Итак, результатом будет X, O или пустая строка. Затем мы используем toString, чтобы преобразовать его в reactElement.
Уф. Мы только что отрендерили игровое поле. Давайте быстро вспомним, как мы это сделали.
Наш компонент верхнего уровня App визуализирует компонент Game, который содержит состояние игры, и передает его вместе с обработчиками компоненту Board.
Затем Board берет свойство состояния платы и сопоставляет строки с компонентом BoardRow, который сопоставляет строки с компонентами Square. У каждого Square есть обработчик onClick, который заполняет его квадратом или кругом.
Заставьте его что-нибудь уже сделать!
Давайте посмотрим, как работает наша логика, управляющая игрой.
Поскольку у нас есть доска, мы можем позволить игроку нажимать на любую клетку. Когда это происходит, запускается обработчик onClick и вызывается обработчик onMark.
// Square component <button className=(getClass(gameState, value)) disabled=(gameState |> isFinished) onClick=(_evt => onMark())> (value |> toValue |> toString) </button>
Обработчик onMark передан из компонента BoardRow, но изначально он был определен в компоненте Game, который заботится о состоянии.
// Game component render: ({state, send}) => <div className="game"> <Board state onRestart=(_evt => send(Restart)) onMark=(id => send(ClickSquare(id))) /> </div>,
Мы видим, что опора onMark - это редуктор ClickSquare, который мы вызываем с помощью метода send self
, что означает, что мы используем его для обновления состояния (как в Redux). Обработчик onRestart работает аналогично.
Обратите внимание, что мы передаем уникальный идентификатор квадрата обработчику onMark внутри компонента BoardRow.
// Board component ( row |> List.mapi((ind: int, value: field) => { let id = string_of_int(index) ++ string_of_int(ind <Square key=id value onMark=(() => onMark(id)) gameState />; }) |> Array.of_list |> ReasonReact.array )
Прежде чем подробно рассматривать наши редукторы, нам нужно определить действия, на которые будут реагировать наши редукторы.
type action = | ClickSquare(string) | Restart;
Как и в случае с глобальными вариантными типами, это заставляет нас задуматься о нашей логике, прежде чем мы начнем ее реализовывать. Определим два варианта действий. ClickSquare принимает один аргумент, имеющий тип строки.
Теперь давайте посмотрим на наши редукторы.
let updateBoard = (board: board, gameState: gameState, id) => board |> List.mapi((ind: int, row: row) => row |> List.mapi((index: int, value: field) => string_of_int(ind) ++ string_of_int(index) === id ? switch (gameState, value) { | (*_*, Marked(*_*)) => value | (Playing(player), Empty) => Marked(player) | (*_*, Empty) => Empty } : value ) ); reducer: (action: action, state: state) => switch (action) { | Restart => ReasonReact.Update(initialState) | ClickSquare((id: string)) => let updatedBoard = updateBoard(state.board, state.gameState, id); ReasonReact.Update({ board: updatedBoard, gameState: checkGameState3x3(updatedBoard, state.board, state.gameState), }); },
Редуктор ClickSquare принимает идентификатор конкретного Square. Как мы видели, мы передаем компонент BoardRow. Затем наш редуктор вычисляет новое состояние.
Для обновления состояния платы мы вызовем функцию updateBoard. Он использует ту же логику сопоставления, которую мы использовали в компонентах Board и BoardRow. Внутри него мы сопоставляем state.board, чтобы получить строки, а затем сопоставляем строки, чтобы получить значения полей.
Поскольку идентификатор каждого квадрата представляет собой набор идентификаторов из обоих сопоставлений, мы будем использовать его, чтобы найти поле, по которому щелкнул игрок. Когда мы его найдем, мы будем использовать сопоставление с образцом, чтобы определить, что с ним делать. В противном случае мы оставим значение квадрата неизменным.
Экскурсия II: сопоставление с образцом
Мы используем сопоставление с образцом для обработки наших данных. Мы определяем ** шаблоны **, которые сопоставляем с нашими данными. При проверке сопоставления с образцом в Reason мы используем оператор switch.
switch (state.gameState, value) { | (*_*, Marked(*_*)) => value | (Playing(player), Empty) => Marked(player) | (*_*, Empty) => Empty }
В нашем случае мы используем кортеж для представления наших данных. Кортежи - это структуры данных, разделяющие данные запятыми. Наш кортеж содержит gameState и значение (содержащее тип поля).
Затем мы определяем несколько шаблонов, которые сопоставляем с нашими данными. Первое совпадение определяет результат всего сопоставления с образцом.
Записывая подчеркивание внутри шаблона, мы сообщаем компилятору, что нам все равно, какое именно значение. Другими словами, мы хотим, чтобы каждый раз совпадал.
Например, первый образец совпадает, когда значение помечено любым игроком. Итак, нас не волнует gameState, как и тип игрока.
Когда этот шаблон совпадает, результатом является исходное значение. Этот шаблон не позволяет игрокам отменять уже отмеченные квадраты.
Второй паттерн обращается к ситуации, когда играет любой игрок, а поле пусто. Здесь мы используем тип игрока в шаблоне, а затем снова в результате. По сути, мы говорим, что нас не волнует, какой игрок играет (Круг или Крест), но мы все равно хотим пометить квадрат в соответствии с игроком, который на самом деле играет.
Последний шаблон действует как шаблон по умолчанию. Если первый или второй шаблон не совпадают, всегда будет совпадать третий. Здесь нас не волнует gameState.
Однако, поскольку мы проверяем состояние игры Playing в предыдущем шаблоне, теперь мы проверяем тип GameState Draw или Winner. В таком случае оставим поле пустым. Этот сценарий по умолчанию не позволяет игрокам продолжать играть после окончания игры.
Замечательная особенность сопоставления с образцом в Reason заключается в том, что компилятор предупредит вас, если вы не охватили все возможные сопоставления с образцом. Это избавит вас от множества проблем, потому что вы всегда будете знать, рассмотрели ли вы все возможные сценарии. Итак, если компилятор не выдает никаких предупреждений, сопоставление с образцом никогда не завершится ошибкой.
Когда сопоставление с образцом завершено, конкретное поле обновляется. Когда все сопоставления выполнены, мы получаем новое состояние платы и сохраняем его как updatedBoard. Затем мы можем обновить состояние компонента, вызвав ReasonReact.Update.
ReasonReact.Update({ board: updatedBoard, gameState: checkGameState3x3(updatedBoard, state.board, state.gameState),
Мы обновляем состояние платы, используя результат сопоставления с образцом. При обновлении gameState мы вызываем помощник checkGameState3x3, который вычисляет для нас состояние игры.
У нас есть победитель?
Давайте посмотрим, что делает checkGameState3x3.
Во-первых, нам нужно определить все возможные комбинации выигрышных полей (для доски 3x3) и сохранить их как WinningCombs. Мы также должны определить тип winRows.
type winningRows = list(list(int)); let winningCombs = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ];
Мы передали этот список в функцию checkGameState в качестве первого аргумента.
let checkGameState3x3 = checkGameState(winningCombs);
Делая это, мы используем принцип каррирования. Когда мы передаем winCombs функции checkGameState, мы возвращаем новую функцию, ожидающую передачи остальных аргументов. Мы сохраняем эту новую функцию как checkGameState3x3.
Такое поведение действительно полезно, поскольку мы можем настроить функцию checkGameState в зависимости от ширины и высоты доски.
Посмотрим, что происходит внутри функции checkGameState.
let checkGameState = ( winningRows: winningRows, updatedBoard: board, oldBoard: board, gameState: gameState, ) => oldBoard == updatedBoard ? gameState : { let flattenBoard = List.flatten(updatedBoard); let rec check = (rest: winningRows) => { let head = List.hd(rest); let tail = List.tl(rest); switch ( getWinner(flattenBoard, head), gameEnded(flattenBoard), tail, ) { | (Cross, *_*, *_*) => Winner(Cross) | (Circle, *_*, *_*) => Winner(Circle) | (*_*, true, []) => Draw | (*_*, false, []) => whosPlaying(gameState) | *_* => check(tail) }; }; check(winningRows); };
Сначала мы проверяем, отличается ли состояние платы от предыдущего. Если это не так, мы вернем неизмененное состояние gameState. В противном случае мы рассчитаем новое состояние игры.
Расчет новых состояний
Мы начинаем определять наше новое игровое состояние с преобразования части состояния доски, которая состоит из списка строк, в простой список с помощью List.flatten. Сглаженный результат будет иметь такую структуру:
[Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty]
Вернувшись к функции, мы определяем функцию проверки, которая получает единственный аргумент rest, который имеет тип winRows. Ключевое слово rec перед его определением означает, что его можно вызывать рекурсивно. Однако для рекурсивных вызовов функций нам также нужны рекурсивные данные. К счастью, список представляет собой рекурсивную структуру данных.
Мы уже узнали, что списки в Reason связаны. Эта функция позволяет нам легко перебирать списки с использованием рекурсии.
Внизу checkGameState мы впервые вызываем функцию проверки и передаем ей список wincombs. Внутри функции мы извлекаем первый элемент из списка и сохраняем его как заголовок. Остальная часть списка сохраняется как хвост.
После этого мы снова используем сопоставление с образцом. Мы уже знаем, как это работает, поэтому я не буду вдаваться в подробности. Но стоит проверить, как мы определяем наши данные и шаблоны.
type winner = | Cross | Circle | NoOne; switch ( getWinner(flattenBoard, head), gameEnded(flattenBoard), tail, ) { ...
Внутри оператора switch мы снова используем кортеж для представления наших данных. Наш кортеж содержит три элемента - тип победителя в результате функции getWinner, логическое значение в результате функции gameEnded и оставшиеся элементы списка (хвост).
Прежде чем идти дальше, давайте немного поговорим об этих двух вспомогательных функциях.
Сначала мы заглянем внутрь функции getWinner.
let getWinner = (flattenBoard, coords) => switch ( List.nth(flattenBoard, List.nth(coords, 0)), List.nth(flattenBoard, List.nth(coords, 1)), List.nth(flattenBoard, List.nth(coords, 2)), ) { | (Marked(Cross), Marked(Cross), Marked(Cross)) => Cross | (Marked(Circle), Marked(Circle), Marked(Circle)) => Circle | (*_*, *_*, *_*) => NoOne };
Когда мы вызываем рекурсивную функцию проверки в первый раз, заголовок будет первым элементом winRows, то есть [0, 1, 2], который является списком. Мы передаем head функции getWinner в качестве аргумента coords вместе с flattenBoard.
Опять же, мы используем сопоставление с образцом с кортежем. Внутри кортежа мы используем метод List.nth для доступа к эквивалентным позициям координат координат в сглаженном списке платы. Функция List.nth принимает список и число и возвращает элемент списка в эту позицию.
Итак, наш кортеж состоит из трех выигрышных координат нашей доски, к которым мы получили доступ с помощью List.nth.
Теперь мы можем сопоставить данные нашего кортежа с шаблонами. Первые два шаблона проверяют, помечены ли все три поля одним и тем же игроком. Если да, то вернем победителя - Крестик или Круг. В противном случае мы вернем NoOne.
Посмотрим, что происходит внутри функции gameEnded. Он проверяет, все ли поля отмечены, и возвращает логическое значение.
let gameEnded = board => List.for_all( field => field == Marked(Circle) || field == Marked(Cross), board, );
Поскольку мы знаем, какие значения могут быть возвращены нашими вспомогательными функциями, давайте вернемся к нашей функции проверки.
switch ( getWinner(flattenBoard, head), gameEnded(flattenBoard), tail, ) { | (Cross, *_*, *_*) => Winner(Cross) | (Circle, *_*, *_*) => Winner(Circle) | (*_*, true, []) => Draw | (*_*, false, []) => whosPlaying(gameState) | *_* => check(tail) };
Наше сопоставление с образцом теперь может определить, закончилась ли игра победой или ничьей. Если эти случаи не совпадают, мы перейдем к следующему.
Если он совпадает, игра продолжится, и будет вызвана функция whosPlaying, а другой игрок сделает ход.
Если эти случаи не совпадают, мы перейдем к следующему. Если он совпадает, игра продолжится, и будет вызвана функция whosPlaying, а другой игрок сделает ход.
let whosPlaying = (gameState: gameState) => switch (gameState) { | Playing(Cross) => Playing(Circle) | *_* => Playing(Cross) };
В противном случае мы вызовем функцию проверки рекурсивно с новой комбинацией выигрышных полей.
Вот и все. Теперь вы знаете, как работает наш код, управляющий логикой игры.
Вот и все, ребята!
Я надеюсь, что этот пост помог вам понять основные особенности этого многообещающего и все еще развивающегося языка. Однако, чтобы в полной мере оценить возможности этого нового синтаксиса поверх OCaml, вам нужно начать создавать свои собственные материалы. Теперь вы готовы к этому.
Удачи!
Если вам понравилась эта статья, дайте ей несколько аплодисментов **. ** Я был бы очень признателен, и больше людей смогут увидеть этот пост.
Если у вас есть какие-либо вопросы, критика, наблюдения или советы по улучшению, не стесняйтесь писать комментарий ниже или связаться с первоначальным автором через Twitter или его блог.
Спасибо mediumexporter от @xdamman и markdowntomedium от @jacobbennet.
Первоначально опубликовано на gist.github.com.