Desugaring scala `for` without implicit `withFilter`s
Clone or download
Latest commit 537bf44 Nov 18, 2018
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
project Initial commit Mar 29, 2018
src Fix: runsRightAfter is an Option, not a List May 4, 2018
.gitignore Actual tests; some code reorg, remove debug println Mar 30, 2018
LICENSE Create LICENSE Mar 30, 2018
README.md Update README.md Nov 18, 2018
build.sbt Fix: runsRightAfter is an Option, not a List May 4, 2018

README.md

better-monadic-for

Gitter Waffle.io - Columns and their card count Maven central

A Scala compiler plugin to give patterns and for-comprehensions the love they deserve

Getting started

The plugin is available on Maven Central.

addCompilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.0-M4")

Supports Scala 2.11 and 2.12.

Available plugin options

All options have form of -P:bm4:$feature:$flag

Feature Flag (default)
Desugaring without withFilter -P:bm4:no-filtering:y
Elimination of identity map -P:bm4:no-map-id:y
Elimination of tuples in bindings -P:bm4:no-tupling:y
Implicit definining patterns -P:bm4:implicit-patterns:y

Supported values for flags:

  • Disabling: n, no, 0, false
  • Enabling: y, yes, 1, true

Changelog
Version Changes
0.3.0-M4 Fix anonymous variables in Scala 2.12.7+
M2, M3 Fixes for implicit patterns
0.3.0-M1 Initial implementation of implicit patterns
0.2.4 Fixed: incompatibility with Dsl.scala
0.2.3 Fixed: if-guards were broken when using untupling
0.2.2 Fixed: destructuring within for bindings (bar, baz) = foo
0.2.1 Fixed: untupling with -Ywarn-unused:locals causing warnings on e.g. _ = println().
0.2.0 Added optimizations: map elimination & untupling. Added plugin options.
0.1.0 Initial version featuring for desugaring without withFilters.

Features

Desugaring for patterns without withFilters

Destructuring Either / IO / Task / FlatMap[F]

This plugin lets you do:

import cats.implicits._
import cats.effect.IO

def getCounts: IO[(Int, Int)] = ???

for {
  (x, y) <- getCounts
} yield x + y

With regular Scala, this desugars to:

getCounts
  .withFilter((@unchecked _) match {
     case (x, y) => true
     case _ => false
  }
  .map((@unchecked _) match {
    case (x, y) => x + y
  }

Which fails to compile, because IO does not define withFilter

This plugin changes it to:

getCounts
  .map(_ match { case (x, y) => x + y })

Removing both withFilter and unchecked on generated map. So the code just works.

Additional Effects

Type ascriptions on LHS

Type ascriptions on left-hand side do not become an isInstanceOf check - which they do by default. E.g.

def getThing: IO[String] = ???

for {
  x: String <- getCounts
} yield s"Count was $x"

would desugar directly to

getCounts.map((x: String) => s"Count was $x")

This also works with flatMap and foreach, of course.

No silent truncation of data

This example is taken from Scala warts post by @lihaoyi

// Truncates 5
for((a, b) <- Seq(1 -> 2, 3 -> 4, 5)) yield a + " " +  b

// Throws MatchError
Seq(1 -> 2, 3 -> 4, 5).map{case (a, b) => a + " " + b}

With the plugin, both versions are equivalent and result in MatchError

Match warnings

Generators will now show exhaustivity warnings now whenever regular pattern matches would:

        import cats.syntax.option._

        for (Some(x) <- IO(none[Int])) yield x
D:\Code\better-monadic-for\src\test\scala\com\olegpy\TestFor.scala:66
:22: match may not be exhaustive.
[warn] It would fail on the following input: None
[warn]         for (Some(x) <- IO(none[Int])) yield x
[warn]                      ^

Final map optimization

Eliminate calls to .map in comprehensions like this:

for {
  x <- xs
  y <- getYs(x)
} yield y

Standard desugaring is

xs.flatMap(x => getYs(x).map(y => y))

This plugin simplifies it to

xs.flatMap(x => getYs(x))

Desugar bindings as vals instead of tuples

Direct fix for lampepfl/dotty#2573. If the binding is not used in follow-up withFilter, it is desugared as plain vals, saving on allocations and primitive boxing.

Define implicits in for-comprehensions or matches

Since version 0.3.0-M1, it is possible to define implicit values inside for-comprehensions using a new keyword implicit0:

case class ImplicitTest(id: String)

for {
  x <- Option(42)
  implicit0(it: ImplicitTest) <- Option(ImplicitTest("eggs"))
  _ <- Option("dummy")
  _ = "dummy"
  _ = assert(implicitly[ImplicitTest] eq it)
} yield "ok"

In current version (0.3.0-M4) it's required to specify a type annotation in a pattern with implicit0.

It also works in regular match clauses:

(1, "foo", ImplicitTest("eggs")) match {
  case (_, "foo", implicit0(it: ImplicitTest)) => assert(implicitly[ImplicitTest] eq it)
}

Notes

  • This plugin reserves one extra keyword, implicit0, if corresponding option for implicit patterns is enabled (which is by default).
  • Regular if guards are not affected, only generator arrows.

License

MIT