Recently, reader feedback has led me to
realize that I have left behind an important aspect of
Scala's language while crafting this series: Scala's package
and access modifier facilities. So I'm going to take a
moment and cover this before diving into one of the more
functional elements of the language, the apply
mechanism.Packaging
To help segregate code in such a way that it doesn't
conflict with one another, Java™ code provides the
package keyword, creating a lexical namespace in
which classes are declared. In essence, putting a class
Foo in a package named com.tedneward.util
modifies the formal class name to
com.tedneward.util.Foo ; it must be referenced as
such. Java programmers will be quick to point out that they
don't do this, they import the package and thus
save themselves from having to type the formal name out.
This is true, but it merely means that the work of
referencing the class by its formal name falls to the
compiler and bytecode. A quick glance at javap output
reveals this to be the case.
Packages in the Java language have a few quirks to them,
however: The package declaration must appear at the top of
the .java file in which the package-scoped classes appear
(which causes some serious havoc with the language when
trying to apply annotations to the package); the declaration
holds scope across the entire file. This means that the rare
case in which two classes are tightly-coupled across package
boundaries has to be split across files, leading the unwary
to not recognize the tight coupling between the two.
Scala takes a slightly different approach in respect to
packaging, treating it as a combination of the Java
language's declaration approach and C#'s
scope d approach. With that in mind, a Java developer
can do the traditional Java approach and put a package
declaration at the top of a .scala file just as normal Java
classes do; the package declaration applies across the
entire file scope just as it does in Java code.
Alternatively, a Scala developer can use Scala's package
"scoping" approach in which curly braces delimit the scope
of the package statement, as in Listing 1:
Listing 1. Packaging made simple
package com
{
package tedneward
{
package scala
{
package demonstration
{
object App
{
def main(args : Array[String]) : Unit =
{
System.out.println("Howdy, from packaged code!")
args.foreach((i) => System.out.println("Got " + i) )
}
}
}
}
}
}
|
Effectively, this code declares one class, App , or to be
more precise, a single class called
com.tedneward.scala.demonstration.App . Note that
Scala also permits the package names to be dot-separated, so
Listing 1 could be written more tersely as shown in Listing
2:
Listing 2. Packaging made simple
(redux)
package com.tedneward.scala.demonstration
{
object App
{
def main(args : Array[String]) : Unit =
{
System.out.println("Howdy, from packaged code!")
args.foreach((i) => System.out.println("Got " + i) )
}
}
}
|
Use whichever style seems more appropriate because they each compile
into exactly the same code constructs. (The scalac compile
goes ahead and generates the .class files in
package-declared subdirectories as javac does.)
Import
Of course, the logical flip-side of packaging is
import , Scala's mechanism for bringing names into the
current lexical namespace. Readers of this series have
already seen import in a couple of samples
before now, but it's time for me to point out some of
import 's features that will come as a surprise to
Java developers.
First of all, you can use import anywhere
inside the client Scala file, not just at the top of the
file and correspondingly, will have scoped relevance. Thus,
in Listing 3, the java.math.BigInteger import
is scoped entirely to the methods defined inside the object
App and nowhere else. If another class or
object inside of mathfun wanted to use
java.math.BigInteger , it would need to import the
class just as App did. Or if several classes in
mathfun all wanted to use
java.math.BigInteger , the import could occur at the
package level outside the definition of App and
all of the classes in this package scope will have
BigInteger imported.
Listing 3. Import scoping
package com
{
package tedneward
{
package scala
{
// ...
package mathfun
{
object App
{
import java.math.BigInteger
def factorial(arg : BigInteger) : BigInteger =
{
if (arg == BigInteger.ZERO) BigInteger.ONE
else arg multiply (factorial (arg subtract BigInteger.ONE))
}
def main(args : Array[String]) : Unit =
{
if (args.length > 0)
System.out.println("factorial " + args(0) +
" = " + factorial(new BigInteger(args(0))))
else
System.out.println("factorial 0 = 1")
}
}
}
}
}
}
|
Importing doesn't stop there, however. Scala sees no real reason to
differentiate between top-level members and nested ones, so
you can use import to bring not just nested
types into lexical scope, but any member; by importing all
of the names inside java.math.BigInteger , for
example, you can drop the scoped references to ZERO and ONE
to just name references as in Listing 4:
Listing 4. Static imports ... without
the static
package com
{
package tedneward
{
package scala
{
// ...
package mathfun
{
object App
{
import java.math.BigInteger
import BigInteger._
def factorial(arg : BigInteger) : BigInteger =
{
if (arg == ZERO) ONE
else arg multiply (factorial (arg subtract ONE))
}
def main(args : Array[String]) : Unit =
{
if (args.length > 0)
System.out.println("factorial " + args(0) +
" = " + factorial(new BigInteger(args(0))))
else
System.out.println("factorial 0 = 1")
}
}
}
}
}
}
|
By using the underscore (remember the wildcard character in Scala?),
you effectively tell the Scala compiler that all of the
members inside BigInteger should be brought
into scope. And because BigInteger has already
been put into scope by the previous import statement,
there's no need to explicitly package-qualify the class
name. In fact, these could even be combined into a single
statement because import can take multiple,
comma-separated targets to import (shown in Listing 5):
Listing 5. Bulk imports
package com
{
package tedneward
{
package scala
{
// ...
package mathfun
{
object App
{
import java.math.BigInteger, BigInteger._
def factorial(arg : BigInteger) : BigInteger =
{
if (arg == ZERO) ONE
else arg multiply (factorial (arg subtract ONE))
}
def main(args : Array[String]) : Unit =
{
if (args.length > 0)
System.out.println("factorial " + args(0) +
" = " + factorial(new BigInteger(args(0))))
else
System.out.println("factorial 0 = 1")
}
}
}
}
}
}
|
This saves you a line or two. Note that the two cannot be combined: the
first imports the BigInteger class itself and
the second, the various members inside that first class.
You can also use import to introduce other
non-constant members as well. For example, consider a math
utility library (of questionable value perhaps, but
still...) in Listing 6:
Listing 6. Enron's accounting code
package com
{
package tedneward
{
package scala
{
// ...
package mathfun
{
object BizarroMath
{
def bizplus(a : Int, b : Int) = { a - b }
def bizminus(a : Int, b : Int) = { a + b }
def bizmultiply(a : Int, b : Int) = { a / b }
def bizdivide(a : Int, b : Int) = { a * b }
}
}
}
}
}
|
Using this library could get quite annoying over time, having to type
BizarroMath every time one of its members was
requested, but Scala allows for each of the members of
BizarroMath to be imported into the top-level lexical
namespace, almost as if they were global functions (shown in
Listing 7):
Listing 7. Calculating Enron's
expenses
package com
{
package tedneward
{
package scala
{
package demonstration
{
object App2
{
def main(args : Array[String]) : Unit =
{
import com.tedneward.scala.mathfun.BizarroMath._
System.out.println("2 + 2 = " + bizplus(2,2))
}
}
}
}
}
}
|
There are other interesting constructs that would allow a Scala
developer to write the more natural 2 bizplus 2 ,
but that will have to wait for another day. (Readers curious
about a potentially heavily-abusable Scala feature can look
at the Scala implicit construct as covered in
Programming in Scala by Odersky, Spoon, and Venners.)
Access
While packaging (and importing) are part of the
encapsulation and packaging story in Scala, a large part of
it, as with Java code, lies in its ability to restrict
access to certain members in a selective way — in other
words, in Scala's ability to mark certain members "public,"
"private," or somewhere in-between.
The Java language has four levels of access: public,
private, protected, and package-level access (frustratingly
applied by leaving out any keyword). Scala:
- Does away with package-level qualification (in a
way)
- Uses "public" by default
- Specifies "private" to mean "accessible only to this
scope"
By contrast, "protected" is definitely different from its
counterpart in Java code; where a Java protected member is
accessible to both subclasses and the package in which the
member is defined, Scala chooses to grant access only to
subclasses. This means that Scala's version of protected is
more restrictive (although arguably more intuitively so)
than the Java version.
Where Scala truly steps away from Java code, however, is
that access modifiers in Scala can be "qualified" with a
package name, indicating a level of access up to
which the member may be accessed. For example, if the
BizarroMath package wants to grant member access to
other members of the same package (but not subclasses), it
can use the code in Listing 8 to do so:
Listing 8. Enron's accounting code
package com
{
package tedneward
{
package scala
{
// ...
package mathfun
{
object BizarroMath
{
def bizplus(a : Int, b : Int) = { a - b }
def bizminus(a : Int, b : Int) = { a + b }
def bizmultiply(a : Int, b : Int) = { a / b }
def bizdivide(a : Int, b : Int) = { a * b }
private[mathfun] def bizexp(a : Int, b: Int) = 0
}
}
}
}
}
|
Note the private[mathfun] expression here. In essence, the
access modifier is saying that this member is private up
to the package mathfun ; this means that any
member of the package mathfun has access to
bizexp but nothing outside of that package
does, including subclasses.
The powerful meaning of this is that any package can be
declared in the "private" or "protected" declaration all the
way up to com (or even _root_
which is an alias for the root namespace, thus essentially
making private[_root_] the same thing as
"public"). This provides a degree of flexibility in access
specification far beyond what the Java language provides.
In fact, Scala offers one more degree of access
specification: the object-private specification,
illustrated by private[this] , which stipulates
that the member in question can only be seen by members
called on that same object, not from different objects, even
if they are of the same type. (This closes a small hole in
the Java access specification system that was useful for
Java programming interview questions and not much more.)
Note that the access modifiers will have to map on top of
the JVM at some level and as a result, some of the
subtleties in their definition will be lost when compiled or
called from regular Java code. For example, the BizarroMath
example above (with the private[mathfun] -declared
member bizexp ) will generate the class
definition in Listing 9 (when viewed with javap):
Listing 9. Enron's accounting library,
JVM view
Compiled from "packaging.scala"
public final class com.tedneward.scala.mathfun.BizarroMath
extends java.lang.Object
{
public static final int $tag();
public static final int bizexp(int, int);
public static final int bizdivide(int, int);
public static final int bizmultiply(int, int);
public static final int bizminus(int, int);
public static final int bizplus(int, int);
}
|
As is obvious from the second line of the compiled BizarroMath
class, the bizexp() method was given a
JVM-level access specifier of public which
means that the subtle private[mathfun]
distinction was lost once the Scala compiler was finished
with its access checks. As a result, for Scala code that is
intended to be used from Java code, I'd prefer to stick with
the traditional "private" and "public" definitions. (Even
"protected" will sometimes end up mapping to JVM-level
"public," so when in doubt, consult javap against the actual
compiled bytecode to be certain of its access level.)
Application
In the preceding article in the series ("Collection
types"), when talking about arrays in Scala (Array[T] s
to be exact) I said, "obtaining the i'th element of the
array" was in fact "another one of those methods with funny
names...." As it turns out, although I didn't want to get
into the details then, this wasn't exactly true.
OK, I admit it, I lied.
Technically, the use of the parentheses against the
Array[T] class is a tad bit more complicated than
simply a "method with a funny name"; Scala reserves a
particular nomenclature association for that particular
sequence of characters (that being the
left-parens-right-parens sequence) because that is so often
used with a particular intent in mind: that of "doing"
something (or in functionalspeak, "applying" something to
something).
In other words, Scala has a special syntax (more
accurately, a special syntactic relationship) in place for
the "application" operator "()". To be precise, Scala
recognizes the method called apply() as the
method to invoke when said object is invoked using ()
as the method call. For example, a class that wants to
behave as a functor (an object that acts as a
function) can define an apply method to provide
function- or method-like semantics:
Listing 10. Play that Functor music,
code boy!
class ApplyTest
{
import org.junit._, Assert._
@Test def simpleApply =
{
class Functor
{
def apply() : String =
{
"Doing something without arguments"
}
def apply(i : Int) : String =
{
if (i == 0)
"Done"
else
"Applying... " + apply(i - 1)
}
}
val f = new Functor
assertEquals("Doing something without arguments", f() )
assertEquals("Applying... Applying... Applying... Done", f(3))
}
}
|
Curious readers will be wondering what makes a functor different from
an anonymous function or closure. As it turns out, the
relationship is fairly obvious: The Function1 type in the
standard Scala library (meaning a function that takes one
parameter) has an apply method on its
definition. A quick glance through some of the generated
Scala anonymous classes for Scala anonymous functions will
reveal that the generated classes are descendants of
Function1 (or Function2 or Function3, depending on how many
parameters the function takes).
This means that where anonymous or named functions don't
necessarily fit the design approach desired, a Scala
developer can create a functor class, provide
it with some initialization data stored in fields, and then
execute it via () without any common base class
required (as would be the case for a traditional Strategy
pattern implementation):
Listing 11. I said 'play that Functor
music, code boy!'
class ApplyTest
{
import org.junit._, Assert._
// ...
@Test def functorStrategy =
{
class GoodAdder
{
def apply(lhs : Int, rhs : Int) : Int = lhs + rhs
}
class BadAdder(inflateResults : Int)
{
def apply(lhs : Int, rhs : Int) : Int = lhs + rhs * inflateResults
}
val calculator = new GoodAdder
assertEquals(4, calculator(2, 2))
val enronAccountant = new BadAdder(50)
assertEquals(102, enronAccountant(2, 2))
}
}
|
Any class that provides the appropriately-argumented apply
method will work when called as long as the arguments line
up in number and in type.
Conclusion
Scala's packaging, import, and access modifier mechanisms
provide the fine degree of control and encapsulation that a
traditional Java programmer has never enjoyed. For example,
they offer the ability to import select methods of an
object, making them appear as global methods, without the
traditional drawbacks of global methods; they make working
with those methods exceedingly easy, particularly if those
methods are methods that provide higher-order functionality,
such as the fictitious tryWithLogging function
introduced earlier in this series ("Don't
get thrown for a loop!").
Similarly, the "application" mechanism allows Scala to
hide execution details behind a functional facade such that
programmers may not even know (or care) that the thing
they are invoking isn't actually a function but an object
imbued with significant complexity. The mechanism provides
another dimension to Scala's functional nature, one which
can certainly be done from the Java language (or C# or C++
for that matter) but not with the degree of syntactic purity
Scala provides.
That's it for this installment; until next time, enjoy! |
No comments:
Post a Comment