了解Scala中的应用函子

Mic*_*ael 1 functional-programming scala functor applicative

假设我需要写一些函数来调用几个的REST API: ,api1,。api2api3

def api1(url: Url) = ???
def api2(url: Url) = ???
def api3(url: Url) = ???
Run Code Online (Sandbox Code Playgroud)

为了简单起见,我使用自己的简化类Url

case class Url(host: String, port: Int, path: Path)  
Run Code Online (Sandbox Code Playgroud)

为了构建一个Url我读host,并port从配置和通话功能api1api2api3,其中添加所需的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)
Run Code Online (Sandbox Code Playgroud)

这是更好的,虽然使用功能Path => Url(或者builder pattern,如果我们在写Java),以隐藏hostport建造的和其他细节Url

def api1(f: Path => Url) = ...
def api2(f: Path => Url) = ...
def api3(f: Path => Url) = ...
Run Code Online (Sandbox Code Playgroud)

这是很容易实现这样的功能f: Path => Urlcurring

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)
Run Code Online (Sandbox Code Playgroud)

到目前为止,还算不错,但是如果有可选的主机和端口呢?

val (hostOpt: Option[String], portOpt: Option[Int]) = ... // from configuration 
Run Code Online (Sandbox Code Playgroud)

现在我们有了一个函数String => Int => Path => Urland Option[String]Option[Int]。如何获得Path => Url

让我们来问一个稍微不同的问题:如何获得Option[Path => Url]给定的String => Int => Path => UrlOption[String]Option[Int]

幸运的是,我们可以轻松定义这样的操作:

trait Option[A] { ... def ap[B](of: Option[A => B]): Option[B] = ??? }
Run Code Online (Sandbox Code Playgroud)

鉴于此,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))
Run Code Online (Sandbox Code Playgroud)

抽象地说,我们使用的Option是一个实用函子M如果是函子并且具有两个附加操作,则为应用函子:

  • ap得到M[B]给予M[A => B]M[A]
  • pure获得M[A => B]A => BSomeOption

这些操作应遵循两个简单的法律,但这是另一回事。

...

是否有意义 ?

Tra*_*own 5

这对我来说听起来很合理,尽管我不确定这里是否有很多问题,这是它本身的问题。

我将其作为答案而不是评论,因为有一件事值得注意。对于许多类型,有理由避免单子绑定,而ap不仅仅是“使用功能较弱的抽象是正确的做法”。

例如:标准库Future API zip是一个应用程序运算符,它允许您并行运行Future,并且如果您使用bar() zip foo()而不是API ,则for { f <- foo(); b <- bar() } yield (f, b)实际上可以加速程序(在许多情况下)。对于其他类型,使用可应用的函子填充而不是单子绑定为优化提供了其他可能性。

的情况并非如此Option。用进行定义并不是没有道理apflatMap。使用应用组合器仍然是“正确的事情”,但是flatMap就在那里,并且不需要额外的定义或依赖项,并且- for理解是如此简单和干净。对于期货之类的东西,您所获得的收益不尽相同。