Что означают операции с картой и плоской картой для ReaderMonad
Я новичок в скале. Я родом из Явы. Я читал монады и сформировал общее представление об этом. Хотя я могу оценить map
а также flatMap
операции над такими типами, как List
я не могу обернуть голову вокруг того, что они имеют в виду, когда дело доходит до reader monad
s. Может кто-нибудь, пожалуйста, приведите несколько простых примеров.
Я понимаю, что нам нужны 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
В целом это можно использовать как хороший способ добиться внедрения зависимостей, используя только функции!