什么功能技术使得不必通过功能传递配置
What functional technique enables not having to pass configuration through functions
随着我对 FP 的深入研究,我很好奇 'best' 存储从配置文件加载的设置的方法。我刚刚创建了一个案例 class,其中包含所有必要的配置变量,并在应用启动时进行了设置。然后我将那个案例 class 传递给任何需要它的信息的函数。
然而,这似乎很烦人,尤其是当设置案例 class 必须通过许多函数传播时。有更好的方法吗?
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
出现在 return 类型中而不是参数。我们可以把这个理解为意思
"When encodeCredentials
runs in the context that provides a
Config
, then it will produce a String
result."
这里的“上下文”由Reader
monad 表示。
此外,请注意 Config
即使在主要业务逻辑中也不是参数
def program: Reader[Config, Either[String, String]] = for {
credentials <- encodeCredentials
response <- basicAuth(credentials)
validation <- validateResponse(response)
} yield validation
我们让方法在包含 Config
的上下文中通过 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
随着我对 FP 的深入研究,我很好奇 'best' 存储从配置文件加载的设置的方法。我刚刚创建了一个案例 class,其中包含所有必要的配置变量,并在应用启动时进行了设置。然后我将那个案例 class 传递给任何需要它的信息的函数。
然而,这似乎很烦人,尤其是当设置案例 class 必须通过许多函数传播时。有更好的方法吗?
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
出现在 return 类型中而不是参数。我们可以把这个理解为意思
"When
encodeCredentials
runs in the context that provides aConfig
, then it will produce aString
result."
这里的“上下文”由Reader
monad 表示。
此外,请注意 Config
即使在主要业务逻辑中也不是参数
def program: Reader[Config, Either[String, String]] = for {
credentials <- encodeCredentials
response <- basicAuth(credentials)
validation <- validateResponse(response)
} yield validation
我们让方法在包含 Config
的上下文中通过 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