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.

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
jwvh
  • 50,871
  • 7
  • 38
  • 64
  • Assume you mean 'disambiguates'? Otherwise it would not compile? i.e. you want to create a new 'deafult' implicit that _does_ compile, right? – Luciano Apr 02 '19 at 15:50
  • 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. – jwvh Apr 02 '19 at 19:19
  • 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. – Luciano Apr 02 '19 at 19:39
  • 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? – jwvh Apr 02 '19 at 19:56
  • 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? – Luciano Apr 04 '19 at 08:02
  • 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. – jwvh Apr 04 '19 at 08:18

2 Answers2

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.

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • 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. – jwvh Sep 17 '19 at 02:05
  • @jwvh 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. – Dmytro Mitin Sep 17 '19 at 08:13
  • @jwvh 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") ` – Dmytro Mitin Sep 17 '19 at 08:27
  • @jwvh It should be `def fromURI(str: String)(implicit ev: ExistLocal[Codec]) = io.Source.fromURI(new URI(str))(ev.instance)`. – Dmytro Mitin Sep 17 '19 at 08:45
-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
Luciano
  • 2,388
  • 1
  • 22
  • 33
  • Your solution _avoids_ implicit ambiguity. My goal is to _create_ an implicit ambiguity without importing `fallbackSystemCodec` because it should already be in scope. – jwvh Apr 02 '19 at 22:30