scalaz "For the Rest of Us"

Adam Rosien
arosien@box.com && adam@rosien.net

@arosien #scalasv #scalaz

Box

Presenter Notes

scalaz has a (undeserved?) reputation as being, well, kind of crazy.

So this talk is specifically not about:

  • Functors, Monads, or Applicative Functors
  • Category theory
  • Other really cool stuff you should learn about (eventually)

Presenter Notes

This talk is about every-day situations where scalaz can:

  • Reduce syntactical noise
  • Provide useful types that solve many classes of problems
  • Add type-safety with minimal "extra work"

Presenter Notes

Getting Started

In build.sbt:

1
2
libraryDependencies += 
  "org.scalaz" %% "scalaz-core" % "6.0.4"

Then:

1
2
3
4
import scalaz._
import Scalaz._

// profit

This is scalaz 6. Also, assume this is imported in all code snippets.

Presenter Notes

Memoization

Presenter Notes

Memoization

The goal: cache the result of an expensive computation.

1
2
3
4
5
6
7
def expensive(foo: Foo): Bar = ...

val f: Foo

expensive(f) // $$$
expensive(f) // $$$
...

Assumption: expensive produces the same output for every input, i.e., is referentially-transparent.

Presenter Notes

Memoization

Typically you might use a mutable.Map to cache results:

1
2
3
4
val cache = collection.mutable.Map[Foo, Bar]()

cache.getOrElseUpdate(f, expensive(f)) // $$$
cache.getOrElseUpdate(f, expensive(f)) // 1¢

Downsides: the cache is not the same type as the function: Foo => Bar vs. Map[Foo, Bar]. It's also not DRY.

Presenter Notes

Memoization

You can try to make it look like a regular function, avoiding the getOrElseUpdate() call:

1
2
3
4
5
6
val cache: Foo => Bar = 
  collection.mutable.Map[Foo, Bar]()
    .withDefault(expensive _)

cache(f) // $$$ (miss & NO fill)
cache(f) // $$$ (miss & NO fill)

But it doesn't actually cache.

Presenter Notes

Memoization

In scalaz:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def expensive(foo: Foo): Bar = ...

// Memo[Foo, Bar]
val memo = immutableHashMapMemo { 
  foo: Foo => expensive(foo) 
}

val f: Foo

memo(f) // $$$ (cache miss & fill)
memo(f) // 1¢  (cache hit)

Presenter Notes

Memoization

Many memoization strategies:

1
2
3
4
5
6
7
8
9
immutableHashMapMemo[K, V]

mutableHashMapMemo[K, V]

// remove + gc unreferenced entries
weakHashMapMemo[K, V]

// fixed size, K = Int
arrayMemo[V](size: Int)

Super-nerdy: the memoizing strategies are just functions of K => V, which means the generic memo() constructor has the same signature as the Y-combinator!

Presenter Notes

Memoization

scalaz memoization:

  • Pros: uniform types for memoizer and function
  • Cons: less low-level control

Presenter Notes

Style

Presenter Notes

Style

Remove the need for temporary variables:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
val f: A => B
val g: B => C

// using temps:
val a: A = ...
val b = f(a)
val c = g(b)

// or via composition, which is a bit ugly:
val c = g(f(a))

// "unix-pipey"!
val c = a |> f |> g

Presenter Notes

Style

When you just can't stand all that (keyboard) typing:

1
2
3
4
5
6
7
8
val p: Boolean

// ternary-operator-ish
p ? "yes" | "no" // if (p) "yes" else "no"

val o: Option[String]

o | "meh"        // o.getOrElse("meh")

Presenter Notes

Style

More legible (and more type-safe):

1
2
3
4
5
6
7
8
// scala
Some("foo")  // Some[String]
None         // None.type

// scalaz
"foo".some   // Option[String]
none         // Option[Nothing], oops!
none[String] // Option[String]

Presenter Notes

Style

More legible (and more type-safe):

1
2
3
4
5
6
7
8
9
// scala
Right(42)   // Right[Nothing, Int], oops!
Left("meh") // Left[String, Nothing], oops!
Right[String, Int](42)   // verbose
Left[String, Int]("meh") // verbose

// scalaz
42.right[String] // Either[String, Int]
"meh".left[Int]  // Either[String, Int]

Presenter Notes

Style

Pros: less noise, more expressive, more type-safe

Cons: you have to know these operators

Presenter Notes

Domain Validation

Presenter Notes

Domain Validation

This isn't good:

1
2
3
4
5
6
7
case class SSN(
  first3: Int, 
  second2: Int, 
  third4: Int)

SSN(123, 123, 1234) 
//       ^^^ noo!

Presenter Notes

Domain Validation

This shouldn't be possible:

1
2
3
4
case class Version(major: Int, minor: Int)

Version(1, -1) 
//         ^^ noooo!

Presenter Notes

Domain Validation

Meh:

1
2
3
4
5
6
7
case class Dependency(
  organization: String,
  artifactId: String,
  version: Version)

Dependency("zerb", "", Version(1, 2))
//                 ^^ nooooo!

Presenter Notes

Domain Validation

noooooo

Presenter Notes

Domain Validation

The problem is that the types as-is aren't really accurate.
Strings and Ints are being used too broadly. We really want "Ints greater than zero", "Strings that match a pattern", etc.

You can do the checks in the constructor:

1
2
3
4
5
6
7
8
case class Version(major: Int, minor: Int) {
  require(
    major >= 0, 
    "major must be >= 0: %s".format(major))
  require(
    minor >= 0, 
    "minor must be >= 0: %s".format(minor))
}

Presenter Notes

Domain Validation

But this has downsides:

  • Validation failures happen as late as possible.
  • You only get one failure, but more than one violation may be happening.
  • You have to catch exceptions, which is just tedious.

Presenter Notes

Domain Validation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
val major: Int = ...
val minor: Int = ...
val version: ??? =
  Version.validate(major, minor)

version | Version(1, 0) // provide default

// handle failure and success
version.fold(
  fail: ???        => ...,
  success: Version => ...)

Presenter Notes

Domain Validation

Using scalaz, a Validation can either be a Success or Failure:

1
2
3
4
5
Version.validate(1, 2)
// Success(Version(1, 2))

Version.validate(1, -1)
// Failure(NonEmptyList("digit must be >= 0"))

Presenter Notes

Domain Validation

Model the >= 0 constraint:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
case class Version(
  major: Int, // >= 0
  minor: Int) // >= 0

object Version {
  def validDigit(digit: Int):
    Validation[String, Int] = (digit >= 0) ? 
      digit.success[String] | 
      "digit must be >= 0".fail

  ...
}

Presenter Notes

Domain Validation

Combine constraints:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
object Version {
  def validDigit(digit: Int): 
    Validation[String, Int] = ...

  def validate(major: Int, minor: Int) = 
    (validDigit(major).liftFailNel |@| // huh?
     validDigit(minor).liftFailNel) {  // huh?
      Version(_, _)                    // huh?
    }
}

Presenter Notes

Domain Validation

Let's break down validDigit(major).liftFailNel:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
validDigit(major)
// Validation[String, Int]

validDigit(major).liftFailNel
//   lift = do stuff inside
//   fail = only work on the failure side
//   nel  = NonEmptyList
// Validation[NonEmptyList[String], Int]

type ValidationNEL[X, A] =
  Validation[NonEmptyList[X], A]

Presenter Notes

Domain Validation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
val maj = validDigit(major).liftFailNel
val min = validDigit(minor).liftFailNel
// Both ValidationNEL[String, Int]
//                            ^^^

val mkVersion = Version(_, _)
// (Int, Int) => Version

val version = (maj |@| min) { mkVersion }
// ValidationNEL[String, Version]
//                       ^^^^^^^

Presenter Notes

Domain Validation

The general form of combining ValidationNEL:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
(ValidationNEL[X, A] |@|
 ValidationNEL[X, B]) {
  (A, B) => C
} // ValidationNEL[X, C]

(ValidationNEL[X, A] |@|
 ValidationNEL[X, B] |@|
 ValidationNEL[X, C]) {
  (A, B, C) => D
} // ValidationNEL[X, D]

// etc.

Presenter Notes

Domain Validation

The "rules":

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Success |@| Success // Success
Success |@| Failure // Failure
Failure |@| Success // Failure
Failure |@| Failure // Failure 
// and accumulate fail values!

// Accumulate?
NonEmptyList("foo") |+| NonEmptyList("bar")
// NonEmptyList("foo", "bar")

// |+|? "appends" things according to rules

Presenter Notes

Domain Validation

An improvement?

  • Pro: Validation/Success/Failure is nicer than try/catch or Either/Left/Right.
  • Pro: Each rule is just a function producing a Validation.
  • Pro: Rules can be composed together into new validations, of differing types.
  • Pro: Composed rules accumulate all the errors vs. failing fast.
  • Con: liftFailNel, |@|, etc., is incomprehensible if you're not familiar.

Overall:

yes

Presenter Notes

Operations on "deep" data structures

Presenter Notes

Operations on "deep" data structures

Let's say you have some nested structure like a tree:

1
2
3
4
5
6
7
// the data
case class Foo(name: String, factor: Int)

// a node of the tree
case class FooNode(
  value: Foo, 
  children: Seq[FooNode] = Seq())

Presenter Notes

Operations on "deep" data structures

Make a tree of Foo's:

1
2
3
4
5
6
val tree = 
  FooNode(
    Foo("root", 11),
    Seq(
      FooNode(Foo("child1", 1)),
      FooNode(Foo("child2", 2)))) // <-- * 4

Task: Create a new tree where the second child's factor is multiplied by 4.

Presenter Notes

Operations on "deep" data structures

Let's try all at once:

1
2
3
4
5
6
7
8
9
val secondTimes4: FooNode => FooNode = 
  node => node.copy(children = {
    val second = node.children(1)
    node.children.updated(
      1, 
      second.copy(
        value = second.value.copy(
          factor = second.value.factor * 4)))
  })

Eww: temporary variables, x.copy(field = f(x.field)) boilerplate, deep nesting for every level.

Presenter Notes

Operations on "deep" data structures

Wouldn't it be better to have one thing to address something in a Foo or FooNode, and just combine them?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
val second = // second node child
val value  = // value of a node
val factor = // factor field of Foo

val secondFactor = second ??? value ??? factor

secondFactor(tree) // get 2

secondFactor.set(tree, 8)     // set 8 there
secondFactor.mod(tree, _ * 4) // modify there

Presenter Notes

Operations on "deep" data structures

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
val second: Lens[FooNode, FooNode] = 
  Lens(
    _.children(1),
    (node, c2) => node.copy(
      children = node.children.updated(1, c2)))

val value: Lens[FooNode, Foo] = 
  Lens(
    _.value,
    (node, value) => node.copy(value = value))

val factor: Lens[Foo, Int] = 
  Lens(
    _.factor, 
    (foo, fac) => foo.copy(factor = fac))

Presenter Notes

Operations on "deep" data structures

The Lens type encapsulates "getters" and "setters" on another type.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Lens[Thing, View](
  get: Thing => View,
  set: (Thing, View) => Thing)

val thing: Thing
val lens:  Lens[Thing, View] = ...

val view: View = lens(thing) // apply = get

// "set" a view
val thing2: Thing = lens.set(thing, view)

Presenter Notes

Operations on "deep" data structures

Lenses compose:

1
2
val secondFactor = 
  second andThen value andThen factor

Presenter Notes

Operations on "deep" data structures

1
2
3
4
5
6
7
8
9
/* FooNode(
     Foo("root", 11),
     Seq(
       FooNode(Foo("child1", 1)),
       FooNode(Foo("child2", 2))))
                             ^
                             ^
                             ^  */
secondFactor(tree)        // 2

Presenter Notes

Operations on "deep" data structures

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/* FooNode(
     Foo("root", 11),
     Seq(
       FooNode(Foo("child1", 1)),
       FooNode(Foo("child2", 2))))
                             ^
                             ^  */
secondFactor.mod(tree,   _ * 4)
/* FooNode(                  ^
     Foo("root", 11),        ^
     Seq(                    ^
       FooNode(Foo("child1", ^)),
       FooNode(Foo("child2", 8))))
 */

Presenter Notes

Operations on "deep" data structures

scalaz for "deep" access:

  • Pros: composable so you can "go deeper" for free.
  • Cons: Need to be manually created. (But there is an experimental compiler plugin to autogenerate them for all case classes!)

Presenter Notes

Thanks!

scalaz "For the Rest of Us"

Adam Rosien
arosien@box.com && adam@rosien.net

@arosien #scalasv #scalaz

Thank the scalaz authors: runarorama, retronym, tmorris and lots others.

Credits, sources and references:

Presenter Notes