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 String
s, 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 println
s 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 objectsa: A
and make a taco with the fillinga
. - 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 intoCornTortilla
s. TheCornTortillaTacos.CornTortilla
is just a type constructor of kind* -> *
, so it's suitable as argument forTacos[T[_]]
, and we provide the right factory methodmakeTaco
in order to implementTacos[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
withCornTortillaTacos
, and obtain the double-wrappedCornTortilla(CornTortilla(42))
.
I'm afraid that reading Bjarnason/Chiusano while still struggling with basic language features might prove rather difficult.