Откуда он взялся?
Первое, что хочется отметить, формально, reason не является новым языком программирования. Это новый синтаксис для уже существующего языка ocaml. Что, как минимум, говорит о том, что язык проверен временем ✅.
Чаще всего о reason'е говорят в контексте javascript'а и reactjs, и не с проста: наряду с новым синтаксисом разрабатывается ещё и компилятор в js - bucklescript, а его (reason) разработкой руководит Jordan Walke, ранее создавший react.
Чего от него ждать? Где использовать?
Повторюсь, reason не новый язык, а синтаксис. Ocaml является языком общего назначения и применяется в самых разных областях (украл с офф сайта):
- автоматические доказыватели теорем 🤷♂️
- компиляторы и интерпретаторы 🙆🏾
- анализаторы программ 🔍
- обучение программированию 👩🏽🎓
Тут вот можно найти истории успеха его использования, в т.ч. и более прикладные.
Reason тоже уже активно используется и можно найти интересные и актуальные примеры на нём:
- revery - фреймворк для разработки кросс-платформенных десктопных приложений 💻
- onivim - среда разработки, построенная на 👆
- rely - jest-like blazing fast native test framework ⚡️
- ...
И что будет более интересным для react разработчиков: поговаривают, что reason станет лучшей платформой для реакта, ведь:
- он функциональный, а значит более идиоматичный для реакта.
js тоже можно назвать функциональным языком, но в отличии от js в reason есть:- каррирование (curring)
- сопоставление с образцом (pattern matching)
- встроенная Option монада (которая закрывает проблему `can't read property of underfined`)
- ...
- статически типизированный, с мощным механизмом выведения типов
Опять же тема, казалась бы, не нова для фронтенд сообщества: typescript, flow хорошо себя зарекомендовали, я себе не представляю старт проекта без них. Но reason это совершенно новый уровень: в нём объявлять типы практически не нужно, даже для параметров функции!
Также reason в отличии от них типизированный как бы изначально. В то время как typescript'у и flow приходится бороться/приспосабливаться к динамической природе js, н.р. есть такое понятие как писать код, который можно (или легче) типизировать.
Не думаю, что у меня получится раскрыть эту тему лучше, чем у Jordan Walk в этой ветке:
The hardest part is telling JS developers “no you can’t do that anymore”. I think the easiest path is a fresh, but familiar start, in order to reset expectations.
— Jordan ⚛️ (@jordwalke) 25 июня 2019 г.
- у него отличный interprop с js - можно использовать js в reason 😲и наоборот reason в js 🙃
Результат компиляции bucklescript'ом минималистичный, оптимизированный для производительности и, что не маловажно, читабельный 📖! - его можно компилировать в машинный код 🤪а это значит вы можете получить максимальную производительность.
Особенно это интересно с точки зрения мобильных приложений. Может скоро js'у не будет места в react native, и мы будем компилировать reason в нативный код?
От слов к делу
Ну теперь, я надеюсь, вы заинтересовались языком, и мы можем попробовать что-нибудь на нём изобразить. Долго думать над приложением, которое мы будем писать, мне не пришлось. Ещё с университетских времён я знаком с игрой "Жизнь". Первую реализацию, как мне кажется, я делал на pascal'е, потом был delphi, c++, java, js и вот теперь reason. Я считаю, что реализация этой игры является отличным способом поизучать язык: там и какой-никакой алгоритм нужно реализовать, поработать с коллекциями, написать UI...
Посмотреть на готовую реализацию и исходный код, кстати, уже можно здесь, т.к. начал я этот проект несколько месяцев назад. Но и то и другое скорее всего будет меняться во время этого цикла статей: надеюсь я буду находить лучшие решения.
Место действия этой игры - вселенная, предлагаю и начать с её сотворения.
Variant!
Согласно википедии, вселенная у нас - плоскость, каждая клетка которой может иметь одно из двух состояний: живая (заполненная) 💃 и мёртвая (пустая) 🙅♂️.
В js, да и во многих других языках, я бы прибегнул к использованию boolean или строки ('alive' | 'dead'
) для описания этого состояния. Но первый подход, на мой взгляд, не самый прозрачный/декларативный, а второй мне не нравится тем, что строки и без того многолики: они и текст хранят, и ключами в объектах/картах выступают и наверняка что-то ещё 🤔. В typescript есть enum, что уже ближе к тому, с чем хотелось бы работать. Но в reason есть нечто лучшее - variant'ы. Чем они лучше?
- Они дают бОльшую типо-безопасность - reason заставляет проверить все кейсы variant'ов при работе с ними. Встроеным variant'ом является
option
, тот самый, что спасёт нас отnull
'ов иundefined
'ов, которых в reason нет. В случае сoption
значение у нас может быть -Some('a)
или не быть -None
. - Они могут хранить одно или несколько значений внутри.
- Они идут вкупе с другим мощным механизмом языка - сопоставлением с образцом (pattern matching)
Пока давайте остановимся на том, что объявим наш variant:
type cellState =
| Dead
| Alive;
let myCell = Alive;
Наглядно, не правда ли?
Теперь давайте создадим вселенную - плоскость. Для этого, очевидно, мы будем использовать двумерный массив. Т.к. как цель статьи - изучения языка, давайте попробуем сами реализовать функции, необходимые для создания массивов.
Т.к. двумерный массив ничто иное как массив массивов, начнём мы с функции создания массива, причём не пустого, а наполненного необходимыми значениями. Т.е. я хочу написать функцию, которая принимает 2 значения: длину создаваемого массива и функцию инициализатор, которая будет получать в качестве аргумента индекс инициализируемого элемента (обычно это очень удобно, далее покажу почему). Такие функции в функциональном программировании (далее просто ФП) носят называние функций высшего порядка, т.к. в качестве аргументов они принимают другие функции (или возвращают их).
Рекурсивное создание массива
Другой особенностью языков ФП является то, что в них как правило отсутствуют операторы управления потоком исполнения (🤯): if/else, for/while... т.к. они уводят разработчика от описания вычислений в описание команд машине (не придумал как лучше выразиться 🤷♂️). Да и вообще как заметил в одном из своих докладов Виталий Брагилевский ни чем не лучше всеми ненавистного оператора goto
. И не смотря на то, что в reason циклы есть, мы попытаемся воспользоваться идеоматической конструкцией - рекурсией.
let init = (l, fn) => {
/*
* на каждом шаге рекурсии
* добавляем новый элемент массива
* и передаём результат в следующий шаг
*/
let rec _init = (l, arr) =>
l > 0 ? _init(l - 1, concat(arr, [|fn(l)|])) : arr;
_init(l, [||]);
};
Для людей пришедших из js, незнакомыми тут являются 2 конструкции:
rec
- в reason мы вынуждены явно указать, что наша функция будет рекурсивнойreason±[||]
- литерал создания массива (более привычныйjs±[]
создаст список)
Tail call optimisation
Кто-то, возможно, заметит, что эту функцию можно было бы реализовать и без дополнительной внутренней, а кто-то, что создавать массивы рекурсивно не комильфо - ведь мы можем переполнить стек. И как ни странно оба эти утверждения связаны 😮.
Да, я действительно мог записать эту функцию несколько короче:
let rec init = (l, fn) =>
l > 0 ? concat(init(l - 1, fn), [|fn(l)|]) : [||];
Но именно эта реализация и привела бы к потенциальному переполнению стека 😆. Почему? Функциональные языки программирования появились не вчера, и компиляторы умеют хорошо их оптимизировать. Примером такой оптимизации является tail call. Если коротко, то суть в том, что можно не создавать фрейм в стеке для рекурсивного вызова, если он является последним действием в функции. За счёт этого производительность такой рекурсии будет равна итерации (и вообще может быть ей заменена компилятором). Поподробнее об этом можно прочитать здесь.
И если посмотреть на первую реализацию, то видно что в функции _init
её рекурсивный вызов и является последним, а значит наш компилятор должен её оптимизировать 😌.
И тут искушённый читатель может заметить, что не смотря на то, что ECMAScript 6 предлагает tail call оптимизацию, большинство браузеров реализовать её пока не смогли. И он будет прав 😭. Но! Reason ведь не js, и у него есть свой компилятор - BuckleScript. И он не полагается на интерпретаторы js и сам реализует эту оптимизацию, заменяя эту контрукцию на while
🤪. Так будет выглядить скомпилированная функция:
function init(l, fn) {
var _l = l
var _arr = /* array */ []
while (true) {
var arr = _arr
var l$1 = _l
var match = l$1 > 0
if (match) {
_arr = /* array */ [Curry._1(fn, (l$1 - 1) | 0)].concat(arr)
_l = (l$1 - 1) | 0
continue
} else {
return arr
}
}
}
Императивность под капотом декларативности
Ещё одним замечанием, которое напрашивается, является конкатенация массивов на каждом шаге. И не смотря на то, что я являюсь противником преждевременных оптимизаций, не обратить на это внимание в статье я не могу. Мы действительно создаём массив из одного элемента, а потом копируем его и массив полученный на предыдущем шаге в новый массив. На создание массива из 10к элеметов наша функция тратит ~60ms на моём macbook pro. И с этим магический компилятор reason'а уже ничего поделать не может. Но и не должен!
- Данная функция лишь пример того, как мы можем использовать рекурсию вместо императивных циклов
- Reason не запрещает использовать циклы
- Также можно использовать более идеоматические для ФП структуры данных.
Например использование списков в рекурсивной функции, а затем преобразование этого списка в массив позволяет создать массив из миллиона элементов за ~300ms (ведь добавление элемента в список очень дешёвое) - У reason есть стандартная библиотека Belt которая предоставляет аналогичный метод
makeBy
, который смог создать массив из 10млн элеметов с теже ~60ms
Он, кстати, под капотом имеет ту самую императивщину:
function makeByU(l, f) {
if (l <= 0) {
return /* array */ []
} else {
var res = new Array(l)
for (var i = 0, i_finish = (l - 1) | 0; i <= i_finish; ++i) {
res[i] = f(i)
}
return res
}
}
Типизация
А вы заметили, что мы не описали ни одного типа? Но reason при этом вывел следующий тип для нашей функции: reason±(int, int => 'a) => Js_array.t('a)
, что означает: фунция которая на вход принимает целое число и функцию, которая тоже принимает целое число а возвращает что-то (a'
), а наша исходная функция вернёт массив этого чего-то. Так мы с вами это и задумывали: целое число - это размер создаваемого массива, функция - наш инициализатор, ну и результат действительно массив результатов инициализатора. Кстати, плагин для WebStorm показывает выведенные типы следующим образом:
Магия не правда ли? Для меня, как для человека большую часть времени работающего с typescript'ом - правда. Как это работает? Ну... Reason это ведь новый синтаксис для OCaml, который является реализацией языка Caml, который, в свою очередь, принадлежит семейству ML, в котором используется система типов Хиндли—Милнера (надеюсь не ошибся в этой цепочке 🤞). Именно эта система то и позволяет автоматически выводить типы для выражений. Из теории это всё, что на данный момент могу сказать 😳. На практике же, как вы убедились, это означает что нам вовсе не надо описывать типы чтобы получить статическую типизацию.
Создание матрицы
Ну теперь когда мы умеем создавать массивы, можно и создать массив массивов. Для этого предлагаю использовать библиотечную функцию reason±makeBy
которая сигнатурой не отличается от созданной нами, но на несколько порядков производительнее:
let makeMatrixBy = (dimx, dimy, fn) =>
makeBy(dimy, y => makeBy(dimx, x => fn((x, y))));
Ну тут вроде всё просто: принимаем размеры матрицы и функцию инициализатор, возвращаем массив размером reason±dimy
каждый элемент которого это массив размером reason±dimx
, наполненный значениями которые возвращает функция reason±fn
.
Tuple
Но особенность тут всё же есть - reason±fn((x, y))
. Я не ошибся, это не лишние скобочки, это кортеж (tuple) - неизменяемый, упорядоченный, разнородный список значений фиксированного размера. Как говорит официальная документация эта структура данных очень удобна, когда вам нужно передать или вернуть несколько значений без лишних церемоний.
Ну с неизменяемостью и фиксированным размером, я думаю, всё понятно. Упорядоченность элементов позволяет обойтись без имён свойств. А благодаря разнородности мы можем хранить значения разных типов в одном кортеже.
Т.е. tuple может заменить нам объекты, но я бы не стал их использовать для сущности с более чем тремя атрибутами - сложно будет запомнить, что зачем идёт. А вот для хранения пары координат они подходят идеально 😉.
В js, кстати, аналогом могут выступить массивы, для них (как и в reason для tuple) есть очень удобный синтаксис деструктуризации: js±let [x, y] = coordinates;
(в reason: reason±let (x, y) = coordinates;
).
Ну давайте наконец попробуем создать случайно заполненную вселенную:
let universe = makeMatrixBy(5, 5, _ => Js_math.random() > 0.5 ? Alive : Dead);
Js.log(universe);
Что в консоли хрома даёт нам вот такую картинку:
Итого
Не смотря на то, что с точки зрения игры мы продвинулись не так далеко, мы теперь знаем что такое reason, откуда он пришёл и где его можно использовать, а также разобрали несколько языковых конструкций.
Надеюсь, вам было интересно, и вы ещё придёте читать другие статьи про reason и не только.
А ещё не забывайте давать обратную связь. Сделать это можно через комментарии, а можно выделив текст на странице отправить сообщение о какой-либо неточности.