Scalaz для ежедневного использования. Часть 1. Классы типов и расширения Scala.
Многие из вас наверно слышали о замечательной JavaScript книге “JavaScript the good parts”. В подобном ключе я бы хотел рассказать о некоторых вещах из Scalaz, которые действительно здорово использовать в повседневных проектах без необходимости вникать в то что происходит внутри Scalaz. В первой части мы рассмотрим несколько полезных классов типов. В будущих частях мы рассмотрим такие вещи как монадные трансформеры, свободные монады, Validation и т.д.
В наших примерах мы будем использовать Scala REPL. Для этого запустим scala
, добавим библиотеку scalaz и импортируем нужные нам пакеты:
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._
В данной статье мы рассмотрим следующие классы типов из библиотеки Scalaz:
- Equals: для типобезопасной операции сравнения на равенство.
- Order: для немного более типобезопасного отношения порядка.
- Enum: для создания богатых типов перечеслений.
Кроме того, мы также рассмотрим несколько простых расширений, которые добавляет Scalaz, для некоторых типов из стандартной библиотеки. Мы не будем рассматривать всё что добавляет Scalaz и остановимся на паре расширений для Option и Boolean.
Полезные классы типов
С помощью классов типов можно легко добавить функциональность к существующим классам. Scalaz уже содержит в себе несколько полезных классов типов, которые вы можете сразу же использовать.
Типобезопасный оператор сравнения
В Scalaz есть типобезопасный оператор сравнения на равенство, который выдаёт ошибку компиляции если мы пытаемся сравнить объекты разных типов. Таким образом, в то время как ==
и !=
из стандартной библиотеки позволят вам сравнивать, например, объекты классов String и Int, использование операторов ===
и =/=
из Scalaz приведет к ошибке компиляции:
scala> 1 == 1
res6: Boolean = true
scala> 1 === 1
res7: Boolean = true
scala> 1 == "1"
res8: Boolean = false
scala> 1 === "1"
<console>:14: error: type mismatch;
found : String("1")
required: Int
1 === "1"
Scalaz предоставляет следующий набор операторов поведение которых легко понять из реализации:
final def ===(other: F): Boolean = F.equal(self, other)
final def /==(other: F): Boolean = !F.equal(self, other)
final def =/=(other: F): Boolean = /==(other)
final def ≟(other: F): Boolean = F.equal(self, other)
final def ≠(other: F): Boolean = !F.equal(self, other)
Класс типов Order
Этот очень простой класс типов позволяет более типобезопасно пользоваться отношением порядка. Так же, как и с операторами из Equals теперь мы можем поймать сравнение двух объектов разных типов во время компиляции:
scala> 1 < 4d
res25: Boolean = true
scala> 1 lte 4d
<console>:14: error: type mismatch;
found : Double(4.0)
required: Int
1 lte 4d
scala> 1 ?|? 1
res31: scalaz.Ordering = EQ
scala> 1 ?|? 2
res32: scalaz.Ordering = LT
scala> 1 ?|? 2d
<console>:14: error: type mismatch;
found : Double(2.0)
required: Int
1 ?|? 2d
Scalaz предоставляет для этого следующие операторы:
final def <(other: F): Boolean = F.lessThan(self, other)
final def <=(other: F): Boolean = F.lessThanOrEqual(self, other)
final def >(other: F): Boolean = F.greaterThan(self, other)
final def >=(other: F): Boolean = F.greaterThanOrEqual(self, other)
final def max(other: F): F = F.max(self, other)
final def min(other: F): F = F.min(self, other)
final def cmp(other: F): Ordering = F.order(self, other)
final def ?|?(other: F): Ordering = F.order(self, other)
final def lte(other: F): Boolean = F.lessThanOrEqual(self, other)
final def gte(other: F): Boolean = F.greaterThanOrEqual(self, other)
final def lt(other: F): Boolean = F.lessThan(self, other)
final def gt(other: F): Boolean = F.greaterThan(self, other)
Класс типов Enum
С Enum из Scalaz очень легко создавать типы перечисления, которые имеют больше функциональных возможностей, чем те, что находятся в стандартных библиотеках Scala и Java. Scalaz предоставляет для этого следующие функции:
final def succ: F = F succ self
final def -+-(n: Int): F = F.succn(n, self)
final def succx: Option[F] = F.succx.apply(self)
final def pred: F = F pred self
final def ---(n: Int): F = F.predn(n, self)
final def predx: Option[F] = F.predx.apply(self)
final def from: EphemeralStream[F] = F.from(self)
final def fromStep(step: Int): EphemeralStream[F] = F.fromStep(step, self)
final def |=>(to: F): EphemeralStream[F] = F.fromTo(self, to)
final def |->(to: F): List[F] = F.fromToL(self, to)
final def |==>(step: Int, to: F): EphemeralStream[F] = F.fromStepTo(step, self, to)
final def |-->(step: Int, to: F): List[F] = F.fromStepToL(step, self, to)
Очень хороший пример можно найти на StackOverflow, который всё же требует нескольких изменений для того чтобы получить все вкусности Scala. Следующий код показывает как использовать данное перечесление:
scala> import scalaz.Ordering._
import scalaz.Ordering._
scala> :paste
// Entering paste mode (ctrl-D to finish)
case class Coloring(val toInt: Int, val name: String)
object Coloring extends ColoringInstances {
val RED = Coloring(1, "RED")
val BLUE = Coloring(1, "BLUE")
val GREEN = Coloring(1, "GREEN")
}
sealed abstract class ColoringInstances {
import Coloring._
implicit val coloringInstance: Enum[Coloring] with Show[Coloring] = new Enum[Coloring] with Show[Coloring] {
def order(a1: Coloring, a2: Coloring): Ordering = (a1, a2) match {
case (RED, RED) => EQ
case (RED, BLUE | GREEN) => LT
case (BLUE, BLUE) => EQ
case (BLUE, GREEN) => LT
case (BLUE, RED) => GT
case (GREEN, RED) => GT
case (GREEN, BLUE) => GT
case (GREEN, GREEN) => EQ
}
def append(c1: Coloring, c2: => Coloring): Coloring = c1 match {
case Coloring.RED => c2
case o => o
}
override def shows(c: Coloring) = c.name
def zero: Coloring = Coloring.RED
def succ(c: Coloring) = c match {
case Coloring.RED => Coloring.BLUE
case Coloring.BLUE => Coloring.GREEN
case Coloring.GREEN => Coloring.RED
}
def pred(c: Coloring) = c match {
case Coloring.GREEN => Coloring.BLUE
case Coloring.BLUE => Coloring.RED
case Coloring.RED => Coloring.GREEN
}
override def max = Some(GREEN)
override def min = Some(RED)
}
}
// Exiting paste mode, now interpreting.
defined class Coloring
defined object Coloring
defined class ColoringInstances
Теперь мы можем использовать все функции определённые в Scalaz Enum:
scala> import Coloring._
import Coloring._
scala> RED
res0: Coloring = Coloring(1,RED)
scala> GREEN
res1: Coloring = Coloring(1,GREEN)
scala> RED |-> GREEN
res2: List[Coloring] = List(Coloring(1,RED), Coloring(1,BLUE), Coloring(1,GREEN))
scala> RED succ
warning: there was one feature warning; re-run with -feature for details
res3: Coloring = Coloring(1,BLUE)
scala> RED -+- 1
res4: Coloring = Coloring(1,BLUE)
scala> RED -+- 2
res5: Coloring = Coloring(1,GREEN)
Правда впечатляет? Это действительно очень хороший способ создания гибких и богатых перечислений.
Расширения стандартных классов
Как мы уже говорили в начале статьи, мы рассмотрим как Scalaz делает стандартную библиотеку более богатой и добавляет новую функциональность к стандартным классам.
####Веселимся с Option
С классом типов Optional Scalaz делает работу с Option проще. Например, он предоставляет функции для более простого конструирования:
scala> Some(10)
res11: Some[Int] = Some(10)
scala> None
res12: None.type = None
scala> some(10)
res13: Option[Int] = Some(10)
scala> none[Int]
res14: Option[Int] = None
Мы видим что результирующим типом является Option[T] вместо Some или None. Вам может быть не понятно где это может быть полезно, но давайте рассмотрим следующую ситуацию: скажем, у нас есть список Option к которому мы хотим применить свёртку:
scala> val l = List(Some(10), Some(20), None, Some(30))
l: List[Option[Int]] = List(Some(10), Some(20), None, Some(30))
scala> l.foldLeft(None) { (el, z) => el.orElse(z) }
<console>:22: error: type mismatch;
found : Option[Int]
required: None.type
l.foldLeft(None) { (el, z) => el.orElse(z) }
Данный код упадёт с ошибкой, потому что результат свёртки должен быть None.type, а не Option. Когда мы используем функции из Scalaz это работает так как мы и ожидали:
И конечно же Scalaz добавляет новых операторов:
// Альтернатива getOrElse
scala> Some(10) | 20
res29: Int = 10
scala> none | 10
res30: Int = 10
// Тернарный оператор
scala> Some(10) ? 5 | 4
res31: Int = 5
// ~ : Возвращает элемент хранимый в Option, если он там есть, а иначе "ноль" типа A
scala> some(List())
res32: Option[List[Nothing]] = Some(List())
scala> ~res32
res33: List[Nothing] = List()
scala> some(List(10))
res34: Option[List[Int]] = Some(List(10))
scala> ~res34
res35: List[Int] = List(10)
Ничего слишком сложного, просто несколько вспомогательных функций. Существует множество двугих интересных вещей вокруг Option в Scalaz, но это уже выходит за рамки данной статьи.
Более функциональный Boolean
Scalaz так же добавляет больше функциональности к типу Boolean:
# Тернарный опертор возвращается!
scala> true ? "This is true" | "This is false"
res45: String = This is true
scala> false ? "This is true" | "This is false"
res46: String = This is false
# Возвращает передаваемый аргумент в случае true, иначе возвращает "ноль" типа A
scala> false ?? List(120,20321)
res55: List[Int] = List()
scala> true ?? List(120,20321)
res56: List[Int] = List(120, 20321)
И целый список операторов для бинарной арифметики:
// Conjunction. (AND)
final def ∧(q: => Boolean) = b.conjunction(self, q)
// Conjunction. (AND)
final def /\(q: => Boolean) = ∧(q)
// Disjunction. (OR)
final def ∨(q: => Boolean): Boolean = b.disjunction(self, q)
// Disjunction. (OR)
final def \/(q: => Boolean): Boolean = ∨(q)
// Negation of Disjunction. (NOR)
final def !||(q: => Boolean) = b.nor(self, q)
// Negation of Conjunction. (NAND)
final def !&&(q: => Boolean) = b.nand(self, q)
// Conditional.
final def -->(q: => Boolean) = b.conditional(self, q)
// Inverse Conditional.
final def <--(q: => Boolean) = b.inverseConditional(self, q)
// Bi-Conditional.
final def <-->(q: => Boolean) = b.conditional(self, q) && b.inverseConditional(self, q)
// Inverse Conditional.
final def ⇐(q: => Boolean) = b.inverseConditional(self, q)
// Negation of Conditional.
final def ⇏(q: => Boolean) = b.negConditional(self, q)
// Negation of Conditional.
final def -/>(q: => Boolean) = b.negConditional(self, q)
// Negation of Inverse Conditional.
final def ⇍(q: => Boolean) = b.negInverseConditional(self, q)
// Negation of Inverse Conditional.
final def <\-(q: => Boolean) = b.negInverseConditional(self, q)
Например:
scala> true /\ true
res57: Boolean = true
scala> true /\ false
res58: Boolean = false
scala> true !&& false
res59: Boolean = true
Больше дополнительных функций
В данной статье мы рассмотрели всего несколько дополнительных функций из Scalaz. Если вас заинтересовала эта тема вам стоит заглянуть в код следующих классов:
- scalaz.syntax.std.BooleanOps
- scalaz.syntax.std.ListOps
- scalaz.syntax.std.MapOps
- scalaz.syntax.std.OptionOps
- scalaz.syntax.std.StringOps
Ещё немного примеров:
Балуемся с List
# взять хвост как Option
scala> List(10,20,30)
res60: List[Int] = List(10, 20, 30)
scala> res60.tailOption
res61: Option[List[Int]] = Some(List(20, 30))
scala> List()
res64: List[Nothing] = List()
scala> res64.tailOption
res65: Option[List[Nothing]] = None
# "усыпать" список дополнительными элементами
scala> List(10,20,30)
res66: List[Int] = List(10, 20, 30)
scala> res66.intersperse(1)
res68: List[Int] = List(10, 1, 20, 1, 30)
# всевозможные перестановки списка
scala> List('a','b','c','d').powerset
res71: List[List[Char]] = List(List(a, b, c, d), List(a, b, c), List(a, b, d), List(a, b), List(a, c, d), List(a, c), List(a, d), List(a), List(b, c, d), List(b, c), List(b, d), List(b), List(c, d), List(c), List(d), List())
Забавляемся с Map
# безопасно изменить запись
res77: scala.collection.immutable.Map[Char,Int] = Map(a -> 10, b -> 20)
scala> res77.alter('a')(f => f |+| some(5))
res78: Map[Char,Int] = Map(a -> 15, b -> 20)
# пересечь два Map'а и определить какое значение оставить для каждого из ключей
scala> val m1 = Map('a' -> 100, 'b' -> 200, 'c' -> 300)
m1: scala.collection.immutable.Map[Char,Int] = Map(a -> 100, b -> 200, c -> 300)
scala> val m2 = Map('b' -> 2000, 'c' -> 3000, 'd' -> 4000)
m2: scala.collection.immutable.Map[Char,Int] = Map(b -> 2000, c -> 3000, d -> 4000)
scala> m1.intersectWith(m2)((m1v,m2v) => m2v)
res23: Map[Char,Int] = Map(b -> 2000, c -> 3000)
scala> m1.intersectWith(m2)((m1v,m2v) => m1v)
res24: Map[Char,Int] = Map(b -> 200, c -> 300)
Развлекаемся с String
# сделать строку множественного числа(наивный способ)
scala> "Typeclass".plural(1)
res26: String = Typeclass
scala> "Typeclass".plural(2)
res27: String = Typeclasss
scala> "Day".plural(2)
res28: String = Days
scala> "Weekly".plural(2)
res29: String = Weeklies
# безопасный парсинг значений типов Boolean, Byte, Short, Long, Float, Double и Int
scala> "10".parseDouble
res30: scalaz.Validation[NumberFormatException,Double] = Success(10.0)
scala> "ten".parseDouble
res31: scalaz.Validation[NumberFormatException,Double] = Failure(java.lang.NumberFormatException: For input string: "ten")
Заключение
Я надеюсь, что вам понравилось это краткое введение в Scalaz. И как вы уже видели эти простые функции уже позволяют делать множество интересных вещей, без необходимости разбираться с внутренними сложностями Scalaz. Паттерн рассматриваемый здесь называется TypeClass Pattern и используется для расширения стандартной функциональности типов в Scala.
В следующей статье мы познакомимся с более сложными вещами и поработаем с монадными трансформерами.