Lovely for-comprehensions

Adam Rosien
CrowdStrike
adam@rosien.net

@arosien #scalaio

Presenter Notes

flatMap that shit!

Presenter Notes

y u no use for-comprehension?

Presenter Notes

Types are wonderful...

1
2
3
4
5
def transitionHaveFsPostOpenSnapshotState(
  input: InputMessage,
  tags: CorrelatorTagMap):
    (CorrelatorData,
      (ActorRef, CorrelatorIndex) => Unit)

Presenter Notes

... but they hide the implementation

which can be quite complicated and difficult

Presenter Notes

Niiiiiice

1
2
3
4
for {
  response <- searchApi(RuleQuery(search)).run
  alert    <- response.fold(toFailure, toAlert)
} yield alert

thumbs up

Presenter Notes

F[_]

Presenter Notes

F[_]

F[_]? What's an F[_]?

1
2
3
import language.higherKinds

def doStuff[F[_], A](fa: F[A]) = ???

Presenter Notes

F[_]

1
2
3
4
5
6
7
package scala.collection
package immutable

sealed abstract class List[+A] { // ... }
                         ^ ^
                         ^ ^
                         F[_]

Presenter Notes

F[_]

Option is an F[_]:

1
2
3
4
5
6
package scala

sealed abstract class Option[+A] { // ... }
                           ^ ^
                           ^ ^
                           F[_]

Presenter Notes

F[_]

F[_]!!

  • List
  • Option
  • Future
  • Either?

Lots more....

Presenter Notes

for-comprehensions

Presenter Notes

F[_] => F[_]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
scala> for {
     |   s <- Option("stuff")
     | } yield s.length
res0: Option[Int] = Some(5)

scala> for {
     |   x <- List(1, 2, 3)
     |   y <- List(4, 5, x)
     | } yield y + 1
res1: List[Int] =
    List(5, 6, 2, 5, 6, 3, 5, 6, 4)

Presenter Notes

As syntactic sugar

A single expression without yield translates to foreach:

1
2
3
4
5
6
7
8
9
scala> reflect.runtime.universe.reify {
     |   for {
     |     x <- Some(12)
     |   } println(x)
     | }.tree
res2: reflect.runtime.universe.Tree =
  Some.apply(12)
    .foreach(((x) =>
      Predef.println(x)))

Presenter Notes

As syntactic sugar

A single expression translates to map:

1
2
3
4
5
6
7
8
9
scala> reflect.runtime.universe.reify {
     |   for {
     |     x <- Some(12)
     |   } yield x + 1
     | }.tree
res3: reflect.runtime.universe.Tree =
  Some.apply(12)
    .map(((x) =>
      x.$plus(1)))

Presenter Notes

As syntactic sugar

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
scala> reflect.runtime.universe.reify {
     |   for {
     |     x <- Some(12)
     |     y <- Some(2)
     |   } yield x * y
     | }.tree
res4: reflect.runtime.universe.Tree =
  Some.apply(12)
    .flatMap(((x) =>
      Some.apply(2)
        .map(((y) =>
          x.$times(y)))))

Multiple expressions translate to flatMap, with the last one translated to map.

Presenter Notes

As syntactic sugar

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
scala> reflect.runtime.universe.reify {
     |   for {
     |     x <- Some(12) if x < 10
     |   } yield x + 1
     | }.tree
res5: reflect.runtime.universe.Tree =
  Some.apply(12)
    .withFilter(((x) =>
      x.$less(10)))
      .map(((x) =>
        x.$plus(1)))

Conditionals are translated to filter or withFilter.

Presenter Notes

As syntactic sugar

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
scala> reflect.runtime.universe.reify {
     |   for {
     |     x <- Some(12)
     |     y = x + 1
     |   } yield y
     | }.tree
res6: reflect.runtime.universe.Tree =
  Some.apply(12).map(((x) => {
    val y = x.$plus(1);
    Tuple2.apply(x, y)
  })).map(((x$1) => x$1: @unchecked match {
    case Tuple2((x @ _), (y @ _)) => y
  }))

Intermediate values are translated to two maps with a tuple.

Presenter Notes

Rules & Techniques

Presenter Notes

Avoid "complexity-braces"

1
2
3
4
5
6
scala> def uggslies = {
     |   val thing = ???
     |   val anotherThingThatDoesStuff = ???
     |   // DANGER COMPLEX STUFFZ!!
     |   ???
     | }

"complexity-braces" are a code smell. Braces contain statements with arbitrary side-effects.

Presenter Notes

Avoid "complexity-braces"

Expressions don't need them. Expressions produce values.

1
2
3
4
5
6
def thePrecious: MagicEffect[Baz] =
  for {
    foo <- magicFoo
    bar <- magicBar(foo)
  } baz(bar)
}

Presenter Notes

RHS must have same shape

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
scala> illTyped("""
     |   for {
     |     x <- Some(12)   // Option[Int]
     |     y <- List(2, 4) // List[Int]
     |   } yield x * y
     | """)
res7: String =
type mismatch;
 found   : List[Int]
 required: Option[?]

Danger! scala.Predef imports confusing implicit conversions like Option => Iterable!

Presenter Notes

RHS must have same shape

1
2
3
4
5
scala> for {
     |   x <- Some(12).toList
     |   y <- List(2, 4)
     | } yield x * y
res8: List[Int] = List(24, 48)

Presenter Notes

traverse

1
2
3
4
5
F[A]    => (A => G[B])      => G[F[B]]

e.g.,

List[A] => (A => Future[B]) => Future[List[B]]

Presenter Notes

traverse

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
scala> illTyped("""
     | val xs = List(1, 2, 3)
     | def doWork(x: Int): Future[Int] =
     |   Future.successful(x)
     | val work: Future[List[Int]] =
     |   for {
     |     x <- xs        // List[Int]
     |     y <- doWork(x) // Future[Int]
     |   } yield y
     | """)
res9: String = 
type mismatch;
 found   : scala.concurrent.Future[Int]
 required: scala.collection.GenTraversableOnce[?]

Presenter Notes

traverse

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
scala> def doWork(x: Int): Future[Int] =
     |   Future.successful(x)
doWork: (x: Int)scala.concurrent.Future[Int]

scala> val work: Future[List[Int]] =
     |   Future.traverse(List(1, 2, 3))(doWork)
work: scala.concurrent.Future[List[Int]] = scala.concurrent.impl.Promise$DefaultPromise@34d8aa71

scala> work.value
res10: Option[scala.util.Try[List[Int]]] = Some(Success(List(1, 2, 3)))

Presenter Notes

sequence

1
2
3
4
5
F[G[A]] => G[F[A]]

e.g.,

List[Future[A]] => Future[List[A]]

Presenter Notes

Avoid match

match, aka deconstruction, is less powerful:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
scala> def zibby(z: Option[Foo]): String =
     |   z match {
     |     case Some(foo) =>
     |       foo.bar match {
     |         case Some(bar) => if (bar.sup.isDefined) "whee" else "boo"
     |         case None => ""
     |       }
     |     case None => ""
     |   }
zibby: (z: Option[Foo])String

Presenter Notes

Avoid match

1
2
3
4
5
6
7
8
scala> def zibby2(z: Option[Foo]): Option[String] =
     |   for {
     |     foo <- z
     |     bar <- foo.bar
     |   } yield
           if (bar.sup.isDefined) "whee"
           else "boo"
zibby2: (z: Option[Foo])Option[String]

Presenter Notes

Inline value declarations

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
scala> val classifications =
  Map.empty[String, Map[String, String]]
scala> val sha256Classifications =
  classifications.get("msg.sha256")
scala> val customerResult =
  sha256Classifications.flatMap(_.get("msg.cid"))
scala> val globalResult =
  sha256Classifications.flatMap(_.get("GlobalColumnName"))
scala> val finalResult: Option[String] =
  customerResult orElse globalResult
finalResult: Option[String] = None

Presenter Notes

Inline value declarations

1
2
3
4
5
6
7
8
scala> val finalResult2: Option[String] =
 |   for {
 |     sha256Classifications <- classifications.get("msg.sha256")
 |     cid = sha256Classifications.get("msg.cid")
 |     global = sha256Classifications.get("GlobalColumnName")
 |     r <- cid orElse global
 |   } yield r
finalResult2: Option[String] = None

Presenter Notes

Abstracting over F[_]

1
2
3
4
scala> for {
     |   i <- Option(1)
     | } yield i + 1
res12: Option[Int] = Some(2)

Presenter Notes

Abstracting over F[_]

1
2
3
4
scala> for {
     |   i <- List(1, 2, 3)
     | } yield i + 1
res13: List[Int] = List(2, 3, 4)

Presenter Notes

Abstracting over F[_]

1
2
3
4
scala> for {
     |   i <- Future.successful(1)
     | } yield i + 1
res14: scala.concurrent.Future[Int] = scala.concurrent.impl.Promise$DefaultPromise@8392122

Presenter Notes

Abstracting over F[_]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
scala> def plusOne[F[_] : Functor]
         (fi: F[Int]): F[Int] =
     |   for {
     |     i <- fi
     |   } yield i + 1
warning: there were 1 feature warning(s); re-run with -feature for details
plusOne: [F[_]](fi: F[Int])(implicit evidence$1: scalaz.Functor[F])F[Int]

scala> plusOne(Option(1))
res15: Option[Int] = Some(2)

scala> plusOne(List(1, 2, 3))
res16: List[Int] = List(2, 3, 4)

scala> Await.result(
     |   plusOne(Future.successful(1)),
     |   1.second)
res17: Int = 2

Presenter Notes

Abstracting over F[_]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
scala> val plusOne2: Int => Int = _ + 1
plusOne2: Int => Int = <function1>

scala> val optionPlusOne:
  Option[Int] => Option[Int] =
     |   Functor[Option].lift(plusOne2)
optionPlusOne: Option[Int] => Option[Int] = <function1>

scala> optionPlusOne(Some(41))
res18: Option[Int] = Some(42)

We can separate our pure functions from our effects!

Presenter Notes

In summary

Presenter Notes

In summary

for-comprehension = just good refactoring

  • Know your desugaring
  • Avoid complexity-braces, match
  • Use inline values to clean up your for-comps
  • Ugly code can be improved!

Thank you!

Adam Rosien / adam@rosien.net / @arosien #scalaio

Presenter Notes