Какой функциональный метод позволяет не передавать конфигурацию через функции

#scala #playframework #functional-programming

#scala #playframework #функциональное программирование

Вопрос:

Поскольку я все больше углубляюсь в FP, мне любопытно узнать о «лучшем» способе сохранения настроек, загруженных из файлов конфигурации. Я только что создал класс case со всеми необходимыми конфигурационными переменными и установил их при запуске приложения. Затем я передаю этот класс case в любую функцию, которая требует от него информации.

Однако это кажется довольно раздражающим, особенно когда этот класс settings case должен распространяться через множество функций. Есть ли лучший способ сделать это?

Комментарии:

1. Просто добавьте config в качестве неявного параметра для каждого метода, которому это необходимо. Вы также могли бы взглянуть на reader monad .

Ответ №1:

Reader monad предоставляет способ распространения конфигурации без необходимости передавать ее в качестве параметра во всех функциях, которым это необходимо. Сравните следующие две реализации:

Конфигурация доступна из контекста через Reader[Config, String]

 object ConfigFunctional extends App {
  case class Config(username: String, password: String, host: String)

  def encodeCredentials: Reader[Config, String] = Reader { config =>
    Base64.getEncoder.encodeToString(s"${config.username}:${config.password}".getBytes())
  }

  def basicAuth(credentials: String): Reader[Config, String] = Reader { config =>
    Http(s"${config.host}/HTTP/Basic/")
      .header("Authorization", s"Basic $credentials")
      .asString
      .body
  }

  def validateResponse(body: String): Reader[Config, Either[String, String]] = Reader { _ =>
    if (body.contains("Your browser made it"))
      Right("Credentials are valid!")
    else
      Left("Wrong credentials")
  }

  def program: Reader[Config, Either[String, String]] = for {
    credentials       <- encodeCredentials
    response          <- basicAuth(credentials)
    validation        <- validateResponse(response)
  } yield validation


  val config = Config("guest", "guest", "https://jigsaw.w3.org")
  println(program.run(config))
}
  

Конфигурация, переданная в качестве аргумента

 object ConfigImperative extends App {
  case class Config(username: String, password: String, host: String)

  def encodeCredentials(config: Config): String = {
    Base64.getEncoder.encodeToString(s"${config.username}:${config.password}".getBytes())
  }

  def basicAuth(credentials: String, config: Config): String = {
    Http(s"${config.host}/HTTP/Basic/")
      .header("Authorization", s"Basic $credentials")
      .asString
      .body
  }

  def validateResponse(body: String): Either[String, String] = {
    if (body.contains("Your browser made it"))
      Right("Credentials are valid!")
    else
      Left("Wrong credentials")
  }

  def program(config: Config): Either[String, String] = {
    val credentials = encodeCredentials(config)
    val response    = basicAuth(credentials, config)
    val validation  = validateResponse(response)
    validation
  }

  val config = Config("guest", "guest", "https://jigsaw.w3.org")
  println(program(config))
}
  

Обе реализации должны выводить Right(Credentials are valid!) , однако обратите внимание, что в первой реализации config: Config нет параметра метода, например, contrast encodeCredentials :

 def encodeCredentials: Reader[Config, String]
def encodeCredentials(config: Config): String
  

Config появляется в возвращаемом типе вместо того, чтобы быть параметром. Мы можем интерпретировать это как значение

«Когда encodeCredentials выполняется в контексте, который предоставляет Config , тогда это приведет к String результату».

«Контекст» здесь представлен Reader монадой.

Кроме того, обратите внимание, что Config не является параметром даже в основной бизнес-логике

 def program: Reader[Config, Either[String, String]] = for {
  credentials       <- encodeCredentials
  response          <- basicAuth(credentials)
  validation        <- validateResponse(response)
} yield validation
  

Мы позволяем методам оценивать в контексте, содержащем функцию Config via run :

 program.run(config)
  

Для выполнения приведенных выше примеров нам нужны следующие зависимости

     scalacOptions  = "-Ypartial-unification",
    libraryDependencies   = Seq(
      "org.typelevel" %% "cats-core" % "1.6.0", 
      "org.scalaj" %% "scalaj-http" % "2.4.1"
    )
  

и импортирует

 import cats.data.Reader
import java.util.Base64
import scalaj.http.Http