Статьи

Функции Scalaz для повседневного использования, часть 1: классы типов и расширения Scala

Большинство из вас, вероятно, слышали о великой книге Javascript: Javascript хорошие части. В этом же свете я хотел бы показать некоторые вещи из Scalaz, которые действительно здорово использовать в повседневных проектах, без необходимости погружаться в (по крайней мере, для меня) страшную внутреннюю работу Scalaz. В этой первой части мы углубимся в ряд полезных классов типов. В следующих частях мы рассмотрим такие вещи, как монадные трансформаторы, бесплатные монады, валидация и т. Д.

Для этих примеров мы будем использовать scala REPL. Поэтому, если вы хотите поиграть, запустите scala и загрузите библиотеку scala:

1
2
3
4
5
6
7
8
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:

  1. Класс типов equals: для безопасных операторов equals.
  2. Класс типов заказа: для более безопасного заказа
  3. Класс типов Enum : для создания многофункциональных перечислений

Кроме того, мы также рассмотрим несколько простых расширений, которые Scalaz добавляет к некоторым типам, предоставляемым стандартной библиотекой Scala. Мы не будем смотреть на все, что добавляет Scalaz, а только на несколько расширений Option и Boolean .

Полезные классы типов

С помощью классов типов вы можете легко добавить функциональность к существующим классам (см. Шаблон Pimp my library ). Scalaz поставляется с парой полезных классов типов, которые вы можете сразу использовать.

Оператор Typesafe Equals

Scalaz предоставляет типизированный оператор equals, который выдаст ошибку компиляции при сравнении недопустимых типов. Так что, хотя == и ! = В Scala позволят вам сравнивать String и Int, используя операторы Scalaz === и = / =, это приведет к ошибке времени компиляции:

01
02
03
04
05
06
07
08
09
10
11
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 предоставляет следующий набор операторов, поведение которых можно легко увидеть из реализации функции.

1
2
3
4
5
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)

Тип заказа

Это очень простой класс типов, который обеспечивает более безопасный тип заказа. Как и в случае с операторами Equals, теперь мы можем отследить сравнение двух разных типов во время компиляции:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
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 предоставляет следующий набор операторов:

01
02
03
04
05
06
07
08
09
10
11
12
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)

Тип перечисления

С типом Scalaz Enum очень легко создавать перечислители, которые обладают большей функциональностью, чем в стандартных библиотеках Scala или Java. Он предоставляет ряд функций для прохождения через Enum и помогает в создании новых, подмножеств из них. Scalaz предоставляет следующий набор функций:

01
02
03
04
05
06
07
08
09
10
11
12
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: http://stackoverflow.com/questions/28589022/enumeration-concept-in-scala-which-option-to-take , который, однако, требует несколько небольших изменений, чтобы получить все вкусности Скалаза. Следующий код показывает, как использовать это перечисление:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
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 делает стандартную библиотеку Scala более функциональной, добавляя функциональность к некоторым ее стандартным классам.

Больше веселья с опциями

С опциональным классом типов Scalaz упрощает работу с опциями Scala. Например, он предоставляет функцию, облегчающую построение:

01
02
03
04
05
06
07
08
09
10
11
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. Вы можете спросить, почему это полезно, но посмотрите на следующее: скажем, у нас есть список опций, который мы хотим свернуть:

1
2
3
4
5
6
7
8
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, он работает как положено:

1
2
scala> l.foldLeft(none[Int]) { (el, z) => el.orElse(z)  }
res19: Option[Int] = Some(10)

И Скалаз не был бы Скалазом без введения новых операторов.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Alternative for getOrElse
scala> Some(10) | 20
res29: Int = 10
  
scala> none | 10
res30: Int = 10
  
// Ternary operator
scala> Some(10) ? 5 | 4
res31: Int = 5
  
// ~ operator: Returns the item contained in the Option if it is defined, otherwise, the zero element for the type 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)

Ничего сложного, только некоторые вспомогательные функции. Есть много других вещей, связанных с Options в библиотеке Scalaz, но это немного выходит за рамки данной статьи.

Больше логических функций

Scalaz также добавляет некоторые функциональные возможности к логическому типу.

01
02
03
04
05
06
07
08
09
10
11
12
13
# Ternary operations are back!
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
  
# Returns the given argument if this is `true`, otherwise, the zero element for the type of the given argument.
scala> false ?? List(120,20321)
res55: List[Int] = List()
  
scala> true ?? List(120,20321)
res56: List[Int] = List(120, 20321)

И целый список дополнительных операторов для двоичной арифметики:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 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)

Например:

1
2
3
4
5
6
7
8
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

Несколько примеров:

Список веселья

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# get the tail as an 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
  
# intersperse the list with additional elements
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)
  
# from list to List[List] of all possibilities
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())

Карта веселья

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
# alter one entry in a safe manner
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)
  
# intersect two maps, and determine which value to keep of the keys that intersect
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)

Струнное веселье

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
# Make a string plural (in a somewhat naive way)
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
  
# safely parse booleans, bytes, shorts, longs, floats, double and ints
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. Используемый здесь шаблон — это простой шаблон TypeClass, используемый для добавления функциональности в некоторые стандартные функции Scala.

В следующей статье мы рассмотрим некоторые более сложные функции, когда рассмотрим Monad Transformers.