Всегда полезно прочитать содержательную книгу, особенно если она посвящена вашему любимому языку программирования. Что касается Scala, есть много замечательных книг, посвященных продвинутым концепциям, таких как «Функциональное программирование в Scala» (также известное как «красная книга»), «Scala with Cats», «The Type Astronaut's Guide to Shapeless» и, конечно же, «Функциональное программирование для смертных».

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

Я выбрал «Функциональное программирование для смертных» в качестве основы для этого списка, поскольку, будучи одной из первых книг, которые я прочитал о Scala, она предоставила мне все ключевые понятия функционального программирования, которые я затем смог подробно изучить позже. на. Все имена и ссылки на код взяты из версии книги, в которой используется библиотека scalaz, хотя я буду предлагать альтернативы из библиотеки cats, где это возможно. Перечисленные отрывки не являются буквальными цитатами из книги, но к ним прилагаются страницы, если вы хотите изучить соответствующие темы из книги более подробно.

Глава 1 Введение

Page 5. Работа с монадическими типами позволяет абстрагироваться от контекстов последовательного выполнения, которые могут обеспечивать различные возможности обработки ошибок, хранения состояний и аудита. Некоторые базовые примеры монадических типов включают Option, List и Either из стандартной библиотеки Scala. См. Раздел 5.7 для получения более подробной информации о том, что такое монада.

Page 6. Функциональное программирование в основном работает с чистыми функциями. Они есть:

  • Итого - возвращает значение для каждого возможного ввода (исключения не считаются возвращаемым значением).
  • Детерминированный - возвращаемые значения никогда не меняются для одних и тех же входных данных.
  • Локально - они не изменяют состояние, видимое вызывающему.

Чистые функции позволяют создавать ссылочно прозрачные выражения, которые можно заменять непосредственно их возвращаемым значением без изменения поведения программы. Например, (5 + 5) можно заменить на 10 в любом месте кода.

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

Глава 3 - Дизайн приложения

Page 25. алгебра определяет набор (возможно) побочных взаимодействий определенной части системы. Этот набор часто имеет форму признака, определяющего необходимые методы с параметром более высокого порядка F[_], который обычно называется эффектом в контексте, в котором указанные взаимодействия осуществляются место .

Page 27. модуль - это реализация алгебры, которая абстрагирована над F[_] и зависит только от других модулей, алгебр и чистых функций, обычно с рядом ограничений класса типов на F[_]. Реализация алгебры, привязанная к конкретному F[_], называется интерпретатором. Подробнее о том, что такое класс типов, см. В части 4.2.

Page 36. Интерпретаторы тестов или конкретные реализации F[_] для целей тестирования позволяют безопасно заменять части системы, вызывающие побочные эффекты, что приводит к более высокому покрытию тестами. Модули обычно сложнее писать, чем интерпретаторы, но они обеспечивают большую гибкость при написании тестов.

Глава 4 - Данные и функциональность

4.1 Данные

Page 38. В функциональном программировании данные обычно представлены алгебраическими типами данных (ADT), которые являются собирательным названием для следующих элементов:

  • (финальные) case-классы, поля которых являются ADT. Это так называемые продукты.
  • запечатанные черты, все члены которых являются ADT. Это так называемые вторичные продукты.
  • значения, состоящие из case-объектов, «примитивов» (например, Integer, String) и чистых функций.

С логической точки зрения, продукты соответствуют логическому соединению («имеет свойство A, свойство B, свойство C») с копроизведениями, представляющими дизъюнкцию («имеет тип A, OR тип B, OR тип C»).

ADT с параметрами типа известны как обобщенные алгебраические типы данных (GADT). ADT также могут быть рекурсивными , ссылаясь на самих себя в их определении.

Page 39. Важно подчеркнуть, что ADT, содержащие функции, могут плохо транслироваться в виртуальную машину Java (JVM), если они полагаются на такие методы, как equals, или интерфейсы, например, Serializable. Хорошая практика - оставлять определения ADT как можно более чистыми и избегать использования встроенных методов JVM.

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

Page 42. Иногда включение примитивных типов в ADT в том виде, в каком они есть, позволяет представить недопустимое состояние - типичным примером этого является использование целых чисел для поля, которое должно допускать только положительные значения. Это можно преодолеть, определив обертки значений, которые применяют набор ограничений к необработанным значениям. Такие оболочки обычно известны как усовершенствованные типы (см. Примеры в библиотеке усовершенствованные).

Page 44. ADT не должны предоставлять какие-либо методы, как обычные классы или черты, они просто контейнеры для данных.

Не предоставляя никаких функций, ADT могут иметь минимальный набор зависимостей, что упрощает их публикацию и совместное использование с другими разработчиками.

Количество значений, которые могут существовать для данного типа, называется его сложностью.

Page 45. Снижение сложности входов и выходов функции помогает поддерживать свойство целостности. Желательно, чтобы выходная сложность была меньше, чем входная.

Page 46. Во многих случаях сложность ADT можно уменьшить, переписав продукты, чтобы они стали сопродуктами, чтобы исключить нежелательное (недопустимое) состояние.

4.2 Функциональность

Page 49. Функциональное программирование обычно полагается на классы типов в порядке для обеспечения полиморфной функциональности, в отличие от наследования в объектно-ориентированном программировании. Классы типов - это черты, которые:

  • Не держите государство.
  • Содержат параметр типа - тип, для которого должен быть предоставлен класс типов. Этот тип может быть высшим.
  • Имейте хотя бы один абстрактный метод.

Кроме того, классы типов могут:

  • Содержат методы с реализациями по умолчанию.
  • Расширить другие классы типов.

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

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

Глава 5 - (Скалаз) Классы типов

5.2 Дополнения

page 70. Semigroup[A] - определяет ассоциативную операцию «комбинирования» для данного типа. Например, строки имеют Semigroup с операцией конкатенации.

Monoid[A] - Semigroup, который также имеет понятие пустого элемента, который не действует в сочетании с другими элементами. Для строк это будет пустая строка.

page 71. Band[A] - Semigroup с операцией идемпотент, которая должна возвращать один из своих аргументов при объединении двух идентичных значений. Хотя интерфейс ничем не отличается от Semigroup, он необходим для выражения намерения. Простым примером Band могут быть операции «комбинирования» для наборов любого фиксированного типа.

5.3 Объективные вещи

page 74. Equal[A] - определяет операцию «равенства» между членами данного типа. Это должно быть:

  • Коммутативный - a === b b === a.
  • Возвратный - a === a.
  • Переходный - a === b, b === ca === c.

page 75. Order[A] - расширяет Equal операциями сравнения, в первую очередь <=. В основном используется для абстрактной логики сортировки.

Enum (Discrete в cats-collections) - определяет «преемников» и «предшественников» для каждого элемента. Для любого целого числа i его преемник и предшественник будут i + 1 и i-1 соответственно.

page 76. Show[A] - обеспечивает удобочитаемое представление для данного типа. По сути, это замена метода toString(), устраняющая необходимость его переопределения.

5.4 Отображаемые вещи

page 77. Functor[F] - определено для более высокого типа F[_] позволяет изменять значения внутри F, оставляя сам F нетронутым. Это достигается с помощью операции map, которая при заданной функции f: A => B преобразует F[A] в F[B]. List, Option и т. Д. Имеют функторы.

Все функторы должны удовлетворять следующим свойствам:

  • Состав: fa.map(f).map(g) === fa.map(f andThen g).
  • Сохранение личности: fa.map(identity) === fa.

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

page 80. Foldable[F] - позволяет уменьшить значения типа F[A] для любого фиксированного A до единственного значения типа A. Обратите внимание, что Foldable учитывает потенциально бесконечные структуры с помощью метода foldRight, который требуется в его реализациях.

page 85. Traverse[F] - преобразует любые F[A] в G[F[B]] с учетом функции f: A => G[B], когда G имеет экземпляр Applicative. Это также позволяет реализовать метод sequence: F[G[A]] => G[F[A]]. Один из наиболее распространенных вариантов использования - это преобразование List[Future[A]] в Future[List[A]] с помощью специального Future.sequence метода в стандартной библиотеке. См. Раздел 5.7 для получения более подробной информации о Applicative.

page 87. Align[F] - Functor, который позволяет align любые F[A] и F[B] до F[Ior[A, B]], где Ior - это структура, содержащая либо A значений, B значений, или и то, и другое. Например, выравнивание двух списков приведет к новому списку, в котором в каждом индексе элемент представляет собой либо «заархивированную» пару элементов исходных списков, если индекс присутствует в обоих списках, либо единичный элемент, если он присутствует только в одном. из них.

5.5 Разница

page 89. InvariantFunctor[F] (Invariant[F] в cats) очень похож на Functor[F], но его метод xmap также требует обратного преобразования g: B => A. Экземпляр InvariantFunctor[Semigroup] позволит определять новые полугруппы, если доступно двунаправленное отображение между элементами.

ContravariantFunctor[F] (Contravariant[F] в cats) - преобразует F[A] в F[B], если указан f: B => A. Контравариантные функторы, хотя и не сразу интуитивно понятны, работают с аргументами функций. Например, легко создать контравариантный функтор для типа Encoder[A] с помощью функции encode: A => String.

Используя эту терминологию, Functor следует называть CovariantFunctor, но первое слово обычно опускается для простоты.

5.6 Применить и привязать

page 91. Apply[F] - расширяет Functor на ap: F[A => B] => F[A] => F[B]. Цель этой функции может быть не сразу ясна, но она в основном обеспечивает возможность параллельного выполнения: поскольку F[A => B] и F[A] полностью независимы, порядок их оценки не имеет значения, если в конце они объединены. Конечно, сам ap редко встречается, но методы, производные от него, предоставляют богатый API для параллельных вычислений.

page 94. Bind[F] (FlatMap[F] в cats) - добавляет метод bind: F[A] => (A => F[B]) => F[B] (также известный как flatMap) в Apply, который позволяет «сглаживать» вложенные F структуры. Обратите внимание, что, в отличие от ap, bind коррелирует с последовательным выполнением, а первое требуется только для поддержки последовательного поведения.

5.7 Аппликатив и монада

page 97. Applicative[F] - предоставляет Apply возможность поднимать произвольные значения в F контекст с помощью метода pure: A => F[A]. Главный закон для Applicative - это закон идентичности: pure(identity).ap(fa) === fa, где identity - унарная функция, возвращающая свой аргумент. Этот закон подразумевает, что значения, созданные методом pure, ничем не отличаются от других F[_] значений и не влияют на них.

page 98. Monad[F] представляет собой комбинацию Bind[F] (FlatMap[F]) и Applicative[F]. Он не предоставляет никаких дополнительных методов, но вместо этого налагает дополнительные ограничения в виде следующих 3 законов:

  • Левый тож - pure(a).bind(f) === f(a).
  • Право тож - fa.bind(pure(_)) === fa.
  • Ассоциативность - fa.bind(f).bind(g) === fa.bind(a => f(a).bind(g)).

Примечания автора:

  • Различие между Applicative и Monad существует, поскольку FlatMap диктует последовательное поведение, поэтому некоторые типы не предоставляют Monad экземпляров в пользу поддержки параллельного выполнения (см. cats.data.Validated).
  • Различие между Apply / Applicative и Bind (FlatMap) / Monad существует, поскольку не все типы могут предоставлять метод pure. Например, невозможно создать законный pure метод для F[A] = Map[String, A].

5.8 Разделяй и властвуй

page 100. Divide[F] - контравариантный аналог Apply с методом divide: F[A] => F[B] => (C => (A, B)) => F[C]. Как следует из определения, он предназначен для использования с типами в контравариантных позициях (например, с аргументами функций). Например, Divide[Equal] можно использовать для создания новых экземпляров Equal для продуктов, элементы которых уже имеют такие экземпляры.

page 101. Divisible[F] - контравариантный аналог Applicative (расширяет Divide), который позволяет создавать универсальные количественные (тривиальные) экземпляры F с conquer[A]: F[A]. Любой сериализатор может иметь экземпляр Divisible с conquer, создающим сериализаторы для пустой структуры.

5.9 Плюс

page 102. Plus[F] (SemigroupK[F] в cats) - универсальный количественный аналог Semigroup, который позволяет «добавлять» два экземпляра F[A] для любого типа A. Например, Plus[List] позволяет объединить любые два списка.

PlusEmpty[F] (MonoidK[F] в cats) - расширяет Plus (SemigroupK) возможностью создавать «пустые» значения F[A] для любых A. Для списков соответствующий метод создаст пустой список.

page 104. ApplicativePlus[F] (Alternative[F] в cats) - расширяет Applicative и PlusEmpty (MonoidK), требуя соблюдения нескольких дополнительных законов. Этот класс сам по себе ничего не добавляет, но очень удобен, когда требуются как конкретные, так и универсально определенные реализации. В документации Cats есть отличный пример.

MonadPlus[F] - расширяет Monad и ApplicativePlus и предоставляет два дополнительных метода:

  • unite[T[_]: Foldable, A](ts: F[T[A]]): F[A] - позволяет исключить произвольные вложенные структуры, если они имеют экземпляр Foldable.
  • withFilter[A](fa: F[A])(f: A => Boolean): F[A] - применяет фильтр, описанный функцией f, к F[A], возможно, ленивым образом.

Прямого аналога MonadPlus в cats нет, но тот же набор методов предоставляется FunctorFilter[F] (withFilter) и Alternative[F] (для unite требуется экземпляр Monad[F]).

5.10 Одинокие волки

page 106. Zip[F] (Semigroupal[F] в cats) - менее мощный аналог Divide, который предоставляет метод zip[A](fa: F[A], fb: F[B]): F[(A, B)], объединяющий два экземпляра F в один. zip называется product в cats.

Unzip[F] - противоположность Zip с методом unzip[A](fab: F[(A, B)]): (F[A], F[B]), который позволяет разделить кортеж, встроенный в эффект F, для разделения эффектов. Включено в Functor в cats.

5.11 Совместные вещи

page 109. Cobind[F] (CoflatMap[F] в cats) - Functor с дополнительной операцией cobind(fa: F[A])(f: F[A] => B): F[B] (coflatMap).

page 110. Comonad[F] - расширяет Cobind (CoflatMap) с помощью copoint(fa: F[A]): A (extract), что в основном устраняет структуру вокруг любого значения.

Хорошей иллюстрацией Comonad является клеточный автомат, который вычисляет ячейки нового поколения F[B] на основе их индивидуального окружения F[A] и позволяет извлекать значение каждой ячейки.

Глава 6 - (Scalaz) Типы данных

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

6.4 Добавление тегов

page 127. Пометка адресов, имеющих несколько экземпляров класса типов для одного и того же типа - добавление тега к типу по существу создает новый без каких-либо затрат времени выполнения и предотвращает конфликты неявных экземпляров. Не путать с изысканными типами.

6.5 Естественные преобразования

page 130. Естественное преобразование - это функция конструкторов типов F[_] и G[_], которая определяет универсальное количественное отображение: F[A] => G[A] для всех возможных типов A. Обычный реальный пример естественного преобразования - это переход от контекста выполнения транзакции базы данных F к контексту выполнения приложения G.

6.6 Изоморфизмы

page 131. Изоморфизм определяет отношение равенства между двумя типами (или конструкторами типов) A и B, что означает, что для каждого объекта типа A существует один и только один равный объект типа B и наоборот. Изоморфизмы особенно полезны для автоматического вывода экземпляров

6.7 Контейнеры

page 138. Validation[E, A] - аналог Either[E, A], позволяющий накапливать ошибки. Это называется аппликативным поведением, поскольку Validation не имеет Monad экземпляра. Validation полезен, когда одновременно может произойти несколько ошибок, и все они должны распространяться вверх по течению, а не только одна (поведение Either).

page 141. Исторически создание исключений на JVM происходит довольно медленно из-за ресурсов, необходимых для построения трассировки стека. Факты показывают, что использование Either или Validation может быть в тысячи раз быстрее, чем использование исключений для случаев сбоя.

These[A, B] (Ior[A, B] в cats) представляет тип, который имеет значения либо A, B, либо оба из них, аналогично включающему логическому ИЛИ. Он смещен в сторону типа B, идентичного Either или Validation, и может использоваться для накопления частичных ошибок или предупреждений.

Примечания автора:

Интересно, что генерация трассировки стека настолько ресурсоемка, что существует специальная опция оптимизации, позволяющая в определенных случаях опускать их.

page 145. Const[A, B] - тип, который имеет постоянное значение типа A, действующее как контекст для B. Это очень полезно в программах, абстрагированных по F[_], для сбора метаданных или статистических данных в объекте типа A и обеспечивает аппликативное поведение для любого A, имеющего Monoid экземпляр.

6.8 Коллекции

page 150. Corecursion начинается с базового состояния и детерминированно генерирует последовательность (возможно, бесконечную) новых состояний, в отличие от рекурсии, которая сводит конечную последовательность шагов к базовому состоянию. Это может быть особенно полезно при работе с Comonad.

Глава 7. Продвинутые монады

7.1 Всегда в движении - будущее

page 173. Future из стандартной библиотеки Scala нетерпелив, что означает, что выполнение начинается, как только создается экземпляр Future. Поскольку Future объединяет определение вычисления и его выполнение, становится неудобно планировать и комбинировать их. Кроме того, это означает, что Future нечисто, поскольку его экземпляр может иметь разные значения в зависимости от того, завершено ли вычисление.

Future также несколько недостатков производительности: закрытие вычисления передается исполнителю каждый раз, когда вызывается flatMap, что потенциально может переключать потоки и контекст выполнения. Это может привести к тому, что значительная часть времени ЦП будет тратиться на планирование и сделать последовательную работу быстрее, чем при распараллеливании с Future.

7.2 Эффекты и побочные эффекты

page 174. Поскольку функции в функциональном программировании должны быть чистыми и, следовательно, не могут иметь побочных эффектов, возникает вопрос, где и как их выполнять. Традиционный ответ FP на этот вопрос - монады ввода-вывода, которые приостанавливают вычисления до их явного запуска. Такие монады явно указывают на побочные эффекты в возвращаемом типе и поэтому обычно называются просто эффектами.

Примечания автора:

В частности, наиболее часто используемые монады ввода-вывода на момент написания - это cats.effect.IO, monix.eval.Task и zio.ZIO.

7.3 Безопасность стека

page 175. Компилятор Scala позволяет создавать безопасные для стека хвостовые рекурсивные методы. И scalaz, и cats расширяют этот контекст до монад с BindRec и StackSafeMonad соответственно, для вызовов bind и flatMap которых должно требоваться только постоянное пространство стека.

Примечания автора:

Безопасность стека встроена в Monad из cats, поскольку требуется предоставить реализацию метода tailRecM. StackSafeMonad предоставляется для ясности.

page 176. Алгебраические типы данных могут использоваться для кодирования приостановленных вызовов функций с частями продуктов, соответствующими аргументам функции. Это известно как церковная кодировка. Он позволяет создавать стековые интерпретаторы для закодированных функций за счет дополнительного места в куче, необходимого для хранения объектов ADT. Такие интерпретаторы часто основаны на конструкции, называемой Free (монада), которая представляет собой ADT-представление интерфейса Monad.

page 179. Несмотря на то, что Free может показаться заметно хуже, чем такое же решение, не использующее объекты для представления вычислений, это может быть не так на современных архитектурах и последних версиях JVM. Совет: всегда проводите сравнительный анализ, прежде чем выносить окончательное решение.

7.4 Библиотека преобразователей монад

page 180. Преобразователи монад обеспечивают дополнительные эффекты поверх других монад. Что наиболее важно, их можно составлять вместе для получения сложных эффектов. Библиотеки, предоставляющие преобразователи монад, обычно известны как библиотеки преобразователей монад (MTL).

Некоторые из наиболее часто используемых трансформаторов:

  • OptionT[F[_], A] - накатывает F[Option[A]].
  • EitherT[F[_], E, A] - обертывает F[Either[E, A]].
  • ReaderT[F[_], A, B] - обертывает A => F[B] (также известный как Kleisli).
  • WriterT[F[_], W, A] - заворачивает F[(W, A)].
  • StateT[F[_], S, A] - обертывает S => F[(S, A)].

Примечания автора:

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

page 186. При обработке ошибок с помощью EitherT или его эквивалента в классе типов MonadError мнения расходятся относительно того, какой тип ошибки должен быть. В основном есть аргументы в пользу того, что это ADT ошибок, String или Throwable. Предлагается использовать гибкие структурированные форматы, такие как JSON, чтобы ошибки были расширяемыми, но не слишком непрозрачными.

page 192. Хотя WriterT можно использовать для реализации ведения журнала, часто утверждают, что для ведения журнала должна потребоваться собственная алгебра, поскольку из соображений производительности с ним обычно обрабатывают по-другому.

page 202. ContT позволяет связывать вычисления с использованием продолжений, также известного как «стиль обратного вызова». Хотя объединение в цепочку само по себе не особенно полезно, поскольку оно также может быть выполнено с помощью монад, ContT позволяет переупорядочить поток управления так, чтобы он стал нелинейным, подобно графику.

7.5 Бесплатный обед

page 212. JIT-компилятор JVM позволяет простым функциям иметь производительность, сравнимую с их эквивалентами C / C ++, потенциально даже устраняя эффекты сборки мусора. Однако его возможности по-прежнему ограничены низкоуровневой оптимизацией.

Структура данных, описывающая программу с точки зрения концепций высокого уровня на мета-уровне, позволила бы применять оптимизацию высокого уровня. Такие конструкции обычно называются Free структурами и могут быть легко сгенерированы для алгебраических интерфейсов программы.

page 218. Создание показателей и анализ времени выполнения - одни из немногих случаев, когда возможен прямой побочный эффект. Если такой мониторинг не влияет на саму программу, свойство ссылочной прозрачности все еще сохраняется.

Заключение

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

Я хотел бы поблагодарить автора оригинальной книги, Сэма Холлидея, и всех людей, стоящих за библиотеками scalaz, cats и zio, за всю их работу над экосистемой Scala, которая в итоге вдохновила меня на написание этой статьи.