了解 Scala 中的应用函子
Understanding Applicative Functor in Scala
假设我需要编写一些函数来调用几个 REST API:api1
、api2
、api3
。
def api1(url: Url) = ???
def api2(url: Url) = ???
def api3(url: Url) = ???
为了简单起见,假设我使用我自己的简化 class Url
:
case class Url(host: String, port: Int, path: Path)
为了构建 Url
我从配置中读取 host
和 port
并调用函数 api1
、api2
、api3
,其中添加了必需的 paths
并调用了它们的 API:
def api1(host: String, port: Int) = ???
def api2(host: String, port: Int) = ???
def api3(host: String, port: Int) = ???
val (host, port) = ... // read from the configuration
// call the APIs
api1(host, port)
api2(host, port)
api3(host, port)
最好使用函数 Path => Url
(或者 builder pattern
,如果我们在 Java
中写入)以便 隐藏 host
和 port
以及构建 Url
.
的其他细节
def api1(f: Path => Url) = ...
def api2(f: Path => Url) = ...
def api3(f: Path => Url) = ...
使用 curring
很容易实现这样的功能 f: Path => Url
val url: String => Int => Path = (Url.apply _).curried
val (host, port) = ... // from the configuration
val f = url(host, port)
api1(f)
api2(f)
api3(f)
到目前为止,还不错,但是如果有 可选 主机和端口怎么办?
val (hostOpt: Option[String], portOpt: Option[Int]) = ... // from configuration
现在我们有函数 String => Int => Path => Url
和 Option[String]
和 Option[Int]
。如何获得 Path => Url
?
让我们问一个稍微不同的问题:给定 String => Int => Path => Url
、Option[String]
和 Option[Int]
如何得到 Option[Path => Url]
?
幸运的是我们可以很容易地定义这样的操作:
trait Option[A] { ... def ap[B](of: Option[A => B]): Option[B] = ??? }
鉴于此 ap
我们可以回答原始问题:
val of: Option[Path => Url] = portOpt ap (hostOpt ap Some(url)
of.map(f => api1(f))
of.map(f => api2(f))
of.map(f => api3(f))
抽象地说,我们使用了 Option
是一个 applicative functor 的事实。 M
是一个应用函子,如果它是一个函子并且有两个额外的操作:
ap
得到 M[B]
给定 M[A => B]
和 M[A]
pure
从 A => B
得到 M[A => B]
(Some
对应 Option
)
这些操作应遵守两个简单的定律,但这是另一回事了。
...
有意义吗?
这对我来说听起来很合理,虽然我不确定这里是否有很多问题,这是它自己的问题。
我将此作为答案而不是评论,因为有一件事值得注意。对于许多类型,有理由避免 monadic 绑定并坚持使用 ap
而不仅仅是 "using less powerful abstractions is the right thing to do".
例如:标准库 future API 的 zip
是一个应用运算符,允许您并行 运行 futures,如果您使用 bar() zip foo()
而不是 for { f <- foo(); b <- bar() } yield (f, b)
你实际上可以加速你的程序(在很多情况下)。对于其他类型,使用 applicative functor 而不是 monadic bind 提供了其他类型的优化可能性。
Option
并非如此。用flatMap
来定义ap
也不是没有道理的。使用应用组合器仍然是 "the right thing to do",但 flatMap
就在那里,不需要额外的定义或依赖项,而且 for
-comprehensions 是如此简单和干净。与您在期货等方面看到的收益不同。
假设我需要编写一些函数来调用几个 REST API:api1
、api2
、api3
。
def api1(url: Url) = ???
def api2(url: Url) = ???
def api3(url: Url) = ???
为了简单起见,假设我使用我自己的简化 class Url
:
case class Url(host: String, port: Int, path: Path)
为了构建 Url
我从配置中读取 host
和 port
并调用函数 api1
、api2
、api3
,其中添加了必需的 paths
并调用了它们的 API:
def api1(host: String, port: Int) = ???
def api2(host: String, port: Int) = ???
def api3(host: String, port: Int) = ???
val (host, port) = ... // read from the configuration
// call the APIs
api1(host, port)
api2(host, port)
api3(host, port)
最好使用函数 Path => Url
(或者 builder pattern
,如果我们在 Java
中写入)以便 隐藏 host
和 port
以及构建 Url
.
def api1(f: Path => Url) = ...
def api2(f: Path => Url) = ...
def api3(f: Path => Url) = ...
使用 curring
很容易实现这样的功能f: Path => Url
val url: String => Int => Path = (Url.apply _).curried
val (host, port) = ... // from the configuration
val f = url(host, port)
api1(f)
api2(f)
api3(f)
到目前为止,还不错,但是如果有 可选 主机和端口怎么办?
val (hostOpt: Option[String], portOpt: Option[Int]) = ... // from configuration
现在我们有函数 String => Int => Path => Url
和 Option[String]
和 Option[Int]
。如何获得 Path => Url
?
让我们问一个稍微不同的问题:给定 String => Int => Path => Url
、Option[String]
和 Option[Int]
如何得到 Option[Path => Url]
?
幸运的是我们可以很容易地定义这样的操作:
trait Option[A] { ... def ap[B](of: Option[A => B]): Option[B] = ??? }
鉴于此 ap
我们可以回答原始问题:
val of: Option[Path => Url] = portOpt ap (hostOpt ap Some(url)
of.map(f => api1(f))
of.map(f => api2(f))
of.map(f => api3(f))
抽象地说,我们使用了 Option
是一个 applicative functor 的事实。 M
是一个应用函子,如果它是一个函子并且有两个额外的操作:
ap
得到M[B]
给定M[A => B]
和M[A]
pure
从A => B
得到M[A => B]
(Some
对应Option
)
这些操作应遵守两个简单的定律,但这是另一回事了。
...
有意义吗?
这对我来说听起来很合理,虽然我不确定这里是否有很多问题,这是它自己的问题。
我将此作为答案而不是评论,因为有一件事值得注意。对于许多类型,有理由避免 monadic 绑定并坚持使用 ap
而不仅仅是 "using less powerful abstractions is the right thing to do".
例如:标准库 future API 的 zip
是一个应用运算符,允许您并行 运行 futures,如果您使用 bar() zip foo()
而不是 for { f <- foo(); b <- bar() } yield (f, b)
你实际上可以加速你的程序(在很多情况下)。对于其他类型,使用 applicative functor 而不是 monadic bind 提供了其他类型的优化可能性。
Option
并非如此。用flatMap
来定义ap
也不是没有道理的。使用应用组合器仍然是 "the right thing to do",但 flatMap
就在那里,不需要额外的定义或依赖项,而且 for
-comprehensions 是如此简单和干净。与您在期货等方面看到的收益不同。