在“ Scalaz日常使用功能”的第二篇文章中,我们将探讨Monad变压器和Reader monad的主题。让我们从Monad变压器开始。 当您不得不处理嵌套的Monad时,Monad变压器会派上用场,这种情况经常发生。 例如,当您必须使用嵌套的Future [Option]或Future [Either]时,您的理解很快就会变得不可读,因为您必须明确处理Option的None和Some情况以及Success和Failure情况。 在本文中,我将展示一些Monad变压器派上用场的示例,以及如何使用它们。
本系列当前提供以下文章:
在没有Monad变压器的情况下工作
就像我们在简介中提到的那样,Monad转换器在处理嵌套Monad时非常有用。 但是,什么时候会遇到这些? 好吧,很多Scala数据库库都倾向于异步(使用Futures ),在某些情况下会返回Options 。 例如,您可能查询返回Future [Option [T]]的特定记录:
# from Phantom cassandra driver (without implicits), which returns Some(Record) if found
# or None if nothing can be found
def one(): Future[Option[Record]]
# or you might want to get the first element from a Slick query
val res : Future[Option[Row]] = db.run(filterQuery(id).result.headOption)
或者,您可能只具有自己的特征或服务定义函数,这些函数最终将最终返回Option或Either :
# get an account, or none if no account is found
def getAccount() : Future[Option[Account]]
# withdraw an amount from an account, returning either the new amount in the account
# or a message explaining what went wrong
def withdraw(account: Account, amount: Amount) : Future[\/[String, Amount]]
让我们看一个不使用Monad转换器时会得到的丑陋代码示例:
def withdrawWithoutMonadTransformers(accountNumber: String, amount: Amount) : Future[Option[Statement]] = {
for {
// returns a Future[Option[Account]]
account <- Accounts.getAccount(accountNumber)
// we can do a fold, using scalaz for the typed None, since a None isn't typed
balance <- account.fold(Future(none[Amount]))(Accounts.getBalance(_))
// or sometimes we might need to do a patten match, since we've got two options
_ <- (account, balance) match {
case (Some(acc), Some(bal)) => Future(Accounts.withdraw(acc,bal))
case _ => Future(None)
}
// or we can do a nested map
statement <- Future(account.map(Accounts.getStatement(_)))
} yield statement
}
如您所见,当我们需要使用嵌套的Monad时,我们需要在理解的每一步的右侧处理嵌套的Monad。 Scala的语言足够丰富,我们可以用很多不同的方法来做到这一点,但是代码的可读性却不高。 如果我们对多个Option感兴趣,我们必须借助折叠来求助于嵌套地图或平面图(对于Option而言 ),有时不得不求助于模式匹配。 可能还有其他方法可以执行此操作,但是总而言之,代码的可读性更高。 由于我们必须显式处理嵌套的Option。
现在使用Monad变压器
使用Monad变压器,我们可以删除所有这些样板,并获得使用此类嵌套结构的非常方便的方法。 Scalaz提供以下类型的Monad变压器:
BijectionT
EitherT
IdT
IndexedContsT
LazyEitherT
LazyOptionT
ListT
MaybeT
OptionT
ReaderWriterStateT
ReaderT
StateT
StoreT
StreamT
UnWriterT
WriterT
尽管其中一些可能看起来有些怪异 , 但 ListT , OptionT , EitherT , ReaderT和WriterT可以应用于很多用例。 在第一个示例中,我们将重点介绍OptionT Monad变压器。 首先让我们看一下如何创建OptionT monad。 对于我们的示例,我们将创建一个OptionT [Future,A] ,将Option [A]包装在Future中 。 我们可以从A像这样创建这些:
scala> :require /Users/jos/.ivy2/cache/org.scalaz/scalaz-core_2.11/bundles/scalaz-core_2.11-7.2.1.jar
Added '/Users/jos/.ivy2/cache/org.scalaz/scalaz-core_2.11/bundles/scalaz-core_2.11-7.2.1.jar' to classpath.
scala> import scalaz._
import scalaz._
scala> import Scalaz._
import Scalaz._ ^
scala> import scala.concurrent.Future
import scala.concurrent.Future
scala> import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.ExecutionContext.Implicits.global
scala> type Result[A] = OptionT[Future, A]
defined type alias Result
scala> 1234.point[Result]
res1: Result[Int] = OptionT(scala.concurrent.impl.Promise$DefaultPromise@1c6ab85)
scala> "hello".point[Result]
res2: Result[String] = OptionT(scala.concurrent.impl.Promise$DefaultPromise@3e17219)
scala> res1.run
res4: scala.concurrent.Future[Option[Int]] = scala.concurrent.impl.Promise$DefaultPromise@1c6ab85
请注意,我们定义了显式类型Result以便能够使点工作。 如果不这样做,您将获得有关类型构造的有用错误消息:
scala> "why".point[OptionT[Future, String]]
<console>:16: error: scalaz.OptionT[scala.concurrent.Future,String] takes no type parameters, expected: one
"why".point[OptionT[Future, String]]
仅在处理monad转换器的内部值时才能使用point 。 如果您已经拥有Future或Option ,则需要使用OptionT构造函数。
scala> val p: Result[Int] = OptionT(Future.successful(some(10)))
p: Result[Int] = OptionT(scala.concurrent.impl.Promise$KeptPromise@40dde94)
使用Monad变压器,我们可以自动拆开嵌套的Monad。 现在,我们现在将如何将值转换为OptionT,让我们回顾一下先前看到的示例,并像这样重写它:
def withdrawWithMonadTransformers(accountNumber: String, amount: Amount) : Future[Option[Statement]] = {
type Result[A] = OptionT[Future, A]
val result = for {
account <- OptionT(Accounts.getAccount(accountNumber))
balance <- OptionT(Accounts.getBalance(account))
_ <- OptionT(Accounts.withdraw(account,balance).map(some(_)))
statement <- Accounts.getStatement(account).point[Result]
} yield statement
result.run
}
不错吧? 无需将所有噪声归纳为正确的类型,我们只需创建OptionT实例并将其返回即可。 为了从OptionT中获取储值,我们只需要调用run 。
即使它已经更具可读性。 现在,我们有了创建OptionT的麻烦 。 即使噪音不大,它仍然会让人分心。
还有更多语法清除
我们甚至可以多清理一点:
// with Monad transformers
type Result[A] = OptionT[Future, A]
/**
* Unfortunately we can't use overloading, since we then run into
* type erase stuff, and the thrush operator not being able to find
* the correct apply function
*/
object ResultLike {
def applyFO[A](a: Future[Option[A]]) : Result[A] = OptionT(a)
def applyF[A](a: Future[A]) : Result[A] = OptionT(a.map(some(_)))
def applyP[A](a: A) : Result[A] = a.point[Result]
}
def withdrawClean(accountNumber: String, amount: Amount) : Future[Option[Statement]] = {
val result: Result[Statement] = for {
account <- Accounts.getAccount(accountNumber) |> ResultLike.applyFO
balance <- Accounts.getBalance(account) |> ResultLike.applyFO
_ <- Accounts.withdraw(account,balance) |> ResultLike.applyF
statement <- Accounts.getStatement(account) |> ResultLike.applyP
} yield statement
result.run
}
在这种方法中,我们仅创建特定的转换器以将结果转换为OptionT单声道。 结果是,实际的理解非常容易理解,没有任何混乱。 在右侧,出于非直接视力的考虑,我们进行了向OptionT的转换。 请注意,这不是最干净的解决方案,因为我们需要指定不同的Apply函数。 这里的重载不起作用,因为在类型擦除之后, applyFO和applyF将具有相同的签名。
读者单子
Reader Monad是Scalaz提供的标准Monad之一。 Reader monad可用于轻松传递配置(或其他值),并可用于诸如依赖项注入之类的东西。
Reader monad解决方案
Reader monad允许您在scala中进行依赖项注入。 那个依赖是否是配置对象,还是对其他服务的引用,实际上并没有那么多。 我们从一个例子开始,因为这最好地说明了如何使用阅读器monad。
对于此示例,我们假设我们有一项服务,该服务需要Session才能完成工作。 这可能是数据库会话,某些Web服务会话或其他。 因此,让我们将之前的示例替换为该示例,现在通过删除期货来对其进行一些简化:
trait AccountService {
def getAccount(accountNumber: String, session: Session) : Option[Account]
def getBalance(account: Account, session: Session) : Option[Amount]
def withdraw(account: Account, amount: Amount, session: Session) : Amount
def getStatement(account: Account, session: Session): Statement
}
object Accounts extends AccountService {
override def getAccount(accountNumber: String, session: Session): Option[Account] = ???
override def getBalance(account: Account, session: Session): Option[Amount] = ???
override def withdraw(account: Account, amount: Amount, session: Session): Amount = ???
override def getStatement(account: Account, session: Session): Statement = ???
}
这似乎有点烦人,因为每次我们要调用其中一个服务时,我们都需要提供Session的实现。 我们当然可以使Session隐式,但是当我们调用该服务的功能时,我们仍然需要确保它在范围内。 如果我们能找到一种以某种方式注入此会话的方法,那就太好了。 我们当然可以在此服务的构造函数中执行此操作,但是我们也可以为此使用Reader Reader,将代码更改为:
// introduce a Action type. This represents an action our service can execute. As you can see in
// the declaration, this Action, requires a Session.
type Action[A] = Reader[Session, A]
trait AccountService {
// return an account, or return none when account can't be found
def getAccount(accountNumber: String) : Action[Option[Account]]
// return the balance when account is opened, or none when it isn't opened yet
def getBalance(account: Account) :Action[Option[Amount]]
// withdraw an amount from the account, and return the new amount
def withdraw(account: Account, amount: Amount) : Action[Amount]
// we can also get an account overview statement, which somehow isn't async
def getStatement(account: Account): Action[Statement]
}
object Accounts extends AccountService {
override def getAccount(accountNumber: String): Action[Option[Account]] = Reader((session: Session) => {
// do something with session here, and return result
session.doSomething
some(Account())
})
override def getBalance(account: Account): Action[Option[Amount]] = Reader((session: Session) => {
// do something with session here, and return result
session.doSomething
some(Amount(10,"Dollar"))
})
override def withdraw(account: Account, amount: Amount): Action[Amount] = Reader((session: Session) => {
// do something with session here, and return result
session.doSomething
Amount(5, "Dollar")
})
override def getStatement(account: Account): Action[Statement] = Reader((session: Session) => {
// do something with session here, and return result
session.doSomething
Statement(account)
})
}
如您所见,我们没有返回结果,而是将结果包装在Reader中 。 很酷的事情是,由于Reader只是monad,我们现在可以开始编写东西了。
def withdrawWithReader(accountNumber: String) = {
for {
account <- Accounts.getAccount(accountNumber)
balance <- account.fold(Reader((session: Session) => none[Amount]))(ac => Accounts.getBalance(ac))
_ <- (account, balance) match {
case (Some(acc), Some(bal)) => Accounts.withdraw(acc,bal)
case _ => Reader((session: Session) => none[Amount])
}
statement <- account match { case Some(acc) => Accounts.getStatement(acc)}
} yield statement
}
这不会返回实际的最终值,但会返回Reader 。 现在,我们可以通过传入Session来运行代码:
// function returns 'steps' to execute, run execute these steps in the context of 'new Session'
withdrawWithReader("1234").run(new Session())
当您回顾withdrawWithReader函数时,您会发现我们再次必须显式管理Option monad,并确保始终创建一个Reader 。 幸运的是,Scalaz还提供了ReaderT ,我们可以使用它来自动处理特定类型的Monad。 在以下代码中,我们显示了此示例的操作方法:
// introduce a Action type. This represents an action our service can execute. As you can see in
// the declaration, this Action, requires a Session.
type Action[A] = ReaderT[Option, Session, A]
trait AccountService {
// return an account, or return none when account can't be found
def getAccount(accountNumber: String) : Action[Account]
// return the balance when account is opened, or none when it isn't opened yet
def getBalance(account: Account) :Action[Amount]
// withdraw an amount from the account, and return the new amount
def withdraw(account: Account, amount: Amount) : Action[Amount]
// we can also get an account overview statement, which somehow isn't async
def getStatement(account: Account): Action[Statement]
}
object Accounts extends AccountService {
override def getAccount(accountNumber: String): Action[Account] = ReaderT((session: Session) => {
// do something with session here, and return result
session.doSomething
some(Account())
})
override def getBalance(account: Account): Action[Amount] = ReaderT((session: Session) => {
// do something with session here, and return result
session.doSomething
some(Amount(10,"Dollar"))
})
override def withdraw(account: Account, amount: Amount): Action[Amount] = ReaderT((session: Session) => {
// do something with session here, and return result
session.doSomething
Some(Amount(5, "Dollar"))
})
override def getStatement(account: Account): Action[Statement] = ReaderT((session: Session) => {
// do something with session here, and return result
session.doSomething
Some(Statement(account))
})
}
def withdrawWithReaderT(accountNumber: String) = {
for {
account <- Accounts.getAccount(accountNumber)
balance <- Accounts.getBalance(account)
_ <- Accounts.withdraw(account, balance)
statement <- Accounts.getStatement(account)
} yield statement
}
withdrawWithReaderT("1234").run(new Session)
如您所见,变化不大。 我们所做的主要更改是将Action的声明更改为使用ReaderT而不是Reader ,并且我们更改了特性和实现以使用它。 现在,当您查看withdrawWithReaderT函数时,您会看到我们不再需要处理Option了,但是它是由我们的ReaderT处理的(实际上是Kleisli,但这是另一把子弹的东西)。 酷吧?
尽管这对于Option而言非常有用 ,但是如果我们回到原始示例并想处理嵌套在Future中的Option并再次在Reader中进行处理,将会发生什么? 好的,到那时,我们可能超出了“ Scalaz日常使用功能”的范围,但是基本设置是相同的:
// introduce a Action type. This represents an action our service can execute. As you can see in
// the declaration, this Action, requires a Session.
type OptionTF[A] = OptionT[Future, A]
type Action[A] = ReaderT[OptionTF, Session, A]
trait AccountService {
// return an account, or return none when account can't be found
def getAccount(accountNumber: String) : Action[Account]
// return the balance when account is opened, or none when it isn't opened yet
def getBalance(account: Account) :Action[Amount]
// withdraw an amount from the account, and return the new amount
def withdraw(account: Account, amount: Amount) : Action[Amount]
// we can also get an account overview statement, which somehow isn't async
def getStatement(account: Account): Action[Statement]
}
/**
* Normally you would wrap an existing service, with a readerT specific one, which would handle
* all the conversion stuff.
*/
object Accounts extends AccountService {
override def getAccount(accountNumber: String): Action[Account] = ReaderT((session: Session) => {
// do something with session here, and return result
session.doSomething
// Assume we get a Future[Option[Account]]
val result = Future(Option(Account()))
// and we need to lift it in the OptionTF and return it.
val asOptionTF: OptionTF[Account] = OptionT(result)
asOptionTF
})
override def getBalance(account: Account): Action[Amount] = ReaderT((session: Session) => {
// do something with session here, and return result
session.doSomething
// assume we get a Future[Option[Amount]]
val result = Future(some(Amount(10,"Dollar")))
// convert it to the Action type, with explicit type to make compiler happy
val asOptionTF: OptionTF[Amount] = OptionT(result)
asOptionTF
})
override def withdraw(account: Account, amount: Amount): Action[Amount] = ReaderT((session: Session) => {
// do something with session here, and return result
session.doSomething
// assume we get a Future[Amount]
val result = Future(Amount(5, "Dollar"))
// convert it to the correct type
val asOptionTF: OptionTF[Amount] = OptionT(result.map(some(_)))
asOptionTF
})
override def getStatement(account: Account): Action[Statement] = ReaderT((session: Session) => {
// do something with session here, and return result
session.doSomething
// assume we get a Statement
val result = Statement(account)
// convert it to the correct type
result.point[OptionTF]
})
}
def withdrawWithReaderT(accountNumber: String) = {
for {
account <- Accounts.getAccount(accountNumber)
balance <- Accounts.getBalance(account)
_ <- Accounts.withdraw(account, balance)
statement <- Accounts.getStatement(account)
} yield statement
}
// this is the result wrapped in the option
val finalResult = withdrawWithReaderT("1234").run(new Session)
// get the Future[Option] and wait for the result
println(Await.result(finalResult.run, 5 seconds))
我们定义了一个不同的ReaderT类型,在这里我们传入OptionT而不是Option 。 该OptionT将处理Option / Future转换。 当我们有了新的ReaderT时,我们当然需要将服务调用的结果提升到此monad,这需要某种类型的强制性才能使编译器理解所有内容(Intellij,也不再对此有所了解)。 结果虽然很好。 实际的理解力保持不变,但是这次可以在Reader的 Future内部处理Option了!
结论
在本文中,我们研究了Scalaz的两个部分,它们在处理嵌套的monad或希望更好地管理组件之间的依赖关系时非常有用。 很棒的事情是,将Monad Transformers与Reader monad一起使用非常容易。 总体结果是,通过几个小步骤,我们可以完全隐藏使用Future和Option手的工作细节(在这种情况下),并具有很好的理解力和其他优点。