1. Introduction

In this tutorial, we’ll explore the features of Scalaz, a Scala library for functional programming. Scalaz ships with purely functional data structures and type classes.

2. Dependencies

To start, let’s add the Scalaz library to our build.sbt:

libraryDependencies += "org.scalaz" %% "scalaz-core" % "7.3.7"

3. Equals

Scala provides the double equals operator (==) for comparing any two values for equality. However, there’s a lack of type-safety. Comparing the values of different types will always yield false. Luckily, Scalaz provides us a *triple equals operator (===) to add the type-safety* we need. Using === to compare values of different types will result in a compile-time failure:

Type Mismatch.
Required: Int
Found: String

This forces us to think about the code and possibly change our approach. Similarly, Scalaz also provides an inequality check operator =/= to use instead of Scala’s != which is equally not type-safe:

assertTrue(15 === 15)
assertTrue(15 =/= 25)

4. Order

Order is a type class that gives us a rich set of operators for comparison of values. Typically, if we want anything to be sortable or ordered, we extend scala.math.Ordering and override the compare method. One of the strengths of Order over Ordering is the presence of convenience operators like <*, *>, >=, and <=. However, these can also be found in the scala.math.Ordered trait. So, why should we care about Order when Scala ships with Ordering and Ordered, which can achieve our objective? One clear advantage is that the Scala operators are not type-safe. This call would fail with Scalaz while it’s valid in Scala:

3 < 5.6

Additionally, we may want to add comparison behavior to a third-party class whose code we don’t have access to. In this case, we don’t have the option to make that class extend Ordering or Ordered. But by using implicits, we can add this behavior to any class. Let’s assume we’re dealing with a domain object called Score that wraps the scores of students:

case class Score(amt: Double)

We want to make sure one instance of Score is comparable to another, so let’s create an implicit object to enable ordering:

implicit object scoreOrdered extends Order[Score] {
  override def order(x: Score, y: Score): Ordering =
    x.amt compare y.amt match {
      case 1  => Ordering.GT
      case 0  => Ordering.EQ
      case -1 => Ordering.LT
    }
}

As a consequence, wherever this implicit object is visible, we can apply a diverse set of operations to compare values of type Score:

assertTrue(Score(0.9) > Score(0.8))
assertTrue(Score(0.9) gt Score(0.8))
assertTrue(Score(0.9) gte Score(0.8))

Additionally, we can also get the ordering relationship as an instance of scalaz.Ordering:

assertEquals(Ordering.GT, Score(0.9) ?|? Score(0.8))
assertEquals(Ordering.LT, Score(0.8) ?|? Score(0.9))
assertEquals(Ordering.EQ, Score(0.9) ?|? Score(0.9))

5. Show

The main purpose of Show is to provide an idiom for String representation. It works like the toString method, but some classes in third-party libraries are final, preventing us from overriding their toString methods. When combined with the concept of implicits, Show can be really convenient where toString falls short. Scalaz provides Show implicit for standard data types like Int, Double, and Float. Calling the method show on an object returns a Cord, which is a purely functional data structure for efficiently storing and manipulating Strings that are potentially very long. Implementations of Show contain another convenience method called shows that returns the actual String representation instead of a Cord:

assertEquals("3", 3.shows)
assertEquals("3.4", 3.4.shows)

Finally, to create a custom Show implementation, we can create an implicit object for the target class, extending Show[Class<?>]:

implicit object threadShow extends Show[Thread] {
  override def show(f: Thread): Cord =
    Cord(s"Thread id = ${f.getId}, name = ${f.getName}")
}

6. Enum

An Enum is a data type that makes it possible to set the value of a variable to a bounded type. This means that the variable can only contain one of a predefined set of values at any one time. For example, a variable representing letters in an alphabet can only contain values A to Z, while one for days of the week can only contain one of MONDAY through SUNDAY. Scalaz provides powerful operators for dealing with enums in the Enum class. Given a range of characters, we can easily generate an Enum out of it. Furthermore, we can convert the Enum to a standard Scala list if that’s more appropriate for our use case:

val enum = 'a' |-> 'g'
val enumAsList = enum.toList

val expectedResult = IList('a', 'b', 'c', 'd', 'e', 'f', 'g')
val expectedResultAsList = List('a', 'b', 'c', 'd', 'e', 'f', 'g')

assertEquals(expectedResult, enum)
assertEquals(expectedResultAsList, enumAsList)

When our starting point is also a single Enum value, we can use .pred and .succ to get the previous or next value, respectively:

assertEquals('c', 'b'.succ)
assertEquals('a', 'b'.pred)

We can also retrieve an Enum value that is n steps ahead of the provided value:

assertEquals('e', 'b' -+- 3)
assertEquals('f', 'b' -+- 4)

Or we can retrieve one that is n steps behind:

assertEquals('b', 'e' --- 3)
assertEquals('a', 'c' --- 2)

We can also attempt to get the minimum and maximum values of an Enum. Scalaz can get an Enum view of standard bounded types like Int and Double. A result is an Option, so when it’s not possible to compute the minimum and maximum values, we get an empty value:

assertEquals(Some(-2147483648), Enum[Int].min)
assertEquals(Some(2147483647), Enum[Int].max)

To use min and max successfully, the Enum must have defined these. To step through the Enum as we saw earlier, it must have implemented Ordering. Let’s say we want to create a priority Enum to reflect the different priorities we can assign to tickets we’re working on. First, we create a case class for the priority object:

case class Priority(num: Int, name: String)

Next, let’s create a few known priority instances:

val HIGH = Priority(1, "HIGH") 
val MEDIUM = Priority(2, "MEDIUM") 
val LOW = Priority(3, "LOW")

Finally, we create an implicit object that extends Enum:

implicit object PriorityEnum extends Enum[Priority] {
  def order(p1: Priority, p2: Priority): Ordering =
    (p1.num compare p2.num) match {
      case -1 => Ordering.LT
      case 0  => Ordering.EQ
      case 1  => Ordering.GT
    }

  def succ(s: Priority): Priority = s match {
    case LOW    => MEDIUM
    case MEDIUM => HIGH
    case HIGH   => LOW
  }

  def pred(s: Priority): Priority = s match {
    case LOW    => HIGH
    case MEDIUM => LOW
    case HIGH   => MEDIUM
  }

  override def max = Some(HIGH)
  override def min = Some(LOW)
}

We can now run all the previous tests on our new enum:

// generate range
val expectedEnum = IList(Priority(1, "LOW"), Priority(2, "MEDIUM"), Priority(3, "HIGH"))
assertEquals(expectedEnum, LOW |-> HIGH)

//range to list
assertEquals(expectedEnum.toList, (LOW |-> HIGH).toList)

//pred and succ
assertEquals(HIGH, LOW.pred)
assertEquals(HIGH, MEDIUM.succ)

//step forward and back
assertEquals(MEDIUM, HIGH -+- 2)
assertEquals(LOW, LOW --- 3)

//min and max
assertEquals(Some(Priority(3, "HIGH")), Enum[Priority].max)
assertEquals(Some(Priority(1, "LOW")), Enum[Priority].min)

7. Option Operations

Scalaz ships with a number of new constructs for Option operations. To some extent, many of them make life easier. Let’s take a look at a few. Creating an Option using Scalaz’s some construct, this internally calls the apply method on Scala’s Some — just notice the case difference for S in Some and N in None:

assertEquals(Some(12), some(12))
assertEquals(None, none[Int])

Alternatively, we can create an Option by calling some directly on a value:

assertEquals(Some(13), 13.some)
assertEquals(Some("baeldung"), "baeldung".some)

Additionally, we have operations to use for extracting Option values. One of the most common is the some/none operator. It works more like getOrElse, however, when a value exists, it executes a function passed to some part of the operator. The none part only returns a fallback value:

val opt = some(12)
val value1 = opt some { a =>
  a
} none 0

val value2 = opt
  .some(_ * 2)
  .none(0)

assertEquals(12, value1)
assertEquals(24, value2)

Another operator for extracting Option values is the pipe operator. It takes two operands: On the left is the Option, and on the right is the default value to return when the Option is empty:

val opt = some(12)
val opt2 = none[Int]

assertEquals(12, opt | 0)
assertEquals(5, opt2 | 5)

The last operator we’ll look at is the unary (~) operator. It extracts the value of the Option, if present, and the zero value of the type if absent. For example, in the case of an integer, the zero value is 0, and for a string, it’s the empty string “”.

assertEquals(25, ~some(25))
assertEquals(0, ~none[Int])
assertEquals("baeldung", ~some("baeldung"))
assertEquals("", ~none[String])

8. String Operations

Scalaz provides some operators for conveniently working with strings. The one that really stands out is the plural operator. It returns the plural form of the word given to it:

assertEquals("apples", "apple".plural(2))
assertEquals("tries", "try".plural(2))
assertEquals("range rovers", "range rover".plural(2))

9. Boolean Operations

A number of convenience utilities for operating on boolean types are also available. Let’s look at how to fold over a boolean value and choose one of two values of any type based on whether the boolean is true or false:

val t = true
val f = false

val expectedValueOnTrue = "it was true"
val expectedValueOnFalse = "it was false"

val actualValueOnTrue = t.fold[String](expectedValueOnTrue, expectedValueOnFalse)
val actualValueOnFalse = f.fold[String](expectedValueOnTrue, expectedValueOnFalse)

assertEquals(expectedValueOnTrue, actualValueOnTrue)
assertEquals(expectedValueOnFalse, actualValueOnFalse)

The option operator allows us to return a value as an Option based on a boolean flag:

val restrictedData = "Some restricted data"

val actualValueOnTrue = true option restrictedData
val actualValueOnFalse = false option restrictedData

assertEquals(Some(restrictedData), actualValueOnTrue)
assertEquals(None, actualValueOnFalse)

One practical use for the option operator on boolean would be in authorization systems where, depending on the logged-in user’s privilege level, we either return or hide some fields. Several Scala developers with a Java background miss the inline if-statement with a ternary operator. Something similar is available in Scalaz:

val t = true
val f = false

assertEquals("true", t ? "true" | "false")
assertEquals("false", f ? "true" | "false")

Sometimes we want to return a value if a certain condition is true or default to the zero-value of the data type we’re dealing with. Scalaz gives us a very concise way of doing this:

val t = true
val f = false

assertEquals("string value", t ?? "string value")
assertEquals("", f ?? "string value")

assertEquals(List(1, 2, 3), t ?? List(1, 2, 3))
assertEquals(List(), f ?? List(1, 2, 3))

assertEquals(5, t ?? 5)
assertEquals(0, f ?? 5)

The inverse of the above operator is !?, and when used, it returns the value when the condition evaluates to false and the zero-value of the type when true.

10. Map Operations

The Map is a very popular data structure in our day-to-day programming. It’s not a surprise, then, that Scalaz has some rich features for dealing with maps beyond what’s available in standard Scala.

10.1. alter

alter is a higher-order function that takes a key as a single parameter in the first parameter group and an anonymous function in the second parameter group. It passes the result of calling get on the Map to the anonymous function as an Option. We can write code in the anonymous function that creates a new value for the passed-in key, and the overall result is a new Map with an altered value for that key:

val map = Map("a" -> 1, "b" -> 2)

val mapAfterAlter1 = map.alter("b") { maybeValue =>
  maybeValue
    .some(v => some(v * 10))
    .none(some(0))
}
val mapAfterAlter2 = map.alter("c") { maybeValue =>
  maybeValue
    .some(v => some(v * 10))
    .none(some(3))
}

assertEquals(Map("a" -> 1, "b" -> 20), mapAfterAlter1)
assertEquals(Map("a" -> 1, "b" -> 2, "c" -> 3), mapAfterAlter2)

10.2. Intersection

*The intersectWith function takes two Maps and returns a new Map that is their intersection*. Additionally, it’s also a higher-order function that takes an anonymous function. It applies the function in turn to the values of any intersecting keys, and the result is set as the final value of the intersecting key:

val m1 = Map("a" -> 1, "b" -> 2)
val m2 = Map("b" -> 2, "c" -> 3)
val m3 = Map("a" -> 5, "b" -> 8)

assertEquals(Map("b" -> 4), m1.intersectWith(m2)(_ + _))
assertEquals(Map("b" -> 4), m1.intersectWith(m2)((v1, v2) => v1 * v2))
assertEquals(Map("a" -> -4, "b" -> -6), m1.intersectWith(m3)(_ - _))

10.3. mapKeys

There’s also a function to map over keys in the Map and update them according to the logic we’ve provided:

val m1 = Map("a" -> 1, "b" -> 2)

assertEquals(Map("A" -> 1, "B" -> 2), m1.mapKeys(_.toUpperCase))

10.4. Union

We can also *get the union of two Maps by key*, and when there are overlapping keys, a function is applied to them. The result of this function becomes the value of that key in the new Map:

val m1 = Map("a" -> 1, "b" -> 2)
val m2 = Map("b" -> 2, "c" -> 3)

assertEquals(Map("a" -> 1, "b" -> 4, "c" -> 3), m1.unionWith(m2)(_ + _))

10.5. insertWith

Finally, the insertWith function allows us to insert a key-value pair into the Map and also takes a function. The function simply handles duplicate keys. Instead of overwriting the existing value, as is the standard case, it takes the two values and applies the function to them, making the result the new value of the conflicting key:

val m1 = Map("a" -> 1, "b" -> 2)

val insertResult1 = m1.insertWith("a", 99)(_ + _)
val insertResult2 = m1.insertWith("c", 99)(_ + _)

val expectedResult1 = Map("a" -> 100, "b" -> 2)
val expectedResult2 = Map("a" -> 1, "b" -> 2, "c" -> 99)

assertEquals(expectedResult1, insertResult1)
assertEquals(expectedResult2, insertResult2)

11. NonEmptyList

One of the most common data structures we use is scala.collection.immutable.List. One problem with List is the need to check if it’s not empty before using it in some scenarios. Let’s say we want to get the head of the list:

assertEquals(1, List(1).head)

One common error that requires the non-empty check is when calling head on an empty List:

@Test(expected = classOf[NoSuchElementException])
def givenEmptyList_whenFetchingHead_thenThrowsException(): Unit = {
  List().head
}

The key takeaway with NonEmptyList is the guarantee that the List we’re dealing with will never be empty, so we don’t need to add any extra checks before using it. It’s similar to the guarantee that a value will never be null when wrapped in an Option. Let’s look at a few ways of creating NonEmptyLists:

//wrap a value in a non-empty list
val nel1 = 1.wrapNel
assertEquals(NonEmptyList(1), nel1)

//standard apply
val nel2 = NonEmptyList(3, 4)

//cons approach
val nel3 = 2 <:: nel2

assertEquals(NonEmptyList(2, 3, 4), nel3)

//append
val nel4 = nel1 append nel3
assertEquals(NonEmptyList(1, 2, 3, 4), nel4)

Since a NonEmptyList is also a List, all the standard List methods apply to it as well.

12. Lens

Lens is a functional programming abstraction that helps to solve the common problem of updating complex immutable nested case classes. Suppose we have a user accounts module in a web application, with some domain objects:

case class UserId(id: Long)
case class FullName(fname: String, lname: String)
case class User(id: UserId, name: FullName)

In some cases, we may want to modify the values in one or more of the domain objects. We’re using the word “modify” loosely here since we’re dealing with immutable structures — these operations actually result in new objects. Using standard Scala, to modify the name field in User:

val name = FullName(fname = "John", lname = "Doe")
val userId = UserId(10)
val user = User(userId, name)

val updatedName = FullName("Marcus", "Aurelius")
val actual = user.copy(name = updatedName)

assertEquals(User(userId, updatedName), actual)

This is fairly concise. But as we described earlier, the power of lenses is seen when objects are deeply nested. Let’s try to modify only the first name in the name field of a User object:

val updatedName = FullName("Jane", "Doe")
val actual = user.copy(name = name.copy(fname = "Jane"))

assertEquals(User(userId, updatedName), actual)

If the nesting keeps getting deeper, we could end up with an update code that looks like this:

user.copy(name = name.copy(fname = fname.copy(xxx)))

With lenses, we can update a field at any level of nesting as though it was at the first level. Let’s rewrite our previous examples by creating lenses for the objects we want to update:

val userFullName = Lens.lensu[User, FullName](
    (user, name) => user.copy(name = name),
    _.name
  )

The above Lens is for the name object on User. It takes the two types, the first being the parent object type and the next being the type of child we want to access the object. It also takes a setter and getter for that object. With the above Lens in scope, we can now change the update code we saw earlier:

val updatedName = FullName("Marcus", "Aurelius")
val actual = userFullName.set(user, updatedName)

assertEquals(User(userId, updatedName), actual)

To update nested fields, we’ll need to chain lenses at every level of nesting until we reach the target field. In our case, we want to update the fname field in the name object in the user object. This requires that we have a Lens from user to name and chain it to another lens from name to fname. Since we already have the user -> name Lens, we only need to create the name -> fname lens:

val firstName = Lens.lensu[FullName, String](
    (fullName, firstName) => fullName.copy(fname = firstName),
    _.fname
  )

At this point, Scalaz gives us several operators we can use, such as compose and andThen. They’re aliased with <=<* and *>=> respectively. Let’s take a look at the user -> fname Lens:

val userFirstName = userFullName >=> firstName

This means that we want to make an update through user to the name object and then from name to the firstName field:

val name = FullName(fname = "John", lname = "Doe")
val userId = UserId(10)
val user = User(userId, name)

val updatedName = FullName("Jane", "Doe")
val actual = userFirstName.set(user, "Jane")

assertEquals(User(userId, updatedName), actual)

With the new Lens in scope, we’re now able to make an update to the nested fname field as though it was a first-level child of user.

13. Conclusion

In this article, we’ve explored the features of the Scalaz library. As usual, all the source code is available over on Github.


» 下一篇: Scala中的单子