Вопрос по monads, configuration, reader-monad, scala – Данные конфигурации в Scala - использовать ли мне монаду Reader?
Как создать правильно функционирующий настраиваемый объект в Scala? Я смотрел Тони Морриса видео наReader
Монада и я все еще не можем соединить точки.
У меня есть жестко закодированный списокClient
объекты:
class Client(name : String, age : Int){ /* etc */}
object Client{
//Horrible!
val clients = List(Client("Bob", 20), Client("Cindy", 30))
}
я хочуClient.clients
быть определенным во время выполнения, с гибкостью чтения его из файла свойств или из базы данных. В мире Java я определяю интерфейс, реализую два типа источника и использую DI для назначения переменной класса:
trait ConfigSource {
def clients : List[Client]
}
object ConfigFileSource extends ConfigSource {
override def clients = buildClientsFromProperties(Properties("clients.properties"))
//...etc, read properties files
}
object DatabaseSource extends ConfigSource { /* etc */ }
object Client {
@Resource("configuration_source")
private var config : ConfigSource = _ //Inject it at runtime
val clients = config.clients
}
Это кажется довольно чистым решением для меня (не много кода, ясное намерение), но этоvar
does выскочить (ОТО, мне это не кажетсяreally хлопотно, так как я знаю этоwill вводиться один раз и только один раз).
Что быReader
Монада выглядит как в этой ситуации, и объясните мне, как я 5, в чем ее преимущества?
val
s може можно изменить с помощью отражения, так что ваша библиотека внедрения зависимостей может "ввести значение"
gerferra
Client
класс с аргументом, поэтому конфиг может быть передан экземплярамClient
?
matt b
Reader
Монада будет предпочтительнее.
Larry OBrien
поверхностной разницы между вашим подходом и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
. Я не буду вдаваться в это здесь, но я просто скажу, что Скалаз (или Потрясающая беседа NEScala Рунара, или Тони который вы уже видели) дает вам все, что вам нужно.
Важным моментом здесь является то, чConfigSource
Зависимость и ее так называемая инъекция в основном скрыты. Единственный «намек», который мы можем увидеть здесь, это то, чтоages
имеет типConfigured[(Int, Int, Int)]
а не просто(Int, Int, Int)
. Нам не нужно явно ссылаться наconfig
где угодно
Как в сторон, так мне почти всегда нравится думать о монадах: они скрыть их эффект так что это не загрязняет поток вашего кода, в то время как явно объявив эффект в подписи типа. Другими словами, вам не нужно повторяться слишком много: вы говорите: «Эй, эта функция имеет дело с эффект X "в возвращаемом типе функции, и больше не связывайтесь с ним.
В этом примере, конечно, эффект заключается в чтении из некоторой фиксированной среды. Еще один монадический эффект, с которым вы, возможно, знакомы, включает обработку ошибок: мы можем сказать, чтоOption
скрывает логику обработки ошибок, делая явной возможность ошибок в типе вашего метода. Или, в отличие от чтения,Writer
monad скрывает то, к чему мы пишем, в то же время явно указывая свое присутствие в системе типов.
Теперь, наконец, так же, как мы обычно должны загружать структуру DI (где-то за пределами нашего обычного потока управления, такого как в файле XML), нам также нужно загружать эту любопытную монаду. Конечно, у нас будет логическая точка входа в наш код, например:
def run: Configured[Unit] = // ...
В конечном итоге все довольно просто: сConfigured[A]
- это просто синоним типа для функцииConfigSource => A
, мы можем просто применить функцию к ее «среде»:
run(ConfigFileSource)
// or
run(DatabaseSource)
Та-да! Таким образом, в отличие от традиционного подхода к DI в стиле Java, здесь не происходит никакой «магии». Единственная магия как бы заключена в определении нашегоConfigured
тип и как он ведет себя как монада. Самое главное, система типов делает нас честными о том, в каком внедрении зависимости «области» происходит: что-нибудь с типомConfigured[...]
находится в мире DI, и ничего без него нет. Мы просто не получаем это в старом школьном DI, гдевс потенциально управляется магией, поэтому вы на самом деле не знаете, какие части вашего кода безопасно использовать повторно вне структуры DI (например, в рамках ваших модульных тестов или в каком-либо другом проекте полностью).
Обновить Я написалСообщение блог что объясняетReader
более подробно.
Configured[...]
среда). Это все о функциях, вызывающих другие функции, а не о внутренностях ваших объектов.
mergeconflict
Configured[PriorReturnedType]
до фн, где мы выбираемrun(ConfigFileSource)
илиrun(DatabaseSource)
? Почему это лучше, чем передатьConfigSource
как аргумент? И я не слежу за тем, как «у нас нет никакой« магии », происходящей здесь». Мы все еще должны выбратьrun(ConfigFileSource)
илиrun(DatabaseSource)
аргументом командной строки или переменной окружения или «магией» DI, не так ли?
Larry OBrien
Configured[...]
подпись, но ваши чистые функции нет. Опять же, хорошо различать эти два мира - это хорошо.
mergeconflict