oleg-py / better-monadic-for

Desugaring scala `for` without implicit `withFilter`s
MIT License
709 stars 32 forks source link
compiler-plugin desugar for-comprehension functional-programming optimization scala

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](https://github.com/ThoughtWorksInc/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 `withFilter`s. ---

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. ```scala def getThing: IO[String] = ??? for { x: String <- getCounts } yield s"Count was $x" ``` would desugar directly to ```scala 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](http://www.lihaoyi.com/post/WartsoftheScalaProgrammingLanguage.html#conflating-total-destructuring-with-partial-pattern-matching) by @lihaoyi ```scala // 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: ```scala 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

License

MIT