Монады в Scala

в 9:03, , рубрики: scala, монады, функциональное программирование, метки: ,

На Хабре много статей по монады с примерами для Haskell (http://habrahabr.ru/post/183150, http://habrahabr.ru/post/127556), но не так много статей, которые описывают, что такое монады с примерами на Scala. По сколько большинство разработчиков на Scala пришли из мира объектно ориентированного программирования, то для них, по началу, сложно понять что такое монады и для чего они нужны, эта статья как раз для таких разработчиков. В этой статье я хочу показать, что это такое и навести примеры использования монады Option, в следующих статьях будут описаны монады Try и Future.

Итак, монада — это параметрический тип данных, который обязательно реализует две операции: создание монады (в литературе функция unit) — и функцию flatMap() (в литературе иногда имеет название bind) и подчиняется некоторым правилам. Применяются они для реализации стратегии связывания вычислений. Приведем пример самой простой монады:

 trait Monad[T] {
    def flatMap[U](f: T => Monad[U]): Monad[U]
  }

  def unit[T](x: T): Monad[T]

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

Что касается функции unit, то она отвечает за создание монады и для каждой монады она отличается. Для примера, функция unit.

для монады Option это Some(x)
для монады List это List(x)
для монады Try это Success(x)

Для каждой монады можно определить функцию map и выразить ее через комбинацю flatMap и unit. Для примера:

def mapExample() {
    val monad: Option[Int] = Some(5)
    assert(monad.map(squareFunction) == monad.flatMap(x => Some(squareFunction(x))))
  }

Также каждая монада должна подчинятся 3 законам, и они должны гарантировать, что монадическая композиция будет работать предсказуемым образом. Проверять эти законы мы будем на монаде Option.

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

  def squareFunction(x: Int): Option[Int] = Some(x * x)

  def incrementFunction(x: Int): Option[Int] = Some(x + 1)

Первый закон имеет название Left unit law и выглядит он так:

unit(x) flatMap f == f(x)

И говорит он, что если применить функцию flatMap для типа с позитивным значением (для Option это Some) и передать туда некоторую функцию то результат будет такой же, как простое применение этой функции к переменной. Это лучше демонстрирует код приведенный ниже:

def leftUnitLaw() {
    val x = 5
    val monad: Option[Int] = Some(x)
    val result = monad.flatMap(squareFunction) == squareFunction(x)
    println(result)
  }

Как и следовало ожидать, результат будет true.

Второй закон имеет название Right unit law и выглядит так:

monad flatMap unit == monad

И говорит он о том, что если передадим в flatMap функцию которая создает монаду из данных (тех что находятся в монаде) — то на выходе мы получаем такую же монаду.

def rightUnitLaw() {
    val x = 5
    val monad: Option[Int] = Some(x)
    val result = monad.flatMap(x => Some(x)) == monad
    println(result)
  }

Функция flatMap раскрывает monad и достает x и передает его в функцию x => Some(x) которая и конструирует новую монаду. Если переменной monad присвоить значение None — то все равно результат будет true, потому что flatMap просто вернет None, и не будет вызывать функцию ей переданную.

Третий закон называется Associativity law:

(monad flatMap f) flatMap g == monad flatMap(x => f(x) flatMap g)

Если записать его на Scala:

  def associativityLaw() {
    val x = 5
    val monad: Option[Int] = Some(x)
    val left = monad flatMap squareFunction flatMap incrementFunction
    val right = monad flatMap (x => squareFunction(x) flatMap incrementFunction)
    assert(left == right)
  }

И этот соблюдение этого закона дает нам право использовать for comprehension в обычном для нас виде, то есть вместо:

for (square <- for (x <- monad; sq <- squareFunction(x)) yield sq;
       result <- incrementFunction(square)) yield result

Мы можем записать:

for (x <- monad;
       square <- squareFunction(x);
       result <- incrementFunction(square)) yield result

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

    def findPort(): Option[Int] = Some(22)

    def findHost(): Option[String] = Some("my.host.com")

    val address: Option[InetSocketAddress] = for {
      host <- findHost()
      port <- findPort()
    } yield new InetSocketAddress(host, port)

    println(address)

Результатом исполнения этого кода будет что-то типа: Some(my.host.com/82.98.86.171:22). Обратите внимание на то, что yield возвращает тоже Option чтобы использовать его для дальнейшего вычисления. Для того чтобы получить сам адрес используем функцию map и выведем результат, если любая из функций в цепочки вычислений вернет None то и общий результат тоже будет None.

address.map(add => println("Address : " + add)).getOrElse(println("Error"))
// Address : my.host.com/82.98.86.171:22

Для практического применения монад для начала следует помнить что flatMap и map никогда не выполнится при отрицательных входных данных (для Option это None). Использование этих функций сильно упрощает борьбу с ошибками.

Автор: DimitrKovalsky

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js