Scala at Light Speed, Part 5: Advanced
Learn more about advanced Scala in the final part of this series!
Join the DZone community and get the full member experience.
Join For FreeThis article series is for busy programmers who want to learn Scala fast, in 2 hours or less. The articles are the written version of Rock the JVM's Scala at Light Speed mini-course, which you can find for free on YouTube or on the Rock the JVM website in video form.
This is the fifth article of the series, which will focus on Scala as a functional programming language. You can watch it in video form or in the embedded video below.
So far, we've covered:
- How to get started with Scala
- The very basics: values, expressions, types
- Object orientation: classes, instances, singletons, methods, and basic generics
- Functional programming
- Pattern matching
You may also like: You may also like: Scala at the Speed of Light, Part 1: The Essentials
Lazy Evaluation
In plain language, a lazy value is only evaluated at first use.
xxxxxxxxxx
lazy val aLazyValue = 2
Lazy evaluation is particularly useful when this value takes a long time to compute: you don't need to stall the entire code, you'll only do that when you need the value. Just to prove this, let's compute a value with a side effect, like printing something to the console.
xxxxxxxxxx
lazy val lazyValueWithSideEffect = {
println("I am so very lazy!")
43
}
If you put this in a standalone application and run it without using the value, nothing will show up — that's because the value is not evaluated. If, instead, you add:
xxxxxxxxxx
val eagerValue = lazyValueWithSideEffect + 1
And run the application again, the string I am so very lazy!
will suddenly show up in the application again. (example is most visible on the video)
Lazy values are useful when operating on infinite collections like Streams.
Option
Think of an Option as a "collection" containing at most one element. Options were created to avoid needing to work with nulls, which causes so much headache in the Java world and so much defensiveness in production code.
xxxxxxxxxx
def methodWhichCanReturnNull(): String = "hello, Scala"
val anOption = Option(methodWhichCanReturnNull()) // Some("hello, Scala")
// option = "collection" which contains at most one element: Some(value) or None
val stringProcessing = anOption match {
case Some(string) => s"I have obtained a valid string: $string"
case None => "I obtained nothing"
}
When you have an unsafe method that might return null, you can make it safe by wrapping a call into an Option. The option will be one of two subtypes:
- Some(value), which is a case class
- None, which is an object
We can pattern match Options very easily to obtain the values inside. More importantly, Options work in the same fashion as Lists do, so we can compose Options with map, flatMap, filter, or for-comprehensions as we did with lists. This is the functional style of thinking, where we compose values (in this case Options) to obtain other values.
Try
Try works in a similar way, in that you can think of it like a "collection" containing at most one element. A Try is used when the code you want to evaluate might throw an exception. In the Java world, this causes either lots of runtime crashes or a barrage of try-catch defensiveness.
xxxxxxxxxx
def methodWhichCanThrowException(): String = throw new RuntimeException
val aTry = Try(methodWhichCanThrowException())
// a try = "collection" with either a value if the code went well, or an exception if the code threw one
val anotherStringProcessing = aTry match {
case Success(validValue) => s"I have obtained a valid string: $validValue"
case Failure(ex) => s"I have obtained an exception: $ex"
}
A Try instance can either be:
- a Success containing a real value which you can then use
- or a Failure containing an exception which you can then process
Notice that instead of crashing an application or throwing an exception (which is a side effect), we're, instead, wrapping the value or the exception in a small data structure (we're describing the side effect if it happens).
Future
Futures were created for asynchronous computation, i.e. computation that happens on some thread you have no control over. In a similar fashion, we can think of a Future as a "collection" containing an element. This time, a Future only contains an element when the computation of the value finishes on the thread it runs.
xxxxxxxxxx
import scala.concurrent.ExecutionContext.Implicits.global
val aFuture = Future({
println("Loading...")
Thread.sleep(1000)
println("I have computed a value.")
67
})
Two notes on the above code:
- Code style: You can eliminate the parentheses in the Future constructor when a code block is the argument; parentheses showed for consistency with the basic syntax used so far
- Notice the import at the top; to run a Future, you need an ExecutionContext (= a thread pool), which is why we import a standard one
In this case, there's no way to pattern match a future because you don't know when it will finish. Instead, we usually compose Futures with map, flatMap, and filter, much like we did with lists.
A Soft Intro to Implicits
This is probably the most powerful feature of the Scala compiler. It's so powerful that it's often dangerous, which is why the upcoming Scala 3 is going to change the language a lot around this area. It will still be supported but deprecated/dropped at some point (perhaps in 3.1 or later).
Just to test the waters on implicits, we're going to exemplify two use cases for implicits.
Use-case 1: implicit arguments. A method taking an implicit argument list will need the compiler to automatically fetch a value to inject for those arguments. Example:
xxxxxxxxxx
def aMethodWithImplicitArgs(implicit arg: Int) = arg + 1
implicit val myImplicitInt = 46
println(aMethodWithImplicitArgs) // aMethodWithImplicitArgs(myImplicitInt)
Notice that the method takes an implicit Int as an argument. Given that I've defined an implicit Int
before I call the method, the compiler is happy to inject that value into the method call, so I don't need to provide the arguments. This same mechanism happened with the Future constructor earlier (which is why we imported the global value). There must only be one implicit value of a given type in the same scope. Otherwise, the compiler will complain because it won't know which value you'd like to use.
Use-case 2: implicit conversions. This is an enormous extension power of the Scala language, but comes with dangers. So, use this feature as in the following example:
xxxxxxxxxx
implicit class MyRichInteger(n: Int) {
def isEven() = n % 2 == 0
}
println(23.isEven()) // new MyRichInteger(23).isEven()
// use this carefully
An implicit class can only take one argument. That's for a very good reason: after you make the implicit class available (by either writing it or importing it) in your scope, the compiler is free to auto-wrap Ints into instances of your implicit class.
In regular code, you write 23.isEven
as if isEven
were a method on the Int
type, but in reality, the compiler auto-boxes the number into an implicit class. You can define as many implicit classes you like, as long as you don't have method conflicts.
Implicits bring Scala's type system to a whole new level, as they allow extending types from libraries you don't have any control over, or even standard types like Int or String (as the above example). The unofficial term is "pimping" a library, the technical term is "type enrichment" - use whichever you like.
So, that's a wrap for Scala at Light Speed! There is much, much more to Scala, but I hope this article series was fun and valuable!
- Daniel for Rock the JVM
Further Reading
Opinions expressed by DZone contributors are their own.
Comments