Примечание. Это Урок 6 из серии Переход от JavaScript к PureScript. Обязательно прочитайте введение в серию, где я освещаю цели и схему, а также установку, компиляцию и запуск PureScript. Я буду публиковать новый учебник примерно раз в месяц. Так что возвращайтесь почаще, впереди еще много всего!

Индекс | ‹ Урок 5 | Урок 7› Урок 27 ››

Добро пожаловать в Урок 6 из серии Сделайте скачок от Javascript к PureScript. Надеюсь, вам понравились полученные знания. Обязательно прочтите серию Введение, чтобы узнать, как установить и запустить PureScript. Я позаимствовал (с разрешения) схему этой серии и образцы кода javascript из курса egghead.io Профессор Фрисби представляет составной функциональный JavaScript автора Брайан Лонсдорф - спасибо, Брайан! Фундаментальное предположение состоит в том, что вы смотрели его видео, прежде чем приступить к эквивалентной абстракции PureScript, представленной в этом руководстве. Брайан очень хорошо описывает представленные концепции, и вам будет лучше, если вы поймете их реализацию с помощью JavaScript.

Все примеры кода и уценка для этой статьи находятся на Github. Если вы читаете что-то, что, по вашему мнению, может быть объяснено лучше, или пример кода, который требует рефакторинга, сообщите мне об этом в комментарии или отправьте мне запрос на перенос. Наконец, если вам нравится эта серия, пожалуйста, помогите мне рассказать другим, порекомендовав эту статью и / или поддержав ее в социальных сетях.

Символы, операции и законы - о боже!

Я должен признать, что поначалу было сложно написать этот туториал. Я все думал, могу ли я отвлечь некоторых читателей, установив окончательную связь между языками программирования и математикой. Почему? Что ж, кажется, там ходит мем о том, что программирование - это не математика; что для программирования не нужна математика.

Например, я видел недавний твит, связанный с изучением PureScript, где плакат спрашивал в качестве мысленного эксперимента, не лучше ли продвигать более простой PureScript, отказавшись от теории категорий. Например, переименуйте категории во что-нибудь более удобное для пользователя, например Monoid => AssociativeConcattableWithIdentity. Я считаю, что такое мышление неверно! Если мы не применяем математику в программировании, мы отказываемся от самого мощного инструмента для понимания того, что мы делаем.

Вместо этого нам нужно намного лучше объяснить связь между языками программирования и математикой. К счастью, время от времени кто-то понимает это правильно, просто обучая этому, так что практически любой, кто хочет учиться, мог учиться. И когда они это сделают, мы должны продвигать этих людей, воспевая им хвалу с самых высоких гор. Итак, вот мой кандидат в зал славы Сделай это просто, но не проще. Его зовут Крис Тейлор @cmtaylor, и он выступил с прекрасным докладом под названием Алгебра алгебраических типов данных. Если вы не видели его выступление, вам следует отложить на мгновение мое руководство и посмотреть его.

В своем выступлении он объясняет, что языки программирования образуют алгебру, мало чем отличную от алгебры, которую мы изучали в школе. Мы узнали, что символы - это 0, 1, 2, x, y, z и т. Д .; операции: +, -, *, ÷ и т.д .; а законы равны 0 + x = x и т. д. Итак, как это перевести на PureScript? Что ж, в PureScript типы - это алгебра. Символы - это сами типы (Int, Boolean, String и т. Д.). Операции - это конструкторы типов (Maybe, Either и т. Д.). И законы. . . , ну, я не знаю, каковы все законы алгебры PureScript. Но мы, безусловно, можем взглянуть на более мелкие подмножества языка, что является моим длинным и сложным способом представить тему этого руководства - Полугруппы!

Полугруппы происходят из абстрактной алгебры

Ты все еще со мной? Повторяю то, что я написал во введении, не позволяйте этому или любому другому математическому имени отпугнуть вас от изучения функционального программирования. Если мы сохраним имена (например, Semigroup), тогда мы получим выгоду от большого количества информации, полученной из математики, а именно типов и их законов! Если мы понимаем типы (например, список, строка, int и т. Д.) И их законы (например, ассоциативность, идентичность и т. Д.), Это очень помогает нам рассуждать о нашей программе.

Полугруппа - это тип вместе с бинарной операцией <>, удовлетворяющий закону ассоциативности. Если мы распакуем это определение дальше, бинарная операция берет два элемента из нашего типа и производит другой элемент того же типа. Более того, если вы подумаете достаточно долго, вы увидите, что бинарная операция - это всего лишь метод композиции функций. Мне нравится думать о бинарных операциях, таких как конкатенация, когда мы смешиваем два объекта вместе, чтобы получить третий объект. В PureScript это называется append с (<>) в качестве инфиксного оператора. Обратите внимание, что Брайан использует concat в своих примерах, но, чтобы уменьшить когнитивную нагрузку, я буду использовать append как в JavaScript, так и в PureScript.

Хорошие примеры <> в Полугруппах включают сложение, умножение. Закон ассоциативности гласит, что если заданы элементы a, b и c нашего типа, то a <> (b <> c) = (a <> b) <> c. Заметьте, я не включил вычитание! Это не Полугруппа, потому что она не удовлетворяет закону ассоциативности. Это означает, что a - (b - c) не равно (a - b) - c. Тип строки и его бинарная операция добавления - еще один хороший пример полугруппы. Проверяя, выполняется ли закон ассоциативности, мы обнаруживаем, что ("foo" <> "bar") <> "baz" равно "foobarbaz", а "foo" <> ("bar" <> "baz") также равно "foobarbaz".

Все просто, правда? Надеюсь, вы теперь задаетесь вопросом, из-за чего весь этот шум. Так что, покончив с этим, я считаю, что мы готовы представить эквиваленты PureScript для фрагментов кода JavaScript из учебника Брайана.

Примеры типов с полугруппами

Давайте остановимся на добавлении в качестве нашей бинарной операции или метода композиции функций (как вам нравится) для следующих примеров. Ниже мы создаем наш первый конструктор типа Sum, который берет два элемента одного типа и складывает их вместе, чтобы создать другой элемент того же типа.

Сумма

// JavaScript
const Sum = x => 
({ 
   x, 
   append: ({ x: y }) => 
     Sum(x + y), 
     inspect: () => 'Sum(${x})' 
}) 
const res1 = Sum(1).append(Sum(2)) // Sum(3)
-- PureScript
newtype Sum a = Sum a 
instance showSum :: Show a => Show (Sum a) where 
  show (Sum x) = "(Sum " <> show x <> ")" 
instance semigroupSum :: Semiring a => Semigroup (Sum a) where
  append (Sum a) (Sum b) = Sum (a + b) 
derive newtype instance eqSum :: Eq a => Eq (Sum a) 
main :: Effect Unit
main = do 
  log "Integers:" 
  logShow $ (Sum 1 <> Sum 2) <> Sum 3 -- (Sum 6)
  log "Associativity law:" 
  logShow $ (Sum 1 <> Sum 2) <> Sum 3 == 
             Sum 1 <> (Sum 2 <> Sum 3) -- true

В своем учебнике по полугруппам Брайан использует деконструкцию в методе добавления Sum, чтобы выявить другую сумму y. Если вам непонятен пример кода JavaScript, просмотрите его руководство. Более интересным (да, я предвзято) является пример PureScript. Мы определяем наш конструктор newtype Sum и три экземпляра класса type, которые мы используем для отображения, добавления и подтверждения ассоциативности. Я рассмотрел конструкторы newtype в моем самом первом уроке. Но если уточнить, они являются частным случаем алгебраических типов данных, которые определяют только один конструктор, и этот конструктор должен принимать ровно один аргумент. В этом примере Sum - это конструктор, а его единственный аргумент - универсальный тип a. Я выбрал общий тип, потому что мы можем добавлять несколько типов чисел, например целые числа, числа с плавающей запятой и натуральные числа. Затем мы определяем объявления наших экземпляров.

Сначала идет класс типа Show, который сообщает компилятору PureScript, как мы хотим отображать наши результаты. Используя деконструкцию типов, мы извлекаем значение x из конструктора и оборачиваем его. Далее идет наш Semigroup, который объявляет сложение как наш метод объединения двух элементов типа вместе для получения третьего элемента. Кроме того, я ввел ограничение класса типа, которого мы не видели до сих пор - Semiring. Здесь снова PureScript использует четко определенный термин из абстрактной алгебры, который описывает элементы типа, которые вы можете складывать или умножать вместе. И, наконец, вопрос об ассоциативности. Чтобы доказать ассоциативность Sum, я извлекаю экземпляр класса типа Eq (т. Е. Равенство), чтобы показать в моих примерах, что левая и правая части моего выражения равны. То есть (Sum 1 <> Sum 2) <> Sum 3 == Sum 1 <> (Sum 2 <> Sum 3).

Все

Эта следующая полугруппа позволяет нам смешать два логических типа вместе, чтобы получить третье логическое значение - true && false = false. Но при создании настраиваемого конструктора Semigroup убедитесь, что вы проверили закон ассоциативности. Ну, (true && false) && true == true && (false && true), так что я думаю, что мы кое-что нашли. Давайте определим наш следующий конструктор типа Semigroup, используя логические значения, и назовем его All.

// JavaScript
const All = x => 
({ 
   x, 
   concat: ({x: y}) => 
     All(x && y), 
   inspect: () => 
     'All(${x})' 
})
const res = All(true).concat(All(false)) // All(false)
-- PureScript
newtype All a = All a 
instance showAll :: Show a => Show (All a) where 
  show (All x) = "(All " <> show x <> ")" 
instance semigroupAll :: BooleanAlgebra a => Semigroup (All a) where
  append (All a) (All b) = All (a && b) 
derive instance eqAll :: Eq a => Eq (All a) 
main :: Effect Unit
main = do 
  logShow $ All true <> All false -- (All false)
  log "Associativity law:" 
  logShow $ (All true <> All true) <> All true == 
             All true <> (All true <> All true) -- true

Нет большой разницы между новыми типами All и Sum и их объявлениями экземпляров. Но если вы заблудились, прочтите раздел Sum еще раз и не стесняйтесь оставлять вопрос в комментариях. Мы просто заменили числа на логические, и наша операция добавления стала && вместо +. Хотя есть это ограничение класса нового типа BooleanAlgebra. Ну, это просто еще один математический термин для описания типов, которые ведут себя как логические значения. Мне не терпится получить легкие победы, и, по сравнению с Semigroup и Semiring, я считаю, что BooleanAlgebra намного легче запомнить.

Первый

И наконец, давайте создадим несколько странную Полугруппу, которую мы назовем First. Помните, что строки - это полугруппа, где "a" <> "b" <> "c" = "abc". Но что, если бы наша операция добавления была построена так, чтобы сохранять только первый аргумент? Это First "a" <> First "b" <> First "c" = First "a". Это все еще является Полугруппой? Проверим это, применив закон ассоциативности - (First "a" <> First "b") <> First "c" == (First "a" <> First "b") <> First "c" == First "a". Да, я думаю, что у нас есть Semigroup, так что давайте его закодируем.

// JavaScript
const First = x => 
({ 
   x, 
   concat: _ => First(x), 
   inspect: () => 'First(${x})' 
})
const res =  First("a").concat(First("b")).concat(First("c")) // First("c")
-- PureScript
newtype First a = First a
instance showFirst :: Show a => Show (First a) where 
  show (First x) = "(First " <> show x <> ")" 
instance semigroupFirst :: Semigroup (First a) where 
  append (First a) _ = (First a) 
derive instance eqFirst :: Eq a => Eq (First a)
main :: Effect Unit 
main = 
  logShow $ First "a" <> (First "b" <> First "c") -- (First "a")
  log "Associativity law:" 
  logShow $ (First "a" <> First "b") <> First "c" ==
             First "a" <> (First "b" <> First "c") -- true

Здесь снова нет большой разницы между объявлениями First и Sum newtype и instance. Мы просто заменили числа на общий тип, будь то строка, число или яд. И наша операция добавления игнорирует все аргументы, кроме первого.

Полугрупповые модули в PureScript

Не знаю, как вы, но я был бы ужасно разочарован, если бы в PureScript не было некоторых из наиболее полезных полугрупп, уже находящихся в модуле, заблокированных и загруженных со всеми экземплярами его классов и ограничениями типов, готовыми к работе. Что ж, не ищите дальше Data.Monoid.Additive для Additive x <> Additive y == Additive (x + y). А что с Data.Monoid.Conj для Conj x <> Conj y == Conj (x && y)? Выглядит знакомо? Они просто замаскированные 46 и 47. Еще есть First от Data.Maybe.First. Мы еще не рассмотрели конструктор типа Maybe, но скоро мы его рассмотрим, и я думаю, вы найдете его полезным в своем коде. Так что посмотри.

Вы можете спросить, почему именно Data.Monoid.XX вместо Data.Semigroup.XX? Что ж, нам нужно сохранить полный ответ для другого урока. Но вот подсказка - все снова о законах, а именно о законе об идентичности.

Резюме примечания

Некоторые из вас могут подумать, что по сравнению с JavaScript или другими нетипизированными языками объявления типов в PureScript слишком многословны. Да, в PureScript действительно требуется больше строк для выражения типа Semigroup. Но опять же, цель состоит в том, чтобы будущие сопровождающие вашего кода могли легко рассуждать о вашей программе, желательно в пределах времени, необходимого для того, чтобы выпить чашку кофе (максимум две чашки). Прежде чем тратить один драгоценный такт процессора на выполнение, я думаю, что лучше использовать типы в полной мере, чтобы доказать правильность вашего решения. Так что, если вы потратите время на изучение Semigroups, Semirings и BooleanAlgebra, то эти объявления типов помогут вам на пути к достижению этой цели.

В следующем уроке мы рассмотрим несколько примеров определений Semigroup, подобных примерам Either в Урок 5. Если вам нравится эта серия, пожалуйста, помогите мне рассказать другим, рекомендуя эту статью и размещая ее в социальных сетях. Спасибо и до следующего раза!