Skip to content

Desugaring scala `for` without implicit `withFilter`s

License

Notifications You must be signed in to change notification settings

oleg-py/better-monadic-for

Folders and files

NameName
Last commit message
Last commit date

Latest commit

d4e53c8 · May 25, 2021

History

80 Commits
Jun 19, 2020
Jul 19, 2019
Jul 2, 2020
Jul 2, 2020
Jul 2, 2020
Apr 25, 2019
Jul 2, 2020
Jul 19, 2019
Apr 13, 2019
Mar 30, 2018
May 25, 2021
Jul 2, 2020

Repository files navigation

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

Note on Scala 3

Scala 3.0.0 natively supports the semantic changes provided by better-monadic-for under -source:future compiler flag. The following code is considered valid under this flag:

for {
  (x, given String) <- IO(42 -> "foo")
} yield s"$x${summon[String]}"

There are no changes to map desugaring and value bindings inside fors still allocate tuples to my current knowledge. I don't currently have plans on rewriting plugin for Scala 3, however.

See changes: pattern bindings and contextual abstractions: pattern-bound given instances.


Getting started

The plugin is available on Maven Central.

sbt

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

maven

<plugin>
  <groupId>net.alchim31.maven</groupId>
  <artifactId>scala-maven-plugin</artifactId>
  <configuration>
    <compilerPlugins>
      <compilerPlugin>
        <groupId>com.olegpy</groupId>
        <artifactId>better-monadic-for_2.13</artifactId>
        <version>0.3.1</version>
      </compilerPlugin>
    </compilerPlugins>
  </configuration>
</plugin>

Supports Scala 2.11, 2.12, and 2.13.1

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.1 Fix issues with wartremover, implicit patterns with = binds & Xplugin-list flag
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) 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