Пользователь
0,0
рейтинг
18 января 2014 в 13: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
карма
9,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (14)

  • +3
    Объяснять про монады в Scala без введения понятия typeclass — это несколько необычно.
    Это обобщение будет введено в дальнейшем?

    А вообще не легкий это труд объяснять ФП. Мне в свое время очень помог learning Scalaz.
    • 0
      Спасибо за совет, следующую статью я планирую начать с typeclass и implicit, что-бы дать основу для понимания Future.
  • 0
    Спасибо за статью.

    Нашёл пример:
    scala> val fruits = Seq("apple", "banana", "orange")
    fruits: Seq[java.lang.String] = List(apple, banana, orange)
    
    scala> fruits.map(_.toUpperCase)
    res0: Seq[java.lang.String] = List(APPLE, BANANA, ORANGE)
    
    scala> fruits.flatMap(_.toUpperCase)
    res1: Seq[Char] = List(A, P, P, L, E, B, A, N, A, N, A, O, R, A, N, G, E)
    


    то flatMap — это (=<<) из Хаскеля, bind с аргументами в обратном порядке.
    (=<<) :: Monad m => (a -> m b) -> m a -> m b
    -- Same as >>=, but with the arguments interchanged.
    
    
    > (map toUpper) =<< ["banana","lemon","orange"]
    "BANANALEMONORANGE"
    
    
    • +1
      Да, но изначально в scala эти методы так не позиционируются, как не позиционируется таким образом и SelectMany в LINQ.

      В scalaz же есть и typeclass Bind и метод >>=:

      List("ab", "cd") >>= {_.toUpperCase.toList}
      // List(A, B, C, D)
      


      Сводку (несколько устаревшую и не полную) по typeclass в scalaz можно посмотреть в Scalaz cheatsheet.
    • 0
      Кстати, полагаю вам будет интересно, что в scala есть и аналог do-notation — for-comprehensions:

      for {
        f <- fruits
        //f2 = f.drop(1)
        c <- f.toUpperCase
        //if c != 'A'
      } yield c
      


      Правда есть отличие: последнее выражение в for — это map, а не flatMap.
      • 0
        Что это я? Они же есть в статье.
      • 0
        for-comprehensions — это do-notation или всего лишь list-comprehension?
        • 0
          Требуется только наличие map и flatMap (для использования if требуется withFilter, но if можно просто не использовать). Работает для коллекций, Option, Try и многих других классов. При наличии соответствующей реализации typeclass Monad будет работать для любого класса.
        • +1
          Скорее monad-comprehensions, т.е. то же, что и list-comprehensions, но для произвольной монады.
  • 0
    > Что касается функции unit, то она отвечает за создание монады и для каждой монады она отличается. Для примера, функция unit.

    Насколько я понимаю, в Scala вместо unit используется apply. Таким образом:

    «для монады Option это Some(x)», не совсем верно, скорее Option.apply(x)
    «для монады List это List(x)» – List.apply(x)
    «для монады Try это Success(x)» – Все-таки Try.apply(x), т.к. x тут это анонимная ф-я «x: => T»
    • 0
      Все-таки на Option(x) есть дополнительная логика — если x == null, то вернется None.

      Но если хотите полностью функционального подхода, то в scalaz есть typeclass `Applicative`:

      1.point[Option]
      1.point[List]
      
      import scalaz.contrib.std.utilTry._
      1.point[Try]
      
    • 0
      Совершенно необязательно иметь определенную функцию для unit.
      Она есть в теории, но на практике она в основном by convention, так же она может иметь разные сигнатуры.

      apply — это тоже договоренность, которая просто позволяет применять синтаксис вызова функции.
      Например: вызов List(x) — это вызов функции apply на объекте List.
  • 0
    В вашем примере используется map
    address.map(add => println("Address : " + add)).getOrElse(println("Error"))
    // Address : my.host.com/82.98.86.171:22
    

    хотя, результат этого выражения Unit. Логичнее было бы использовать foreach, или вынести println за скобки.
    println(address.map(add => "Address: " + add).getOrElse("Error"))
    

    • 0
      Да, вы правы, лучше было бы использовать foreach, но так как в статье я упоминал про map, и хотел показать пример именно с ним.

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