Ответ 1
Начните с простой, поверхностной разницы между вашим подходом и подходом Reader
, который вам больше не нужно висеть на config
в любом месте. Скажем, вы определяете следующий неопределенный умственный синоним:
type Configured[A] = ConfigSource => A
Теперь, если мне понадобится ConfigSource
для некоторой функции, скажем, функция, которая получает n-й клиент в списке, могу объявить эту функцию как "настроенную":
def nthClient(n: Int): Configured[Client] = {
config => config.clients(n)
}
Итак, мы по существу вытягиваем config
из воздуха, в любое время, когда нам это нужно! Пахнет инъекцией зависимостей, правильно? Теперь предположим, что мы хотим, чтобы в первом списке присутствовали первые, второй и третий клиенты (при условии, что они существуют):
def ages: Configured[(Int, Int, Int)] =
for {
a0 <- nthClient(0)
a1 <- nthClient(1)
a2 <- nthClient(2)
} yield (a0.age, a1.age, a2.age)
Для этого, конечно, вам нужно определенное определение map
и flatMap
. Я не буду вдаваться в это здесь, но просто скажу, что Scalaz (или Rúnar awesome NEScala talk, или Tony's, который вы уже видели) дает вам все, что вам нужно.
Важным моментом здесь является то, что зависимость ConfigSource
и его так называемая инъекция в основном скрыты. Единственный "намек", который мы видим здесь, состоит в том, что ages
имеет тип Configured[(Int, Int, Int)]
, а не просто (Int, Int, Int)
. Нам не нужно явно ссылаться на config
где угодно.
В качестве стороннего я почти всегда люблю думать о монадах: они скрывают свой эффект, чтобы он не загрязнял поток вашего кода, а явно объявляющее эффект в сигнатуре типа. Другими словами, вам не нужно слишком много повторять: вы говорите "эй, эта функция имеет дело с эффектом X" в возвращаемом типе функции и больше не вмешивайтесь в нее.
В этом примере, конечно, эффект заключается в чтении из некоторой фиксированной среды. Другим монадическим эффектом, с которым вы могли бы быть знакомы, является обработка ошибок: мы можем сказать, что
Option
скрывает логику обработки ошибок, делая возможность явных ошибок в вашем типе метода. Или, как бы противоположность чтения, монадаWriter
скрывает то, что мы пишем, делая явное присутствие в системе типов.
Теперь, наконец, так же, как мы обычно должны загружать структуру DI (где-то вне нашего обычного потока управления, например, в файле XML), нам также необходимо загрузить эту любопытную монаду. Конечно, у нас будет логическая точка входа в наш код, например:
def run: Configured[Unit] = // ...
Это заканчивается довольно просто: поскольку Configured[A]
является просто синонимом типа для функции ConfigSource => A
, мы можем просто применить эту функцию к ее "среде":
run(ConfigFileSource)
// or
run(DatabaseSource)
Та-да! Таким образом, в отличие от традиционного подхода DI, основанного на Java, у нас нет никакой "магии", происходящей здесь. Единственное волшебство, как бы, инкапсулировано в определение нашего типа Configured
и того, как он ведет себя как монада. Самое главное, система типов держит нас честными, о которых происходит инъекция зависимостей "царства": все, что имеет тип Configured[...]
, находится в мире DI, и все без него - нет. Мы просто не получаем это в старой школе DI, где все потенциально управляется магией, поэтому вы действительно не знаете, какие части вашего кода безопасны для повторного использования вне рамок DI (например, внутри вашего подразделения тесты или в каком-либо другом проекте полностью).
update: Я написал сообщение в блоге, в котором более подробно объясняется Reader
.