На Хабре много статей по монады с примерами для Haskell (http://habrahabr.ru/post/183150, http://habrahabr.ru/post/127556), но не так много статей, которые описывают, что такое монады с примерами на Scala. По сколько большинство разработчиков на Scala пришли из мира объектно ориентированного программирования, то для них, по началу, сложно понять что такое монады и для чего они нужны, эта статья как раз для таких разработчиков. В этой статье я хочу показать, что это такое и навести примеры использования монады Option, в следующих статьях будут описаны монады Try и Future.
Итак, монада — это параметрический тип данных, который обязательно реализует две операции: создание монады (в литературе функция unit) — и функцию flatMap() (в литературе иногда имеет название bind) и подчиняется некоторым правилам. Применяются они для реализации стратегии связывания вычислений. Приведем пример самой простой монады:
Функция
Что касается функции
Для каждой монады можно определить функцию
Также каждая монада должна подчинятся 3 законам, и они должны гарантировать, что монадическая композиция будет работать предсказуемым образом. Проверять эти законы мы будем на монаде Option.
Для начала определим две простые функции, которые будем использовать в для проверки, это поднесения в квадрат и инкремент, они возвращают Option, это сделано для возможности передачи их в flatMap и для дальнейшей композиции.
Первый закон имеет название
И говорит он, что если применить функцию flatMap для типа с позитивным значением (для Option это Some) и передать туда некоторую функцию то результат будет такой же, как простое применение этой функции к переменной. Это лучше демонстрирует код приведенный ниже:
Как и следовало ожидать, результат будет
Второй закон имеет название
И говорит он о том, что если передадим в flatMap функцию которая создает монаду из данных (тех что находятся в монаде) — то на выходе мы получаем такую же монаду.
Функция flatMap раскрывает monad и достает
Третий закон называется
Если записать его на Scala:
И этот соблюдение этого закона дает нам право использовать
Мы можем записать:
И так, все эти законы дают нам то, что мы можем инкапсулирувать логику цепочки вычислений, собственно то для чего и нужны монады если верить Википедии. Это очень хорошо видно при применении монады
Результатом исполнения этого кода будет что-то типа:
Для практического применения монад для начала следует помнить что
Итак, монада — это параметрический тип данных, который обязательно реализует две операции: создание монады (в литературе функция 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
). Использование этих функций сильно упрощает борьбу с ошибками.