create an ambiguous low priority implicit
Asked Answered
H

2

11

Consider the default codec as offered in the io package.

implicitly[io.Codec].name  //res0: String = UTF-8

It's a "low priority" implicit so it's easy to override without ambiguity.

implicit val betterCodec: io.Codec = io.Codec("US-ASCII")

implicitly[io.Codec].name  //res1: String = US-ASCII

It's also easy to raise its priority level.

import io.Codec.fallbackSystemCodec
implicit val betterCodec: io.Codec = io.Codec("US-ASCII")

implicitly[io.Codec].name  //won't compile: ambiguous implicit values

But can we go in the opposite direction? Can we create a low level implicit that disables ("ambiguates"?) the default? I've been looking at the priority equation and playing around with low priority implicits but I've yet to create something ambiguous to the default.

Hun answered 1/4, 2019 at 19:29 Comment(6)
Assume you mean 'disambiguates'? Otherwise it would not compile? i.e. you want to create a new 'deafult' implicit that does compile, right?Twink
Not quite. I want to create a dummy implicit at the same low priority as fallbackSystemCodec so that all code that requires an implicit codec will break (won't compile) unless a higher priority implicit is in scope. The higher priority implicit is easy, but I haven't been able to create an implicit at the same priority level (and thus is ambiguous with) the fallback codec.Hun
I think you are confusing priority/search order vs there being multiple implicits in scope. It doesn't matter whether an implicit is 'low priority' or 'high priority'. As long as there is more than one implicit in scope, the compiler will complain.Twink
But that's what I want. I want the compiler to complain. I want to write an implicit that won't compile because it is ambiguous with fallbackSystemCodec, but do this without importing fallbackSystemCodec because it's already in scope anyway, isn't it?Hun
I don't quite follow how this would help you, generally speaking. Say you achieve this behaviour. How do you then 'fix' your program? You can't remove io.Codec.fallbackSystemCodec (because it's part of the stdlib). And as your 'clashing' implicit is not explicitly imported, you can't remove that either. Am I understanding this correctly?Twink
Consider it a learning exercise. It might turn out to be a dead-end, as far as achieving my ultimate goal, but even so, if this is at all doable I'll have learned a lot about implicit scoping. And if it can't be done perhaps someone will explain why.Hun
O
1

If I understand correctly you want to check at compile time that there is local implicit io.Codec ("higher-priority") or produce compile error otherwise. This can be done with macros (using compiler internals).

import scala.language.experimental.macros
import scala.reflect.macros.{contexts, whitebox}

object Macros {

  def localImplicitly[A]: A = macro impl[A]

  def impl[A: c.WeakTypeTag](c: whitebox.Context): c.Tree = {
    import c.universe._

    val context = c.asInstanceOf[contexts.Context]
    val global: context.universe.type = context.universe
    val analyzer: global.analyzer.type = global.analyzer
    val callsiteContext = context.callsiteTyper.context

    val tpA = weakTypeOf[A]

    val localImplicit = new analyzer.ImplicitSearch(
      tree = EmptyTree.asInstanceOf[global.Tree],
      pt = tpA.asInstanceOf[global.Type],
      isView = false,
      context0 = callsiteContext.makeImplicit(reportAmbiguousErrors = true),
      pos0 = c.enclosingPosition.asInstanceOf[global.Position]
    ) {
      override def searchImplicit(
                                   implicitInfoss: List[List[analyzer.ImplicitInfo]],
                                   isLocalToCallsite: Boolean
                                 ): analyzer.SearchResult = {
        if (isLocalToCallsite)
          super.searchImplicit(implicitInfoss, isLocalToCallsite)
        else analyzer.SearchFailure
      }
    }.bestImplicit

    if (localImplicit.isSuccess)
      localImplicit.tree.asInstanceOf[c.Tree]
    else c.abort(c.enclosingPosition, s"no local implicit $tpA")
  }
}
localImplicitly[io.Codec].name // doesn't compile
// Error: no local implicit scala.io.Codec
implicit val betterCodec: io.Codec = io.Codec("US-ASCII")
localImplicitly[Codec].name // US-ASCII
import io.Codec.fallbackSystemCodec
localImplicitly[Codec].name // UTF-8
import io.Codec.fallbackSystemCodec
implicit val betterCodec: io.Codec = io.Codec("US-ASCII")
localImplicitly[Codec].name // doesn't compile
//Error: ambiguous implicit values:
// both value betterCodec in object App of type => scala.io.Codec
// and lazy value fallbackSystemCodec in trait LowPriorityCodecImplicits of type => //scala.io.Codec
// match expected type scala.io.Codec

Tested in 2.13.0.

libraryDependencies ++= Seq(
  scalaOrganization.value % "scala-reflect" % scalaVersion.value,
  scalaOrganization.value % "scala-compiler" % scalaVersion.value
)

Still working in Scala 2.13.10.

Scala 3 implementation

import scala.quoted.{Expr, Quotes, Type, quotes}
import dotty.tools.dotc.typer.{Implicits => dottyImplicits}

inline def localImplicitly[A]: A = ${impl[A]}

def impl[A: Type](using Quotes): Expr[A] = {
  import quotes.reflect.*

  given c: dotty.tools.dotc.core.Contexts.Context =
    quotes.asInstanceOf[scala.quoted.runtime.impl.QuotesImpl].ctx

  val typer = c.typer

  val search = new typer.ImplicitSearch(
    TypeRepr.of[A].asInstanceOf[dotty.tools.dotc.core.Types.Type],
    dotty.tools.dotc.ast.tpd.EmptyTree,
    Position.ofMacroExpansion.asInstanceOf[dotty.tools.dotc.util.SourcePosition].span
  )

  def eligible(contextual: Boolean): List[dottyImplicits.Candidate] =
    if contextual then
      if c.gadt.isNarrowing then
        dotty.tools.dotc.core.Contexts.withoutMode(dotty.tools.dotc.core.Mode.ImplicitsEnabled) {
          c.implicits.uncachedEligible(search.wildProto)
        }
      else c.implicits.eligible(search.wildProto)
    else search.implicitScope(search.wildProto).eligible

  val searchImplicitMethod = classOf[typer.ImplicitSearch]
    .getDeclaredMethod("searchImplicit", classOf[List[dottyImplicits.Candidate]], classOf[Boolean])
  searchImplicitMethod.setAccessible(true)

  def implicitSearchResult(contextual: Boolean) =
    searchImplicitMethod.invoke(search, eligible(contextual), contextual)
      .asInstanceOf[dottyImplicits.SearchResult]
      .tree.asInstanceOf[ImplicitSearchResult]

  implicitSearchResult(true) match {
    case success: ImplicitSearchSuccess => success.tree.asExprOf[A]
    case failure: ImplicitSearchFailure => 
      report.errorAndAbort(s"no local implicit ${Type.show[A]}: ${failure.explanation}")
  }
}

Scala 3.2.0.

Optimize answered 15/9, 2019 at 20:35 Comment(4)
Very impressive. +1. Alas, still not quite what I hope to achieve. Consider the io.Source.fromURI() method. It takes an implicit Codec parameter. Can I disable the default fallbackSystemCodec? My thinking was to create a conflicting/ambiguous implicit at the same priority level. Then fromURI() wouldn't work unless a higher priority implicit were in scope. Perhaps this is not possible. Perhaps there is a better way.Hun
@Hun Let's think what "disable the default fallbackSystemCodec" means. You want to check at compile time that there is local implicit io.Codec ("higher-priority") or produce compile error otherwise. I guess localImplicitly[io.Codec]; io.Source.fromURI(new URI("aaa")) satisfies the condition.Optimize
@Hun You can even make the macro an implicit one. trait ExistLocal[A] { def instance: A }; object ExistLocal { implicit def materialize[A]: ExistLocal[A] = macro impl[A]; def impl[A: c.WeakTypeTag](c: whitebox.Context): c.Tree = { ... if (localImplicit.isSuccess) q"new ExistLocal[$tpA] { def instance = ${localImplicit.tree.asInstanceOf[c.Tree]} }" else c.abort(c.enclosingPosition, s"no local implicit $tpA") } }; def fromURI(str: String)(implicit ev: ExistLocal[Codec]) = io.Source.fromURI(new URI(str)); fromURI("aaa") Optimize
@Hun It should be def fromURI(str: String)(implicit ev: ExistLocal[Codec]) = io.Source.fromURI(new URI(str))(ev.instance).Optimize
T
-1

Sort of, yes.

You can do this by creating a 'newtype'. I.e. a type that is simply a proxy to io.Codec, and wraps the instance. This means that you also need to change all your implicit arguments from io.Codec to CodecWrapper, which may not be possible.

trait CodecWraper {
  def orphan: io.Codec
}

object CodecWrapper {
  /* because it's in the companion, this will have the highest implicit resolution priority. */
  implicit def defaultInstance: CodecWrapper = 
    new CodecWrapper {
      def orphan = new io.Codec { /* your default implementation here */ }
    }
  }
}

import io.Codec.fallbackSystemCodec
implicitly[CodecWrapper].orphan // io.Codec we defined above - no ambiguity
Twink answered 2/4, 2019 at 15:49 Comment(1)
Your solution avoids implicit ambiguity. My goal is to create an implicit ambiguity without importing fallbackSystemCodec because it should already be in scope.Hun

© 2022 - 2024 — McMap. All rights reserved.