Если вы следите за мной в Твиттере, возможно, вы видели, как я радостно заявлял о своей новой любви к языкам в стиле ML и сильной выразительной типизации. В ближайшие дни я напишу больше о функциональном программировании и статической типизации, а также о том, почему я думаю, что это будущее быстрой разработки пользовательского интерфейса. А пока я хотел бы познакомить вас с моим любимым функциональным языком - PureScript. Основная команда недавно выпустила отличную новую версию - 0.9.1, и теперь, когда экосистема стабилизируется, самое время начать!

Я предполагаю, что вы обладаете промежуточным знанием текущей экосистемы Node / JS. Я намерен сделать это прагматичным введением с точки зрения профессионала отрасли, стремящегося однажды использовать эту технологию или что-то подобное в производстве. Я ценю PureScript за то, насколько он полезен для инженерных команд и предприятий, создающих реальные продукты с реальным трафиком.

Почему не вяз? Смотрите мою сноску!

Что такое PureScript?

PureScript - это язык с синтаксисом и функциями, очень похожими на Haskell, но с некоторыми заметными отличиями. Он компилируется в читаемый и производительный JavaScript и предоставляет богатый интерфейс внешних функций для непосредственного взаимодействия с другими библиотеками JavaScript. Компилятор построен на Haskell и в настоящее время нацелен на Node, хотя в настоящее время существуют экспериментальные бэкенды для C и Erlang. Он совместим с такими инструментами, как Browserify и Webpack, поэтому его можно запускать в браузере. Одним из больших преимуществ перед аналогичными реализациями является отсутствие громоздкой среды выполнения.

Стиль программирования, который он расширяет, основан на максимальной функциональной чистоте, строгом ограничении состояний и побочных эффектов и принятии совместимости над стандартами. Вы можете использовать богатую систему типов и инструменты тестирования, чтобы математически доказать правильность вашего чистого кода и строго контролировать свой код с отслеживанием состояния. Каждый побочный эффект представляет собой последовательность монадических функций с отслеживанием состояния, которая дает значение, а асинхронные побочные эффекты с типом Aff делают такие вещи, как запросы Ajax, простыми и лаконичными.

Настраиваем все!

Моя локальная среда PureScript использует сценарии npm и поддерживает Babel для транспилирования модулей FFI. Для начала используйте простой скелет проекта с начальным пакетом package.json, .babelrc и другими типичными исходными файлами Node / JS.

Настройка npm

Для начала вы установите PureScript, популярный инструмент сборки pulp, инструмент форматирования ошибок под названием purescript-psa и инструмент разработки под названием pscid.

$ npm install --save-dev purescript pulp purescript-psa pscid

Это установит их в вашу локальную папку node_modules вашего проекта, и вы будете работать с ними через скрипты npm. Давайте теперь настроим эти скрипты.

"scripts": {
    "build": "pulp build"
},

Первый простой. Он просто вызывает локальную целлюлозу в ваших node_modules. Поскольку мы управляем этими зависимостями для каждого проекта, вам не нужно иметь глобальную пульпу для вызова из командной строки.

Далее вам нужно выполнить тесты:

"test": "pulp test"

Этот тоже простой, но с крутым шаром. Ваши тесты будут часто терпеть неудачу, и это неплохо, но npm дает вам много отладочной информации после каждой ошибки, которая может быть вам бесполезна. На практике в вашей среде IDE и CI-системах будет использоваться npm test, но, как вы увидите ниже, npm run dev - это то, что вы будете использовать при разработке.

Я разработчик, ориентированный на тестирование, поэтому мне нравится следить за своими тестами, чтобы они запускались при изменении файла. На самом деле это одна из вещей, которые предоставляет мне инструмент pscid. Настройте быструю команду, которую вы можете использовать для запуска pscid:

"dev": "pscid --test --port 1701"

Я сказал, что использую Babel для своих модулей FFI, и вот как я это использую:

"build:babel": "babel src -s -d lib"

Когда у меня действительно есть несколько модулей FFI, я включаю build: babel в свою команду сборки верхнего уровня.

В настоящее время выбор сообщества PureScript для управления пакетами - это Bower. Скорее всего, это скоро изменится, но пока мы устанавливаем bower в качестве дополнительной зависимости разработчика, а затем добавляем следующую «postinstall»:

"postinstall": "bower install"

В этом я отличаюсь от многих разработчиков PureScript, которых я наблюдал до сих пор. Я ценю модульные тесты как первоклассный инструмент разработки, поэтому первое, что я делаю в новой среде, - это узнаю, как запускать модульные тесты. Мой любимый инструмент сейчас - purescript-test-unit от бесстрашного Bodil Stokke.

$ bower install --save-dev purescript-test-unit

Настройка вашего редактора

Пришло время выбрать текстовый редактор по выбору. Мой редактор - Atom, и он имеет отличную поддержку IDE для PureScript. Если вы используете другой редактор, ознакомьтесь с предложениями на вики-странице Поддержка редакторов и инструментов.

Для Atom вам нужно установить пару пакетов:

$ apm install language-purescript ide-purescript

Чтобы включить ide-purescript, мы должны установить purescript глобально с помощью npm install -g purescript. Я хотел бы настроить его для использования версии для конкретного проекта, но мне пока не удалось это сделать.

Чтобы унифицировать сборки IDE и CLI, создайте новую запись «scripts» в моем package.jsonfile.

"build:json": "pulp build --include lib:test --no-psa --json-errors",

Часть - include lib: test гарантирует, что я буду получать подсказки в моем редакторе, когда я работаю над своими тестами. Часть - json-errors просит пульпу выводить json вместо форматированного текста. Часть - no-psa пропускает purescript-psa, если он найден, потому что форматирование не работает в режиме json.

Наконец, я открываю страницу настроек ide-purescript в Atom и устанавливаю «команду сборки» на npm run build: json. Я убеждаюсь, что отмечены флажками «Создавать при сохранении» и «Использовать быстрое восстановление», и все готово!

Модульное тестирование

Пришло время создать свой первый файл PureScript! Назовите это test / Main.purs. Да, верно, это не src / Main.purs - первый файл PureScript, который мы собираемся создать сегодня, называется test / Main.purs, потому что вы следуете руководству разработчика, ориентированного на тестирование. 🤓

Этот файл будет содержать ваш первый модуль Test.Main. Основная функция, которую мы определим, выполняется автоматически при запуске теста пульпы.

Арт Ванделай, импортер-экспортер

Сначала мы определяем наш модуль и импортируем несколько вещей:

module Test.Main where

import Prelude
import Test.Unit (suite, test)
import Test.Unit.Assert (equal)
import Test.Unit.Main (runTest)

По умолчанию ваш модуль экспортирует все определения верхнего уровня. Вы можете ограничить это, используя круглые скобки:

module Test.Main (main) where

Следующий импорт включает Prelude, минимальную стандартную библиотеку основных строительных блоков. Я импортирую его, не вызывая впоследствии ничего в скобках. Это означает, что все экспортированные определения в Prelude импортируются автоматически.

Однако в последующем импорте Test.Unit я импортирую только те функции, которые использую. Мне нравится делать весь мой импорт явным, но для удобства я делаю исключение для Prelude.

Приложение функции

Теперь давайте напишем набор Hello World:

main = runTest do
  suite "Hello" do
    test "World!" do
      equal (1 + 1) 2

Хорошо. Импорт был неплохим, но для типичного фронтендера это выглядит безумно. 😳 Что это за безудержное безумие? Где точки с запятой, фигурные скобки и функции с аргументами?

Мы поговорим больше о том, что это на самом деле означает за кулисами, но теперь вам нужно знать, что PureScript - это язык, чувствительный к пробелам. Ура! Я скучал по этому поводу с тех пор, как занимался взломом Django на Python! Блоки do вводят новую последовательность вызовов функций. Под капотом это синтаксический сахар вокруг использования PureScript монад для управления побочными эффектами, поскольку ваш тестовый прогон отслеживает состояние и отслеживает, какие тесты прошли или не прошли.

Если вы хотите добавить второй тест, вы бы не делали отступ:

main = runTest do
  suite "Hello" do
    test "World" do
      equal (1 + 1) 2
    test "Far out!" do
      equal (2 + 2) 4

Сам тест на самом деле является вызовом функции:

equal (1 + 1) 2

Если бы вы преобразовали это в простой JavaScript, это выглядело бы так:

equal(1 + 1, 2)

Первое слово выражения - это имя функции, равно. Это произошло из модуля Test.Unit.Assert, который вы импортировали выше. Пробел отделяет имя функции от ее аргументов, а пробел отделяет каждый аргумент. Итак, в этом случае первый аргумент - (1 + 1). Этот аргумент применяется к функции, и здесь происходит волшебство.

PureScript автоматически картирует каждую функцию. Это означает, что каждый раз, когда вы применяете аргумент к функции, она возвращает новую функцию, которая принимает следующий аргумент. Если нет оставшихся аргументов, он выполняет функцию и возвращает результат. Если бы вы преобразовали это в JavaScript, это выглядело бы так:

const equal = result => expected => {
  // ... here lies the implementation of the equal function
}

Или, в простом ES5:

function equal(result) {
  return function(expected) {
    // ... here lies the implementation of the equal function
  }
}

Еще один последний поворотный момент. Знак плюс? Это определяемый пользователем оператор, установленный в Prelude. PureScript дает вам возможность определять оператор как псевдоним для функции. Оператор + - это псевдоним для добавления.

Хорошо, сохраните файл. Есть проблемы со сборкой? В Atom вы, вероятно, будете предупреждены желтой подсветкой о том, что вы не добавили сигнатуру типа к значению верхнего уровня, main. Ничего страшного, о типах пока беспокоиться не будем. PureScript имеет богатую систему вывода типов, которая делает многие аннотации необязательными (хотя часто лучше аннотировать в любом случае). Если вы получаете другие ошибки, убедитесь, что вы выполнили установку npm, и все равно переходите к следующему шагу. Возможно, будет немного легче читать ошибки сборки в CLI с их форматированием purescript-psa, и наш тестовый прогон покажет их нам.

Затем запустите тест целлюлозы из командной строки, чтобы запустить тесты. Ваш результат должен выглядеть примерно так:

Взаимодействие с сообществом

Если ваш тестовый запуск прошел не так гладко, и вы не можете определить, что происходит, по сообщению об ошибке, у PureScript есть отличное активное сообщество! Я вижу, что основные разработчики чаще всего болтают в [#purescript] (http://ircbrowse.net/browse/purescript) на Freenode (для которого я рекомендую бесплатную учетную запись IRCCloud), но есть и активные чаты. доступно на Gitter и Slack. Тусуюсь на всех троих.

Ваш собственный модуль

Пришло время создать первый модуль вашего нового пакета. Давайте займемся чем-нибудь практичным, например, нажмем на API. Давайте установим purescript-affjax, библиотеку, предназначенную для безболезненного AJAX.

$ bower install --save purescript-affjax

Создайте файл src / Main.purs:

module Main where

import Prelude
import Control.Monad.Eff.Console (log)
import Control.Monad.Eff.Class (liftEff)
import Control.Monad.Aff (launchAff)
import Network.HTTP.Affjax (get)

main = launchAff do
  res <- get "http://jsonplaceholder.typicode.com/todos"
  liftEff $ log $ "GET /api response: " <> res.response

Основная функция модуля src / Main.purs - это функция, которая выполняется, когда вы запускаете pulp из командной строки:

Анатомия вызова AJAX

Давайте разберем основную функцию, чтобы увидеть, что происходит. Во-первых, у вас есть launchAff. Мы можем взглянуть на источник здесь и увидеть, что эта функция принимает Aff, дающее что-либо, и возвращает Eff, которое дает Canceler. Комментарий над функцией объясняет: launchAff преобразует вычисление в синхронный эффект Eff и выполняет его. Поскольку обратного вызова успеха нет, значение, полученное в конце блока do, игнорируется, а функция отмены становится новым значением, которое монада возвращает после завершения. Используйте это, когда вы хотите выполнить асинхронный эффект, вам не нужна никакая обработка ошибок, кроме throw, и вас не заботит значение, выдаваемое Aff. Это хорошо соответствует нашей ситуации, поскольку мы просто хотим зарегистрировать ответ, а затем отбросить его.

Затем у вас есть блок do, который в этом случае создает монаду Aff, потому что компилятор замечает, что вы используете get внутри блока, и определяет тип Aff.

Первый оператор в последовательности блока do вызывает функцию get, источник которой вы можете найти здесь. Стрелка, указывающая влево, является частью нашей монадической нотации do, присваивая значение, полученное функцией get, локальной переменной res.

Второй оператор записывает тело ответа в консоль с помощью функции журнала. Оператор ‹› - это псевдоним для функции добавления из Prelude. Поскольку он используется как инфиксный оператор, он применяет значение слева в качестве первого аргумента для добавления, а значение справа в качестве второго аргумента, что делает его эквивалентным следующему:

append "GET /api response: " res.response

В JavaScript это будет выглядеть так:

append('GET /api response: ', res.response)

Если вы хотите использовать такую ​​обычную функцию, а не пользовательский оператор, вам просто нужно заключить ее в обратные кавычки:

"GET /api response: " `append` res.response

А что насчет оператора $, который вы видите в паре мест? Это утилита, которая поможет вам избавиться от громоздких скобок. Эквивалентный оператор без оператора $ будет выглядеть так:

liftEff (log ("GET /api response: " <> res.response))

Оператор $, по сути, говорит: «сначала оцените все справа, а затем примените это к левой стороне».

Наконец, у нас есть функция liftEff. Это берет функцию, предназначенную для работы с эффектами Eff, и «поднимает» ее для работы с эффектами Aff более высокого порядка. Это необходимо, потому что, как мы помним, наш блок do создает Aff из-за вывода типа и использования get внутри блока.

Время для некоторой теории

Хорошо, давайте отойдем от практического применения на сегодня и расскажем подробнее о том, что происходит за кулисами. Начнем с типов!

Хорошо, так что это за вещи?

Типы - это способ добавить подробную структуру к вашему коду и дать компилятору глубокое статическое представление о том, как используется код. Системы звуковых типов позволяют разработчикам жестко контролировать ошибки времени выполнения или, в некоторых случаях, полностью их устранять! Они позволяют использовать чрезвычайно производительные инструменты разработки за счет глубокого статического анализа. Они помогают в рефакторинге с ошибками компилятора «хлебных крошек», которые помогают понять, на что влияют ваши изменения, и позволяют использовать такие мощные методы, как сопоставление с образцом и ограничения классов типов. Однако, возможно, более важным, чем все это, является отличная читаемость, которую обеспечивают выразительные типы. Если вы потратите время на изучение концепций, вы часто сможете получить хорошее представление о том, что делают функции, просто взглянув на их типовые сигнатуры.

Типы в PureScript позволяют использовать абстракции, основанные на теории категорий, которые в значительной степени сосредоточены на композиционном программировании. Теория категорий регулируется математическими законами, которые действуют как ограничения. Эти ограничения позволяют нам выводить абстрактные подробности о категориях, которые следуют этим законам. В сочетании с такими концепциями, как шаблон проектирования функтора, эта стратегия обеспечивает мощную функциональную совместимость, которая ставит совместимость выше стандартов.

Это глубокий предмет, требующий исследований и экспериментов, чтобы полностью понять его, и я не собираюсь делать вид, что прошел этот путь. Я бы порекомендовал окунуться и начать экспериментировать с PureScript, и это понимание со временем начнет расти. Функциональные программисты часто называют это «построением интуиции», потому что это внутреннее понимание часто намного сильнее, чем то, что вы можете выразить словами.

Как насчет побочных эффектов?

Чтобы понять побочные эффекты, вы должны сначала понять чистоту. Чистая функция - это функция, в которой каждый возможный аргумент сопоставляется с одним и тем же возвращаемым значением независимо от того, когда функция вызывается, и нет наблюдаемого эффекта на что-либо за пределами функции. Вы можете думать об этом как о функции без состояния, без понятий «до» и «после». Никакие внешние значения, такие как 'this', 'window', 'console', глобальная переменная или переменная во внешнем замыкании, не могут использоваться внутри функции, потому что это будет означать, что функция наблюдает или влияет на значения за пределами своей собственной области и времени. станет фактором.

Что, если вам нужно время, чтобы сыграть роль? Что, если вам нужно что-то, чтобы иметь состояние, или что-то должно произойти в определенной последовательности? В PureScript существует множество методов обработки состояния с помощью побочных эффектов, таких как монады и аппликативные функторы. Истина, лежащая в основе загадки функции «do», заключается в том, что PureScript использует ее для представления монадических преобразований, которые происходят последовательно, и поэтому для сохранения состояния требуются побочные эффекты. Это может быть внутренний побочный эффект для PureScript или что-то внешнее в среде выполнения. Если вы хотите прочитать или изменить что-то в собственной среде (среде выполнения JavaScript), вы используете тип «Eff». Взаимодействие с этим типом осуществляется через монадический интерфейс, поэтому вы можете использовать с ним нотацию «делать».

Как на все это влияет неизменность? В JavaScript непримитивные аргументы передаются по ссылке. Это означает, что изменение аргументов приводит к изменению этого значения вне функции, делая его нечистым. Вам нужно будет обработать побочный эффект с помощью «Eff». Вы можете представить мутацию значений, полностью инкапсулированных внутри функции, используя такие вещи, как монады.

Где я могу найти дополнительную информацию?

Что ж, на сегодня все! Мы многое рассмотрели, и, если я справился хорошо, у вас, вероятно, будет больше вопросов, чем ответов. Надеюсь, вас вдохновят начать путь к программированию в стиле машинного обучения, и даже если вы не выберете PureScript, вы сможете увидеть преимущества этого подхода.

Если вы хотите узнать больше, вот несколько хороших мест для начала:

Удачи!

Сноска: А как насчет Вяза?

Вам может быть интересно, почему я сейчас не пишу об Эльме. Элм сейчас вызывает большой ажиотаж в сообществе React, и не зря! Он представляет прелести сильной выразительной типизации и синтаксиса в стиле ML для многих из нас, занимающихся фронтенд-инжинирингом, которые никогда даже не задумывались об этом пути. Мне лично очень нравится Elm, и я хочу, чтобы он продолжал расти, но сейчас это не мой любимый язык, потому что он очень ориентирован на то, чтобы быть языком пользовательского интерфейса, который очень доступен для новых разработчиков. Это абсолютно правильный подход для Elm, но я ищу что-то более общее. Чтобы быть конкретным, я предпочитаю PureScript из-за специального полиморфизма через классы типов, системы эффектов Eff, полиморфизма строк и его низкоуровневого интерфейса внешних функций для JavaScript.