Что означают операции с картой и плоской картой для ReaderMonad

Я новичок в скале. Я родом из Явы. Я читал монады и сформировал общее представление об этом. Хотя я могу оценить map а также flatMap операции над такими типами, как List я не могу обернуть голову вокруг того, что они имеют в виду, когда дело доходит до reader monads. Может кто-нибудь, пожалуйста, приведите несколько простых примеров.

Я понимаю, что нам нужны ReaderMonads для облегчения унарной композиции функций, чтобы мы могли использовать причудливый синтаксис, такой как - для понимания. Я также понимаю, что нам нужно, чтобы боги монад были удовлетворены, чтобы это произошло. Все, что я хочу понять, это "Что означает карта и плоская карта" для функций?

1 ответ

Решение

Читатель монада, часто пишется Reader[A, B], это просто тип функции A => B, Кодировка монад в Scala выглядит следующим образом:

trait Monad[M[_]] {
  def pure[A](a: A): M[A]
  def flatMap[A, B](ma: M[A])(f: A => M[B]): M[B]
}

где map может быть реализовано с точки зрения pure а также flatMap вот так:

def map[A, B](ma: M[A])(f: A => B): M[B] = flatMap(ma)(a => pure(f(a)))

Итак, первое, что нам нужно сделать, это сделать наш конструктор двоичного типа Reader вписывается в конструктор унарного типа, который Monad надеется. Это делается путем фиксации первого (входного) параметра типа и оставления второго (выходного) параметра типа свободным.

implicit def readerMonad[X]: Monad[X => ?] = ???

(Использование ? вот через замечательный плагин компилятора вида-проектор).

Начиная с pureМы заменяем события M[_] с X => _,

def pure[A](a: A): X => A

Учитывая некоторое значение типа Aмы должны вернуть функцию с заданным значением типа X, возвращает значение типа A, Единственное, что может быть, это постоянная функция.

def pure[A](a: A): X => A = _ => a

Сейчас подставляю flatMap..

def flatMap[A, B](ma: X => A)(f: A => (X => B)): X => B = (x: X) => ???

Это немного сложно, но мы можем использовать типы, чтобы вести нас к реализации! У нас есть:

ma: X => A
f: A => (X => B)
x: X

И мы хотим получить B, Единственный способ, которым мы знаем, как это сделать, это через f, который хочет A и X, У нас ровно один X от x, но нам нужен A, Мы видим, что можем получить только A от ma, который хочет X, который снова только x предоставляет нам. Поэтому у нас есть..

def flatMap[A, B](ma: X => A)(f: A => (X => B)): X => B =
  (x: X) => {
    val a = ma(x)
    val b = f(a)(x)
    b
  }

Читай вслух, flatMap на Reader говорит, чтобы прочитать какое-то значение A вне "среды" (или "конфигурации") X, Затем ветвление на Aпрочитайте другое значение B из окружающей среды.

Мы можем пойти дальше и реализовать map а также вручную:

def map[A, B](ma: X => A)(f: A => B): X => B = ???

Глядя на аргументы X => A а также A => Bс ожидаемым результатом X => Bэто выглядит точно так же, как композиция функций, которая действительно так и есть.

Пример использования с использованием кошек с импортом:

import cats.data.Reader

Допустим, у нас есть некоторые Config тип:

case class Config(inDev: Boolean, devValue: Int, liveValue: Int)

который сообщает нам, находимся ли мы в среде "dev" и дает нам значения для чего-то для "dev" и для "live". Мы можем начать с написания простого Reader[Config, Boolean] который зачитал inDev флаг для нас.

val inDev: Reader[Config, Boolean] = Reader((c: Config) => c.inDev)

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

def branch(flag: Boolean): Reader[Config, Int] =
  if (flag) Reader((c: Config) => c.devValue)
  else      Reader((c: Config) => c.liveValue)

Теперь мы можем составить два:

val getValue: Reader[Config, Int] =
  for {
    flag <- inDev
    value <- branch(flag)
  } yield value

И теперь мы можем получить наши Int передавая в различных Config ценности:

getValue.run(Config(true, 1, 2)) // 1
getValue.run(Config(false, 1, 2)) // 2

В целом это можно использовать как хороший способ добиться внедрения зависимостей, используя только функции!

Другие вопросы по тегам