在Scala中使用Cats库的9个技巧

由于语言的某些句法和语义特征,Scala中的函数式编程可能很难掌握。 特别是,当您熟悉主库时,一些语言工具和方法来实现您计划的内容就很明显了-但是在学习之初(尤其是您自己学习)并不容易识别它们。

因此,我认为在Scala中分享一些功能编程技巧将很有用。 示例和名称对应于猫,但是由于一般的理论基础,scalaz中的语法应相似。



9)扩展方法构造函数


让我们从最基本的工具开始-可以将实例转换为Option,Either等的任何类型的扩展方法,尤其是:

  • .someOption的相应none构造方法;
  • .asRight.asLeft Either
  • .valid.invalid.validNel.invalidNel用于Validated

使用它们的两个主要优点:

  1. 它更加紧凑和易于理解(因为保存了方法调用的顺序)。
  2. 与构造函数选项不同,这些方法的返回类型扩展为超类型,即:

 import cats.implicits._ Some("a") //Some[String] "a".some //Option[String] 

尽管多年来类型推断已得到改善,并且这种行为有助于程序员保持镇定的可能情况有所减少,但是今天在Scala中仍然可能由于过度专业化的输入而导致编译错误。 通常,与Either一起工作时会产生将头撞到桌子上的渴望(请参见《 带猫的Scala》第4.4.2章)。

关于该主题的.asRight件事: .asRight.asLeft还有一个类型参数。 例如, "1".asRight[Int]Either[Int, String] 。 如果未提供此参数,则编译器将尝试输出它并获取Nothing 。 但是,与每次构造两个参数或不提供两个参数相比,它更方便。

8)五十种阴影*>


在任何Apply方法(即ApplicativeMonad等)中定义的*>运算符仅表示“处理初始计算并将结果替换为第二个参数中指定的结果”。 用代码的语言(例如Monad ):

 fa.flatMap(_ => fb) 

为什么对不起眼的操作使用模糊的符号运算符? 开始使用ApplicativeError和/或MonadError,您会发现该操作将保留整个工作流程的错误效果。 以Either为例:

 import cats.implicits._ val success1 = "a".asRight[Int] val success2 = "b".asRight[Int] val failure = 400.asLeft[String] success1 *> success2 //Right(b) success2 *> success1 //Right(a) success1 *> failure //Left(400) failure *> success1 //Left(400) 

如您所见,即使发生错误,计算仍会短路。 *>将帮助您处理MonixIOMonix延迟计算。

有一个对称运算,<*。 因此,在前面的示例中:

 success1 <* success2 //Right(a) 

最后,如果符号的使用对您而言是陌生的,则不必诉诸它。 *>只是productR的别名,而* <是productL的别名。

注意事项


在个人对话中,亚当·沃斯基(Adam Warski)(感谢亚当!)正确地指出,除了*>( productR )之外,还有FlatMapSyntax >>。 >>的定义方式与fa.flatMap(_ => fb) ,但有两个细微差别:

  • 它是独立于productR定义的,因此,如果由于某种原因此方法的协定发生更改(从理论上讲,可以在不违反单子法则的情况下对其进行更改,但我不确定MonadError ),则您将不会受苦;
  • 更重要的是>>具有第二个操作数,该操作数由按名称调用,即 fb: => F[B] 。 如果执行可能导致堆栈爆炸的计算,则语义上的差异就变得至关重要。

基于此,我开始更频繁地使用*>。 一种或另一种方式,不要忘记上面列出的因素。

7)扬起帆!


许多人花时间将lift概念付诸实践。 但是当您成功时,您会发现他无处不在。

就像函数式编程中飞速发展的许多术语一样, lift来自类别理论 。 我将尝试解释:进行一项操作,更改其类型的签名,使其与抽象类型F直接相关。

在Cats中,最简单的示例是Functor

 def lift[A, B](f: A => B): F[A] => F[B] = map(_)(f) 

这意味着:更改此功能,使其作用于给定类型的函子F。

lift函数通常与给定类型的嵌套构造函数同义。 因此, EitherT.liftF本质上是EitherT.right. 来自Scaladoc的示例

 import cats.data.EitherT import cats.implicits._ EitherT.liftF("a".some) //EitherT(Some(Right(a))) EitherT.liftF(none[String]) //EitherT(None) 

蛋糕上的樱桃:Scala标准库中到处都有lift 。 最受欢迎(也许在日常工作中最有用)的示例是PartialFunction

 val intMatcher: PartialFunction[Int, String] = { case 1 => "jak się masz!" } val liftedIntMatcher: Int => Option[String] = intMatcher.lift liftedIntMatcher(1) //Some(jak się masz!) liftedIntMatcher(0) //None intMatcher(1) //jak się masz! intMatcher(0) //Exception in thread "main" scala.MatchError: 0 

现在我们可以继续处理更紧迫的问题。

6)mapN


mapN是用于元组的有用的辅助函数。 再次,这不是新奇的东西,而是好的旧运算符的替代品。 他是尖叫。

这是在两个元素的元组的情况下mapN的样子:

 // where t2: Tuple2[F[A0], F[A1]] def mapN[Z](f: (A0, A1) => Z)(implicit functor: Functor[F], semigroupal: Semigroupal[F]): F[Z] = Semigroupal.map2(t2._1, t2._2)(f) 

本质上,它允许我们映射来自任何F的元组内的值,这些F是半群(乘积)和函子(映射)。 因此:

 import cats.implicits._ ("a".some, "b".some).mapN(_ ++ _) //Some(ab) (List(1, 2), List(3, 4), List(0, 2).mapN(_ * _ * _)) //List(0, 6, 0, 8, 0, 12, 0, 16) 

顺便说一句,不要忘记,对于猫来说,您会得到元组的map和leftmap

 ("a".some, List("b","c").mapN(_ ++ _)) //won't compile, because outer type is not the same ("a".some, List("b", "c")).leftMap(_.toList).mapN(_ ++ _) //List(ab, ac) 

另一个有用的.mapN函数是实例化案例类:

 case class Mead(name: String, honeyRatio: Double, agingYears: Double) ("półtorak".some, 0.5.some, 3d.some).mapN(Mead) //Some(Mead(półtorak,0.5,3.0)) 

当然,您更愿意为此使用for循环运算符,但是mapN在简单情况下避免使用单声道转换器。

 import cats.effect.IO import cats.implicits._ //interchangable with eg Monix's Task type Query[T] = IO[Option[T]] def defineMead(qName: Query[String], qHoneyRatio: Query[Double], qAgingYears: Query[Double]): Query[Mead] = (for { name <- OptionT(qName) honeyRatio <- OptionT(qHoneyRatio) agingYears <- OptionT(qAgingYears) } yield Mead(name, honeyRatio, agingYears)).value def defineMead2(qName: Query[String], qHoneyRatio: Query[Double], qAgingYears: Query[Double]): Query[Mead] = for { name <- qName honeyRatio <- qHoneyRatio agingYears <- qAgingYears } yield (name, honeyRatio, agingYears).mapN(Mead) 

方法具有相似的结果,但后者无需使用单相变压器。

5)嵌套


Nested本质上是monad变压器的广义双重形式。 顾名思义,它允许您在某些条件下执行附件操作。 这是.map(_.map( :

 import cats.implicits._ import cats.data.Nested val someValue: Option[Either[Int, String]] = "a".asRight.some Nested(someValue).map(_ * 3).value //Some(Right(aaa)) 

除了FunctorNested泛化了ApplicativeApplicativeErrorTraverse 。 其他信息和示例在此处

4).recover / .recoverWith / .handleError / .handleErrorWith / .valueOr


Scala中的函数式编程与处理错误影响有很大关系。 ApplicativeErrorMonadError有一些有用的方法,可能对您找出四个主要方法之间的细微差别很有用。 因此,使用ApplicativeError F[A]:

  • handleError根据指定的函数将调用点处的所有错误转换为A。
  • recover操作的方式类似,但是接受部分功能,因此可以将您选择的错误转换为A。
  • handleErrorWithhandleErrorWith相似,但是其结果应类似于F[A] ,这意味着它可以帮助您转换错误。
  • recoverWith作用类似于recover,但也需要F[A]

如您所见,可以将handleErrorWith限制为handleErrorWithrecoverWith ,它们涵盖了所有可能的功能。 但是,每种方法都有其优点,并且以其自己的方式很方便。

通常,我建议您熟悉ApplicativeError API,它是Cats中最丰富的API之一,并且继承自MonadError-这意味着cats.effect.IOmonix.Task支持它。

还有另一个用于Either/EitherTValidated.valueOr - .valueOr 。 本质上,它与Option .getOrElse ,但对于包含“左侧”内容的类通用。

 import cats.implicits._ val failure = 400.asLeft[String] failure.valueOr(code => s"Got error code $code") //"Got error code 400" 

3)胡同猫


alley-cats是两种情况的便捷解决方案:

  • 不遵守其法律的图块类实例为100%;
  • 异常的辅助性输入错误,可以正确使用。

从历史上看, Try的monad实例在该项目中最受欢迎,因为就致命错误而言, Try不能满足所有的monadic律。 现在,他真正地被介绍给猫。

尽管如此,我还是建议您熟悉此模块 ,它对您似乎很有用。

2)负责任地对待进口


您必须从文档,书籍或其他地方知道猫使用特定的导入层次结构:

cats.x用于基本(内核)类型;
cats.data用于验证类型,monad转换器等数据类型;
cats.syntax.x._支持扩展方法,以便您可以调用sth.asRight,sth.pure等。
cats.instances.x. _将各种类型类的实现直接导入到各个具体类型的隐式范围中,这样,在调用sth.pure时,不会发生“找不到隐式”错误。

当然,您注意到了cats.implicits._的导入,该导入了隐式范围内类型类的所有语法和所有实例。

原则上,在使用Cats开发时,您应该从FAQ的某些导入序列开始,即:

 import cats._ import cats.data._ import cats.implicits._ 

如果您更好地了解图书馆,则可以将其与自己的品味相结合。 遵循一个简单的规则:

  • cats.syntax.x提供与x相关的扩展语法;
  • cats.instances.x提供了实例类。

例如,如果您需要.asRight (这是Either的扩展方法), Either执行以下操作:

 import cats.syntax.either._ "a".asRight[Int] //Right[Int, String](a) 

另一方面,要获取Option.pure您必须导入cats.syntax.monad cats.instances.option

 import cats.syntax.applicative._ import cats.instances.option._ "a".pure[Option] //Some(a) 

通过手动优化导入,您将限制Scala文件中的隐式作用域,从而减少编译时间。

但是,请:如果不满足以下条件,请不要这样做:

  • 您已经很好地掌握了Cats
  • 您的团队拥有同一级别的图书馆

怎么了 因为:

 //  ,   `pure`, //    import cats.implicits._ import cats.instances.option._ "a".pure[Option] //could not find implicit value for parameter F: cats.Applicative[Option] 

这是因为cats.implicitscats.instances.option都是cats.instances.option的扩展。 实际上,我们将其隐式作用域导入两次,而不是使编译器混淆。

而且,隐式层次结构中没有魔术-这是类型扩展的明确序列。 您只需要参考cats.implicits的定义并检查类型层次结构即可。

在大约10到20分钟的时间里,您可以进行足够的研究来避免出现此类问题-相信我,这项投资肯定会有所回报。

1)不要忘了猫更新!


您可能会认为您的FP库是永恒的,但实际上scalazscalaz积极更新。 以猫为例。 以下是最新更改:


因此,在处理项目时,请不要忘记检查库的版本,阅读新版本的注释并及时进行更新。

Source: https://habr.com/ru/post/zh-CN448128/


All Articles