3

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.

melston
  • 2,198
  • 22
  • 39
  • The `import P.*` just put all the methods defined in `Parsers` in scope, it doesn't know, nor care, about any underlying implementation; it is just sugar syntax so you can do something like `foo(bar)` instead of `P.foo(bar)` - Aout the self reference I guess either `Parsers` or `Reference` define a class, trait, type called `Parser` which is parametric, that is it, nothing fancy nor self-reference. – Luis Miguel Mejía Suárez Jul 28 '22 at 21:49
  • @LuisMiguelMejíaSuárez, I don't want to give the entire listing, but the `Parsers` trait is entirely abstract so there is no actual implementation to make use of by doing this. And the last line that I quoted from the listings implies that it is passing a reference to the `Reference` object as `P`. I was just not aware of this bit of syntax and was looking for some further insight. And, yes, `Reference` defines an `extension` of `Parsers` which, I assume, is the implementation of the abstract bits of the original trait. – melston Jul 29 '22 at 02:42
  • Also, from https://stackoverflow.com/a/1755521/780350, it seems that the `Reference` singleton *does* implement `Parsers.Parser` (through the `extension` methods). But I don't ever recall reading about importing from an *object* instead of a *package*. That seems to be what is happening here. – melston Jul 29 '22 at 02:59
  • _"But the Parsers trait is entirely abstract"_ so what? Of course it is abstract, is a `trait` - _"so there is no actual implementation to make use of by doing this"_ of course there is, `P` is a value, thus is concrete. - _"And the last line that I quoted from the listings implies that it is passing a reference to"_ in **Scala** everything gets passed by value, there is no pass by reference in this language. - _2Reference defines an extension of Parsers"_ it does not, it defines an extension for `Parse` and we still haven't seen the `Parser` definition. _"from an object"_ yes, is just sugar. – Luis Miguel Mejía Suárez Jul 29 '22 at 04:29

1 Answers1

2

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 all objects (not only singletons)

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.

Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
  • Outstanding answer! Thanks. You are probably right that I am not at the level I need to be to really understand the book but I'm not sure where to go to find this information in the first place. I have an ebook of Odersky (5th edition) and I can't find much of this addressed in there, either. – melston Jul 30 '22 at 16:02
  • In defense of Odersky's 5th ed: 2nd paragraph in 12.3 "Imports": _"An `import` clause makes members of a package *or object* available by their names alone"_; Then in 4th paragraph _"Scala imports are actually much more general. [...]imports in Scala can appear anywhere [...] Also, they can refer to arbitrary values"_ And then immediately after that, on page 223, there is `def showFruit(fruit: Fruit) = { import fruit.*; s"${name}s are $color"`, where they import property `color` of an instance `fruit`. So, yeah... Most of it is probably in there, but still, it just takes time & needs exposure. – Andrey Tyukin Jul 31 '22 at 11:02
  • Thanks, Andrey. I guess a search for 'import' was too broad and allowed me to completely miss that. – melston Aug 01 '22 at 21:59