Вопрос по monads, configuration, reader-monad, scala – Данные конфигурации в Scala - использовать ли мне монаду Reader?

33

Как создать правильно функционирующий настраиваемый объект в 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, в чем ее преимущества?

vals може можно изменить с помощью отражения, так что ваша библиотека внедрения зависимостей может "ввести значение" gerferra
почему бы не сделатьClient класс с аргументом, поэтому конфиг может быть передан экземплярамClient? matt b
@ gerferra какая точка вэл изменена отражением, если у нас есть var? om-nom-nom
Ты можешь найти первую половину Беседа Рунара по NEScala чтобы быть немного более доступным. mergeconflict
@ matt Правильно, это был бы другой способ сделать это, но это все еще оставило бы меня неясным относительно того, / почеReader Монада будет предпочтительнее. Larry OBrien

Ваш Ответ

1   ответ
46

поверхностной разницы между вашим подходом и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
Хмм ... Так что, в конечном счете, нам все еще нужно провести рефакторинг всех наших подписей fn, чтобы они вернулисьConfigured[PriorReturnedType] до фн, где мы выбираемrun(ConfigFileSource) илиrun(DatabaseSource)? Почему это лучше, чем передатьConfigSource как аргумент? И я не слежу за тем, как «у нас нет никакой« магии », происходящей здесь». Мы все еще должны выбратьrun(ConfigFileSource) илиrun(DatabaseSource) аргументом командной строки или переменной окружения или «магией» DI, не так ли? Larry OBrien
re: «Так что, в конечном счете, нам все еще нужно провести рефакторинг всех наших подписей fn ...» - некоторые из них, а не все Все, что зависит от глобальной конфигурации, будет нуждаться вConfigured[...] подпись, но ваши чистые функции нет. Опять же, хорошо различать эти два мира - это хорошо. mergeconflict
re: «магия» - я могу объяснить, как Reader работает легко: это функция, и она требует аргумента. Напротив, как работает Spring (например)? Ну, у вас есть несколько классов в вашей системе, которые вы не создаете сами; Вы позволяете Spring создавать их для вас. Spring знает это, потому что вы настроили контекст приложения. Таким образом, контекст вашего приложения сообщает Spring, какие методы он должен вызывать при создании объектов. Но не совсем, они должны следовать соглашению об именовании бинов, чтобы оно могло сумасшедшим сумасшедшим, д mergeconflict
Вид связанного вопроса о том, как Reader Monad DI сравнивается с DI конструктора-params, и как использовать RM с несколькими зависимостями / вызовами вложенных методов: / Stackoverflow.com вопросы / 29174500 / ... adamw

Похожие вопросы