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 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 CornTortilla
s. 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.