Подтвердить что ты не робот

Конфигурационные данные в Scala - следует ли использовать монаду читателя?

Как создать правильно настраиваемый объект в 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 выпрыгивает (OTOH, мне это не кажется действительно хлопотным, так как я знаю, что это будет вводится один раз и только один раз).

Как бы выглядела монада Reader в этой ситуации и объяснить ее мне, как будто мне 5, каковы ее преимущества?

4b9b3361

Ответ 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.