当前位置: 首页 > 工具软件 > Scalaz > 使用案例 >

scalaz使用_日常使用的Scalaz功能第2部分:Monad变形金刚和Reader Monad

隆谦
2023-12-01

scalaz使用

在“ Scalaz日常使用功能”的第二篇文章中,我们将探讨Monad变压器和Reader monad的主题。让我们从Monad变压器开始。 当您不得不处理嵌套的Monad时,Monad变压器会派上用场,这种情况经常发生。 例如,当您必须使用嵌套的Future [Option]或Future [Either]时,您的理解很快就会变得不可读,因为您必须明确处理OptionNoneSome情况以及SuccessFailure情况。 在本文中,我将展示一些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)

或者,您可能只是拥有自己的特征或服务定义函数,这些函数最终将最终返回OptionEither

# 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

尽管其中一些可能看起来有些怪异 ListTOptionTEitherTReaderTWriterT可以应用于很多用例。 在第一个示例中,我们将重点介绍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 。 如果您已经拥有FutureOption ,则需要使用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 monad。 结果是,实际的理解非常容易理解,没有任何混乱。 在右侧,出于非直接视力考虑,我们进行了向OptionT的转换。 请注意,这不是最干净的解决方案,因为我们需要指定不同的Apply函数。 这里的重载不起作用,因为在类型擦除之后, applyFOapplyF将具有相同的签名。

读者单子

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中嵌套的Option会发生什么呢? 到那时,我们可能超出了“ 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一起使用非常容易。 总体结果是,通过几个小步骤,我们可以完全隐藏使用FutureOption手的工作细节(在这种情况下),并具有很好的理解力和其他优点。

翻译自: https://www.javacodegeeks.com/2016/05/scalaz-features-everyday-usage-part-2-monad-transformers-reader-monad.html

scalaz使用

 类似资料: