I've put together a semi-beginnerish guide for some of the pesky parts of classes and objects in Scala, ideal for someone coming from from languages like JavaScript & Python, and has barely encountered compiled, strongly typed languages.
I attempt to
1. Give the simplest examples
2. Never ever dare compare Scala to Java
3. Avoid selling you on functional programming or Scala
Each section builds on the previous one.
Recommended Resources
These resources are not directly related to this tutorial, but are very useful for learning Scala and functional programming. without making assumptions about your
[insert from here: functional programming/type theory/object oriented programming/category theory/math phd/actor model/Java 8/reactive programming] prowess.
- Creative Scala (eBook)
- Essential Scala (eBook)
- Tour of Scala
- Gitter - beginner question welcome!
- Mostly Adequate Guide to Functional Programming (eBook)
- Haven't watched this, but I'm sure it's great: Professor Frisby Introduces Composable Functional Javascript (video course)
Traits
Traits are used to share interfaces and fields between classes... Classes and objects can extend traits but traits cannot be instantiated and therefore have no parameters. (docs)
Traits can have both abstract and concrete methods.
trait Person {
// concrete method, can only be overridden by class if it's a def, not a var or val
def name = "Tim"
// abstract method, must be implemented in class
def greeting: String
}
class Tim extends Person {
def greeting: String = s"Welcome, ${name}"
}
val timClass = new Tim
timClass.greeting
// res0: String = Welcome, Tim
You can override name
in the Person trait:
class Tim extends Person {
// override trait name.
override val name: String = "Mr. Tim"
def greeting: String = s"Welcome, ${name}"
}
val timClass = new Tim
timClass.greeting
// res0: String = Welcome, Mr. Tim
Plain Objects
An object on its own is simply a way to package together data and functions. Technically, it's a singleton instance of an anonymous class.
trait Person {
val name: String
}
object Tim extends Person {
val name = "Tim"
}
Tim.name
// res0: String = Tim
Another example from the App trait:
The App trait can be used to quickly turn objects into executable programs." (docs)
The verbose way:
object HelloWorld {
def main(args: Array[String]): Unit = {
val name = args(0)
println(s"Hello, $name")
}
}
// "Hello, dude"
Without the boilerplate:
object HelloWorld extends App {
val name = args(0)
println(s"Hello, $name")
}
To execute, run scala HelloWorld.scala dude
from command line.
Option/Some
Any value that could potentially contain a null/empty value, has an option type, and when assigned, must be wrapped in Some.
val name: Option[String] = Some("Tim")
name.getOrElse("Anon")
// res0: String = Tim
The getOrElse method will provide a default for when name is None
. It will also reduce name
from a Some("Tim")
value, to a plain "Tim"
value.
A nice article on idiomatic handling of Option
values.
The introduction of Maybe can cause some initial discomfort. Users of Swift and Scala will know what I mean as it's baked right into the core libraries under the guise of Option(al). When pushed to deal with null checks all the time (and there are times we know with absolute certainty the value exists), most people can't help but feel it's a tad laborious. However, with time, it will become second nature and you'll likely appreciate the safety. After all, most of the time it will prevent cut corners and save our hides.
Writing unsafe software is like taking care to paint each egg with pastels before hurling it into traffic; like building a retirement home with materials warned against by three little pigs. It will do us well to put some safety into our functions and Maybe helps us do just that. (Mostly Adequate Guide to Functional Programming Ch 8)
Pattern Matching
Pattern matching in Scala, is like a switch
statement on steroids.
val name: Option[String] = Some("Bob")
name match {
// for an exact match on a Tim name
case Some("Tim") => "Hi, Tim!"
// for any other Some(value)
case Some(name) => s"Hi ${name}"
// default case for any other value
case _ => "Anonymous has no name!"
}
// res0: String = Hi Bob
Object's apply() and unapply() Methods
The way I think about these functions is that apply()
is an object's built in constructor, while unapply()
is how an object deconstructs, which particularly useful for pattern matching. These two functions are mutually exclusive, so you can define one without the other. (StackOverflow) Example from the docs.
object Person {
def apply(name: String) = {
s"Welcome--$name"
}
def unapply(name: String): Option[String] = {
val nameSplit = name.split("--")
// we can't be sure the value exists in the object
if (nameSplit.tail.nonEmpty) Some(s"Goodbye--${nameSplit(1)}")
else None
}
}
// using apply() to construct the object
val dudePerson = Person("dude")
println(dudePerson)
// res0: Welcome--dude
// using unapply() to assign a variable
val Person(dudeName) = dudePerson
println(dudeName)
// res1: Goodbye--dude
// using unapply() for pattern matching
val matchTheDude = dudePerson match {
case Person(name) => name
case _ => "Goodbye--anonymous dude"
}
println(matchTheDude)
// res2: Goodbye--dude
Class Companion Object
When an object shares the same name with a class, it is commonly referred to as a companion object. It is useful for storing class functionality which isn't tied to an instance of the class (think of them as static or class methods).
For a class/companion object named Person
with a constructor of name: String
, the object can access anything from the class by calling new Person("Tim").value
, while the class can access anything from its object by calling Person.value
. Even if the values are set to private
.
class Person(val name: String) {
// Person(Tim) instead of the default memory address Person@3018b152
override def toString = s"Person($name)"
}
object Person {
// static method to track number of people objects instantiated
var count = 0
def apply(name: String): Person = {
count = count + 1
// using the Person object as the constructor, allows you to call Person("Tim")
// without the need to write "new Person("Tim")"
new Person(name)
}
def unapply(person: Person): Option[String] = Some(person.name)
}
// calling apply() to instantiate the Person class
val bob = Person("Bob")
// our custom toString function
println(bob.toString)
// res0: Person(Bob)
// the apply() increments the count variable for each new Person
val tim = Person("Tim")
Person.count
// res1: 2
// unapply() with pattern matching
val matchTim = tim match {
case Person(name) => name
case _ => "Sorry, no match"
}
matchTim
// res2: String = Tim
Case Classes
case class
is a type of class, which is useful for storing immutable data with minimal boilerplate. case object
has similar functionality, but for a singleton class.
case class Person(title: Option[String] = None, name: String)
// comes with a companion object apply() method, no need for new Person("Dude")
val dude = Person(name="Dude")
// name parameter is set to an immutable val
dude.name
// res0: String = Dude
// sane toString method, as opposed to Person@721d791f weird hex stuff
dude.toString
// res1: String = Person(None,Dude)
val tim = Person(name="Tim")
// convenient copy method if you only need to change specific values
val timCopy = tim.copy(title=Some("Mr"))
// timCopy: Person = Person(Some(Mr),Tim)
// comes with a companion object unapply() method, useful for pattern matching
timCopy match {
case Person(title, name) => title
case _ => "No title"
}
// res2: Some(Mr)
// sane equals method which compares the content of case classes
dude.equals(tim)
// res3: Boolean = false
// class equality is usually done by comparing the memory address of each class
// instance, that's why you get this weird behavior
class Guy(name: String)
val guy1 = new Guy("guy")
// guy1: Guy = Guy@5f46825d
val guy2 = new Guy("guy")
//guy2: Guy = Guy@17be95d
guy1 == guy2
//res4: Boolean = false
Implicits
A nice example from here:
// probably in a library
class Prefixer(val prefix: String)
def addPrefix(s: String)(implicit p: Prefixer) = p.prefix + s
// then probably in your application
implicit val myImplicitPrefixer = new Prefixer("***")
addPrefix("abc") // returns "***abc"
The Play Framework controllers use implicits, so that any function call from an Action
will gain access to the http request
variable:
def action = Action { implicit request =>
anotherMethod("Some para value")
Ok("Got request [" + request + "]")
}
def anotherMethod(p: String)(implicit request: Request[_]) = {
// do something that needs access to the request
}
implicit
comes in handy when converting case classes to and from Json with the Play Json library. This is simple way to get type-safe JSON when creating an API:
import play.api.libs.json.Json
case class Totals(
subTotal: Double = 0,
estimatedShippingAndInsurance: Double = 0,
estimatedTax: Option[Double] = Some(0),
orderTotal: Double = 0
)
// withDefaultValues is required if you want your case class defaults to be applied // to the Json conversion
object Totals { implicit val writeTotals = Json.using[Json.WithDefaultValues].format[Totals] }
Class Structuring
In object oriented programming, we make a distinction between is-a and has-a. is-a is a candidate for inheritance.
A Product has-a salePrice
and a regularPrice
:
trait Product {
val salePrice: Double
val regularPrice: Double
}
A computer is-a product in a store:
case class Computer(
salePrice: Double,
regularPrice: Double,
processor: String,
operatingSystem: String
) extends Product
Clothes is-a Product as well:
case class Clothes(
salePrice: Double,
regularPrice: Double,
size: Int,
material: String
) extends Product
Another point to consider, Product is of type Computer or Clothes.
A common pattern for is-or (from Essential Scala):
sealed trait TrafficLight {
// pattern matching on case objects
def next: TrafficLight = this match {
case Red => Green
case Green => Yellow
case Yellow => Red
}
}
final case object Red extends TrafficLight
final case object Green extends TrafficLight
final case object Yellow extends TrafficLight
val myLight = Red.next
// res0: TrafficLight = Green
myLight.next
// res0: TrafficLight = Yellow
myLight.next.next
// res1: TrafficLight = Red
sealed trait
is used to tell the compiler to only expect TrafficLight of types Red, Green and Yellow. If we were to leave out Yellow while pattern matching, the compiler would complain that match may not be exhaustive
. final
is to forbid anyone form extending the Red, Green and Yellow classes.
Top comments (1)
Nice article, I like the initial point of not comparing Scala to Java, its hard to find a good explanation of something in Scala that doesn't refer to Java or mention something like "In Java you would do this ... "