На Хабре много статей по монады с примерами для 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). Использование этих функций сильно упрощает борьбу с ошибками.