makkarpov / explicits   0.1

Apache License 2.0 GitHub

A tiny library to have more control over Scala 3 implicit resolution in macros

Scala versions: 3.x

explicits

A tiny library which gives macro authors much more control over Scala 3 implicit resolution process.

libraryDependencies += "mx.m-k" %% "explicits" % "0.1"

Implicit resolution

Vanilla Implicits.search method provided by the compiler has several major drawbacks:

  • It resolves implicits strictly in the scope of a macro application: you cannot inject any additional imports or givens even if you know where to look,
  • and it either resolves the implicit completely or completely fails. You cannot get half-resolved result like "I can do that, but you need to provide X and Y for me".

Second drawback is critical if you are writing a macros to derive typeclasses recursively. Consider the following example:

trait CanMeow[T] { /* ... */ }

given seqCanMeow[T](using CanMeow[T]): CanMeow[Seq[T]] = ???

case class Foo(/* ... */)
case class Bar(foos: Seq[Foo]) derives CanMeow

In this case, you will never resolve seqCanMeow implicit with default API. This library could provide you a half-completed result saying "give me a CanMeow[Foo] and I will build CanMeow[Seq[Foo]] for you".

Usage example

All magic starts with ImplicitSearch.builder method:

import mx.mk.explicits.{ImplicitSearch, Symbol}

def deriveMeow[T](using Type[T], Quotes): Expr[CanMeow[T]] = {
  val r = ImplicitSearch.builder[T]
    // inject additional imports:
    .extraLocations(Symbol.forModule("com.example.MeowModule"))
    // provide explicit givens:
    .give[CanMeow[Foobar]]('{ ??? })
    // setup the assisted resolution:
    .assist {
      case '[ CanMeow[t] ]  => true   // sure, everything can meow with me
      case _                => false  // can't assist with anything else
    }
    .search()   // get the final result
    .toSuccess  // aborts the macro if search failed
  
  // inspect and derive what was missing:
  val meows: Seq[Expr[?]] = success
    .missingTypes
    .map {
      case '[ CanMeow[t] ] => 
        '{ new CanMeow[t] { /* teach `t` to meow here */ } }
    }
  
  // construct the final expression:
  success.construct(meows)
}

If you don't use the .assist() method, ImplicitSearch.Success will always have missingTypes field empty, and simple .construct(Nil) is sufficient to get the final expression. Any missing implicit will fail the overall resolution process if assisted resolution is disabled.

If you want to assist the compiler with implicit resolution:

  1. .assist() method takes a type filter predicate (Type[?] => Boolean). This predicate should test whether a value for the type could be generated by your code. Search will fail if it encounters a missing value which fails the test.
  2. You should inspect .missingTypes field of the search result and provide expressions for all types listed there. Final expression is then constructed from the provided parts by the .construct() method.

Compatibility

Since this library heavily depends on the compiler internals, it could easily break even on slightest compiler change. To ensure seamless operation across a wide range of compiler versions, this library internally packs multiple backend implementations and selects a correct one at runtime.

Currently, this library is tested to work on all released compiler versions from 3.2.0 up to 3.4.2.

License

This library is licensed under Apache 2.0 license.