Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implicit class which extends Dynamic doesn't work #16258

Closed
plokhotnyuk opened this issue Oct 28, 2022 · 11 comments
Closed

Implicit class which extends Dynamic doesn't work #16258

plokhotnyuk opened this issue Oct 28, 2022 · 11 comments
Labels
itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label

Comments

@plokhotnyuk
Copy link

plokhotnyuk commented Oct 28, 2022

Compiler version

3.2.1

Minimized code

import scala.language.dynamics
import scala.collection._

object ImplicitDynamics {
  type JsonPrimitive = String | Int | Double | Boolean | None.type
  type SomeJson = mutable.Map[String, JsonPrimitive]

  def obj(values: (String, JsonPrimitive)*): SomeJson =
    (new mutable.HashMap[String, JsonPrimitive]()).addAll(values)

  implicit class Json(val underlying: SomeJson) extends Dynamic {    
    def selectDynamic(key: String): JsonPrimitive = underlying(key)
  }
}

import ImplicitDynamics._

println(Json(obj("name" -> "John", "age" -> 42)).name)
println(obj("name" -> "John", "age" -> 42).apply("name"))
println(obj("name" -> "John", "age" -> 42).name) // <-- Doesn't compile

Output

The last line doesn't compile.

Expectation

The list line should print the same result as previous 2.

I stuck with that during adding Scala 3 support for the dijon project in the following PR: jvican/dijon#202

@plokhotnyuk plokhotnyuk added itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label labels Oct 28, 2022
@SethTisue SethTisue changed the title Implicit class which extends Dynamics doesn't work Implicit class which extends Dynamic doesn't work Oct 28, 2022
@prolativ
Copy link
Contributor

To my knowledge the fact that this doesn't work is in accordance with the specification of the language.
In general, given that we have a value foo of type Foo, when we write foo.bar the compiler tries to understand bar as the first matching of:

  1. A member of type Foo (either declared directly in Foo or inherited)
  2. An extension
    a) extension method, like extension (f: Foo) def bar
    or
    b) extension from an implicit conversion (including implicit classes)
  3. If Foo extends Dynamic and we enable scala.language.dynamics - foo.selectDynamic("bar")

But in your case SomeJson doesn't extend Dynamic and rules 2 & 3 don't compose.

@plokhotnyuk Do you have a self contained example, which works in scala 2 but doesn't in 3?

@plokhotnyuk
Copy link
Author

plokhotnyuk commented Oct 28, 2022

@prolativ I will do it when I start to understand how it works for

implicit class Json[A: JsonType](val underlying: A) extends Dynamic {

Will you share any tips?

@prolativ
Copy link
Contributor

Just try to take your scala 2 codebase and try to reduce it up to the point when it's reasonably small (preferably one file) and has no library dependencies but includes this Json implicit class and an example of a dynamic selection. If this works somehow with : JsonType type bounds you should be able to make a minimization without it too.

@prolativ
Copy link
Contributor

I tried to minimize it myself:

import scala.language.dynamics

object Test {
  type Foo = Map[String, Any]
  implicit class DynamicFoo(foo: Foo) extends Dynamic {
    def selectDynamic(name: String) = foo(name)
  }

  val foo: Foo = Map("bar" -> 123)
  val x = foo.bar
}

but this doesn't work when compiled with scala 2.13.10

@plokhotnyuk
Copy link
Author

@prolativ Ok, but why it works for that generic Json[A: JsonType]?

@prolativ
Copy link
Contributor

I hoped you would tell me how you made your code work 😆 Unfortunately I don't have that much time to go through your entire codebase so you have to help me understand it. As I said, implicit conversions and dynamics don't seem to compose well in either scala 2 or 3 but maybe you could at least point me some exact places in your code where this does seem to work to make me believe?

@plokhotnyuk
Copy link
Author

plokhotnyuk commented Oct 28, 2022

@prolativ That code wasn't written by me ;)

I just touched it during migration to jsoniter-scala parser.

Here is a working code snippet from README

@prolativ
Copy link
Contributor

Note that with the original library in scala 2 your example doesn't work either but in a different way:

import dijon._

println(Json(obj("name" -> "John", "age" -> 42)).name) // <-- Doesn't compile
println(obj("name" -> "John", "age" -> 42).apply("name")) // <-- Compiles
println(obj("name" -> "John", "age" -> 42).name) // <-- Compiles

That's because obj returns Json (which does extend Dynamic), not SomeJson

@plokhotnyuk
Copy link
Author

plokhotnyuk commented Oct 31, 2022

Below is my scala-cli minimization of Scala 2 version of the dijon library to show working Dynamic for implicit class:

//> using scala "2.13"

object UnionType {
  sealed trait ¬[-A]

  sealed trait TSet {
    type Compound[A]
    type Map[F[_]] <: TSet
  }

  sealed trait  extends TSet {
    type Compound[A] = A
    type Map[F[_]] = 
  }

  sealed trait [T <: TSet, H] extends TSet {
    type Member[X] = T#Map[¬]#Compound[¬[H]] <:< ¬[X]

    type Compound[A] = T#Compound[H with A]

    type Map[F[_]] = T#Map[F]  F[H]
  }
}

object ImplicitDynamic {
  import scala.language.{dynamics, existentials}
  import UnionType.{, }
  import scala.collection.mutable

  type JsonTypes =   String  Int  Double  Boolean  JsonArray  JsonObject  None.type
  type JsonType[A] = JsonTypes#Member[A]
  type SomeJson = Json[A] forSome { type A }
  type JsonObject = mutable.Map[String, SomeJson]
  type JsonArray = mutable.Buffer[SomeJson]

  def obj(values: (String, SomeJson)*): SomeJson =
    new mutable.HashMap[String, SomeJson]().addAll(values)

  implicit class Json[A: JsonType](val underlying: A) extends Dynamic {
    def selectDynamic(key: String): SomeJson = apply(key)

    def apply(key: String): SomeJson = underlying match {
      case obj: JsonObject =>
        obj.get(key) match {
          case Some(value) => value
          case _ => None
        }
      case _ => None
    }

    override def toString: String = underlying.toString
  }
}

import ImplicitDynamic._

println(obj("name" -> "John", "age" -> 42).name)

@prolativ
Copy link
Contributor

Ad I said, this works because obj returns a subtype of Json, which is a subtype of Dynamic.
In your example at the beginning of this issue report SomeJson is an alias for mutable.Map[String, JsonPrimitive], not for Json[A] forSome { type A }.
In the last snippet the implicit conversion from a Map to Json is performed inside the body of obj so there's no place where you would actually try to select .name from a Map

@plokhotnyuk
Copy link
Author

@prolativ Thanks for your help!

Closing as not an issue

@som-snytt som-snytt closed this as not planned Won't fix, can't repro, duplicate, stale Dec 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label
Projects
None yet
Development

No branches or pull requests

3 participants