Home > Enterprise >  Dynamic import from object instead of package
Dynamic import from object instead of package

Time:07-31

I'm reading the MEAP of the second edition of "Functional Programming in Scala" and I ran across the following in a listing:

In Parsers.scala:

trait Parsers[Parser[ _]]:
  ...
  extension [A](p: Parser[A])
    // Many abstract methods (declaring?) Parser[A].
    // Some combinators defined in terms of the abstract primitives

In Reference.scala we have:

object Reference extends Parsers[Reference.Parser]:
  ...
  extension [A](p: Parser[A])
    // A block providing implementations of the Parsers trait
    ...

In JSON.scala (an implemetation of a JSON parser using the Parsers trait):

def jsonParser[Parser[ _]](P: Parsers[Parser]): Parser[JSON] =
  import P.*  // <--- I don't understand this!!
  ...

and later in that file, in an example:

  val parser = JSON.jsonParser(Reference)
  ...

I'm guessing that the import P.* in the jsonParser function is equivalent to import Reference.* but I've never seen this done before nor do I understand how this works. Does this code really import the members of the singleton object? All the documentation I have seen discusses importing members of a package. I had no idea you could import members of an object, singleton or otherwise.

At the very least I would like to see the documentation on this construct but I don't know what to search for.

Also, the 'self-reference' in the Reference object to extending Parsers[Reference.Parser] is a bit mind-bending. Is this (the Reference.Parser) making reference to the Parser extension block later in the object?

If so, it reminds me of the inheritance trick used by ATL back in the COM/DCOM days. That, also, took a while to really get a handle on...

Edit 7/28: Added additional info about Parser inside Parsers trait

Edit 7/28: Changed title and modified the question a bit.

CodePudding user response:

Bunch of different things here...

I'll just try to address all the mentioned problems, in no particular order.

Regarding Imports

What import does

The import doesn't care about packages or singletons: it just imports members. For example, on Strings, you can call the method contains:

"foobar".contains("foo") == true

Here is how you can import the member contains of a string s:

val s = "hello world"
import s.contains
println(contains("hello")) // true
println(contains("world")) // true
println(contains("12345")) // false

More generally, whenever you have some object a in scope for which the chain of names a.b.c.d.e.f is valid, you can select a point at which you want to break this chain (at d, say), then import a.b.c.d, then simply use d wherever you previously would have written a.b.c.d.

In the following code snippet, both printlns are equivalent:

object a:
  object b:
    object c:
      object d:
        object e:
          def f: String = "hey!"

println(a.b.c.d.e.f)

import a.b.c.d 
println(d.e.f)

Imports are not limited to top-level

It doesn't matter where you use the import, it's not constrained to be on top-level of the file. You can do the same in a function (or anywhere else, really):

// same `object a` as above

def foobar(): Unit =
  println(a.b.c.d.e.f)
  import a.b.c.d 
  println(d.e.f)

foobar()

Works on any objects

It's also not constrained to "static" imports or singleton objects or packages or anything like that:

case class Foo(bar: String)

def useFoo(foo: Foo): Unit =
  import foo.bar
  println(bar)

useFoo(Foo("hello"))

Here, foo is not a singleton, it's just some instance passed from the outside.

It works in exactly the same way for Givens

It also doesn't matter whether you pass a parameter or whether the compiler does it for you automatically: import works on all parameters in exactly the same way. For example, here the timesThree imports a member of an implicitly given adder:

trait Addition[A]:
  def addStuff(x: A, y: A): A

def timesThree[A](a: A)(using adder: Addition[A]): A =
  import adder.addStuff
  addStuff(a, addStuff(a, a))

given Addition[Int] with
  def addStuff(x: Int, y: Int) = x   y

given [A]: Addition[List[A]] with
  def addStuff(x: List[A], y: List[A]) = x    y

println(timesThree(42))           // 126
println(timesThree(List(0, 1)))   // 0 1 0 1 0 1

Again, it works with the singleton given Addition[Int], as well as with instances of Addition[List[A]], which are generated on-the-fly whenever needed.

Regarding Reference.Parser

That's just an ordinary member access. The only thing to know about it is that there are no weird constraints about what can be accessed where. Type members and value members work in the same way:

object Foo:
  type Bar[X] = List[X]
  val baz: Int = 42

val x: Foo.Bar[Int] = List(1, 2, 3) // accessing a type member
println(x)
println(Foo.baz)                    // accessing a value member

Regarding extensions

The extension [A](p: Parser[A]) ... doesn't "define" or "declare" the Parser. The Parser is just left generic. The extension only guarantees that if you have some Foobar[_]-type constructor, and an implementation of Parsers[Foobar], then any instance of Foobar[A] can be patched up with a bunch of useful methods.

It's just a bunch of generic methods, but with x.foo(y,z) syntax (as opposed to foo(x, y, z)), and with a slightly more convenient type inference behavior.

Putting it all together

Here is a little example that uses everything at once:

trait Tacos[T[_]]:
  def makeTaco[A](a: A): T[A]

  extension [A](t: T[A])
    def doubleWrap: T[T[A]] = makeTaco(t)


object CornTortillaTacos extends Tacos[CornTortillaTacos.CornTortilla]:
  case class CornTortilla[A](wrappedContent: A)
  def makeTaco[A](a: A): CornTortilla[A] = CornTortilla(a)


def intTaco[T[_]](tacos: Tacos[T]) =
  import tacos.*
  makeTaco(42).doubleWrap


println(intTaco(CornTortillaTacos))

Here is what happens:

  • It defines an interface of a taco factory: Tacos. The only thing that a taco factory can do is to take any objects a: A and make a taco with the filling a.
  • Because it's so convenient, and because we don't want to reimplement it in every taco factory, we provide an extension method that can wrap a taco twice, by putting a taco into a taco.
  • We implement a concrete CornTortillaTacos factory, which knows how to wrap anything into CornTortillas. The CornTortillaTacos.CornTortilla is just a type constructor of kind * -> *, so it's suitable as argument for Tacos[T[_]], and we provide the right factory method makeTaco in order to implement Tacos[CornTortillaTacos.CornTortilla]. Nothing fancy here.
  • We define a generic intTaco method, which takes any taco factory, and produces a double-wrapped answer to all questions.
  • We invoke intTaco with CornTortillaTacos, and obtain the double-wrapped CornTortilla(CornTortilla(42)).

I'm afraid that reading Bjarnason/Chiusano while still struggling with basic language features might prove rather difficult.

  • Related