arosien@box.com && adam@rosien.net
@arosien #scalasv #scalaz
scalaz
has a (undeserved?) reputation as being, well, kind of crazy.
So this talk is specifically not about:
This talk is about every-day situations where scalaz
can:
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.
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.
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.
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.
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)
|
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!
scalaz
memoization:
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
|
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")
|
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]
|
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]
|
Pros: less noise, more expressive, more type-safe
Cons: you have to know these operators
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!
|
This shouldn't be possible:
1 2 3 4 | case class Version(major: Int, minor: Int)
Version(1, -1)
// ^^ noooo!
|
Meh:
1 2 3 4 5 6 7 | case class Dependency(
organization: String,
artifactId: String,
version: Version)
Dependency("zerb", "", Version(1, 2))
// ^^ nooooo!
|
The problem is that the types as-is aren't really accurate.
String
s and Int
s are being used too broadly.
We really want "Int
s greater than zero",
"String
s 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))
}
|
But this has downsides:
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 => ...)
|
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"))
|
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
...
}
|
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?
}
}
|
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]
|
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]
// ^^^^^^^
|
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.
|
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
|
An improvement?
Validation
/Success
/Failure
is nicer than try
/catch
or Either
/Left
/Right
.Validation
.liftFailNel
, |@|
, etc., is incomprehensible if you're not familiar.Overall:
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())
|
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.
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.
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
|
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))
|
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)
|
Lenses compose:
1 2 | val secondFactor =
second andThen value andThen factor
|
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
|
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))))
*/
|
scalaz
for "deep" access:
arosien@box.com && adam@rosien.net
@arosien #scalasv #scalaz
Thank the scalaz
authors: runarorama, retronym, tmorris and lots others.
Credits, sources and references:
Table of Contents | t |
---|---|
Exposé | ESC |
Full screen slides | e |
Presenter View | p |
Source Files | s |
Slide Numbers | n |
Toggle screen blanking | b |
Show/hide slide context | c |
Notes | 2 |
Help | h |