Tomasz Wegrzanowski
Posted on January 17, 2022
Scala is one of the JVM languages trying to dethrone Java. Currently Kotlin is leading this race by a lot, but Scala, Clojure, and Groovy are all quite popular, with JRuby being somewhat behind them in the race.
This post is about Scala 2. Scala 3 is currently being developed, which plans to make fundamental changes to the language.
Hello, World!
println("Hello, World!")
You can run it without separate compilation:
$ scala hello.scala
Hello, World!
FizzBuzz
The FizzBuzz is very reasonable:
// FizzBuzz in Scala
for (i <- 1 to 100) {
if (i % 15 == 0) {
println("FizzBuzz")
} else if (i % 5 == 0) {
println("Buzz")
} else if (i % 3 == 0) {
println("Fizz")
} else {
println(i)
}
}
Unicode
Just like Kotlin, Clojure, and Groovy, Scala string handling is also broken for any data involving characters outside Unicode Basic Plane. It's broken on JVM, and so every language using it directly has broken string handling. JRuby is the only major JVM language which had courage to fix it, and pay performance price for that.
val strings = List("Hello", "ลนรณลw", "๐ฉ")
for (s <- strings) {
println(s"Length of $s is ${s.length}")
}
$ scala unicode.scala
Length of Hello is 5
Length of ลนรณลw is 4
Length of ๐ฉ is 2
On the upside, Scala has nice string interpolation. Of course with its own unique syntax, as that's one thing which every language uses different syntax for, with no consensus emerging so far.
Fibonacci
On the upside, we don't need pointless return
.
On the downside, we need to declare types for both arguments and return values, type inference is completely failing us here. There's some limited type inference, but in this case it would give us error: recursive method fib needs result type
.
def fib(n : Int) : Int = {
if (n <= 2) {
1
} else {
fib(n - 1) + fib(n - 2)
}
}
for (n <- 1 to 20) {
println(s"fib($n) = ${fib(n)}")
}
$ scala fib.scala
fib(1) = 1
fib(2) = 1
fib(3) = 2
fib(4) = 3
fib(5) = 5
fib(6) = 8
fib(7) = 13
fib(8) = 21
fib(9) = 34
fib(10) = 55
fib(11) = 89
fib(12) = 144
fib(13) = 233
fib(14) = 377
fib(15) = 610
fib(16) = 987
fib(17) = 1597
fib(18) = 2584
fib(19) = 4181
fib(20) = 6765
Types
Scala just like Haskell has extremely complicated type system, featuring type classes. By the way this is one of the features which is getting full rewrite in Scala 3, so presumably Scala devs are not too happy with its state.
Let's try to define a function like this, in a way that would allow valid combinations (like Int
+ Int
, or String
+ String
), but not invalid combinations (like Int
+ String
, or Double
+ HttpRequest
):
def add(a, b) = {
println(s"${a} + ${b} = ${a + b}")
}
The first step is to make this function generic:
def add[T](a : T, b : T) = {
println(s"${a} + ${b} = ${a + b}")
}
This won't work - as +
is not defined for every type. Scala gives a completely meaningless error message (required: String; incompatible interpolation method s
) for this, but it's no big deal.
What works is defining a type class (spelled a trait
in Scala, but documentation still refers to it as a "type class") Additive
. Then defining various instances of Additive[...]
. Then passing Additive
as an implicit parameter.
trait Additive[T] {
def plus(x: T, y: T): T
}
implicit object AdditiveInt extends Additive[Int] {
def plus(x: Int, y: Int): Int = x + y
}
implicit object AdditiveDouble extends Additive[Double] {
def plus(x: Double, y: Double): Double = x + y
}
implicit object AdditiveString extends Additive[String] {
def plus(x: String, y: String): String = x + y
}
def add[T](a : T, b : T)(implicit additive: Additive[T]) = {
println(s"${a} + ${b} = ${additive.plus(a, b)}")
}
add(6, 0.9)
add(400, 20)
add("foo", "bar")
How does it compare with other languages with complex type systems:
- Crystal managed to figure it out with zero type annotations
- Scala managed to do this, with very heavy annotations
- Haskell almost works, with very heavy annotations and a few language extensions, but then in the end it doesn't (this is largely because Haskell string is not a real type, it's just a list of characters, and Haskell is bad at type classes over such composite types)
- OCaml doesn't even try
Simple Types
For simple immutable classes, Scala supports case class
shortcut, very similar to Kotlin's data class
. It defines common basic operations like ==
, .toString()
, hashCode()
and so on.
case class Point(x : Double, y : Double) {
def length() = Math.sqrt(x * x + y * y)
}
val a = List(1, 2, 3)
val b = List(1, 2, 3)
val c = Point(30.0, 40.0)
val d = Point(30.0, 40.0)
println(a == b)
println(c == d)
println(null == d)
println(s"len of ${c} is ${c.length()}")
$ scala point.scala
false
true
false
len of Point(30.0,40.0) is 50.0
This is all fine.
Generic Point Type
So let's define a generic Point type that can take be Point[Int]
or Point[Double]
or such, and always implement +
. This can be done, but it's quite convoluted:
trait Additive[T] {
def plus(x: T, y: T): T
}
implicit object AdditiveInt extends Additive[Int] {
def plus(x: Int, y: Int): Int = x + y
}
implicit object AdditiveDouble extends Additive[Double] {
def plus(x: Double, y: Double): Double = x + y
}
implicit object AdditiveString extends Additive[String] {
def plus(x: String, y: String): String = x + y
}
case class Point[T](x : T, y : T)(implicit additive: Additive[T]) {
def +(other : Point[T]) = {
Point(additive.plus(x, other.x), additive.plus(y, other.y))
}
}
implicit def AdditivePoint[T] : Additive[Point[T]] = {
new Additive[Point[T]] {
def plus(x: Point[T], y: Point[T]): Point[T] = x + y
}
}
def add[T](a : T, b : T)(implicit additive: Additive[T]) = {
println(s"${a} + ${b} = ${additive.plus(a, b)}")
}
println(Point(1, 2) + Point(3, 4))
add(Point(300, 60), Point(120, 9))
add(Point(6.0, 250.0), Point(0.9, 170.0))
add(Point("foo", "much"), Point("bar", "wow"))
$ scala typeclasses2.scala
Point(4,6)
Point(300,60) + Point(120,9) = Point(420,69)
Point(6.0,250.0) + Point(0.9,170.0) = Point(6.9,420.0)
Point(foo,much) + Point(bar,wow) = Point(foobar,muchwow)
What Scala has is definitely among the most complicated type systems ever, and we're really just barely scratching the surface here.
One important advantage Scala has over Haskell is that if type system really doesn't like what you're doing, you can just declare something as Any
and do all the type checking at runtime. Haskell is fundamentalist about type checking, and if type checker doesn't like your perfectly valid code, there's nothing you can do about that.
What Scala notably lacks is union types, which are extremely necessary for such basic things like parsing JSON. Scala 3 plans to add union types. And speaking of parsing JSON...
Libraries
Scala doesn't come with any JSON library, which in this day and age, is ridiculous.
It looks like there are two popular package managers for Scala - Scala-specific sbt
and more generic gradle
. Both are convoluted enough that I'll just pass on this whole mess to keep this post reasonable size.
That's not a Scala specific issue, the whole JVM world suffers from extremely convoluted package management, and lacks anything comparable to rubygems
or npm
or pip
.
This is sort of acceptable for bigger projects, as all that sbt
or gradle
setup will be a tiny part, but for small ones, it's a huge pain.
This might have been acceptable 10 years ago, nobody should accept this today.
Functional Programming
Basic functional programming patterns work just as you'd expect it. There's implicit arguments _
, similar to Perl's $_
or Kotlin's it
.
val alist = List(1, 2, 3, 4, 5)
println(alist.map{ x => x * 2 })
println(alist.map{ _ * 2 })
println(alist.filter{ _ % 2 == 1 })
println(alist.reduce{ (a, b) => a + b })
JVM Interoperability
Scala can call any JVM code, so in theory, it should have access to the entire JVM ecosystem, right? Well, in practice if you actually try to do that, it will look like ass:
import java.awt._
import java.awt.event._
import javax.swing._
var clicks = 0
val f = new JFrame()
f.setLayout(new GridLayout(2, 1))
f.setSize(300, 300)
val l = new JLabel("0")
l.setHorizontalAlignment(SwingConstants.CENTER)
f.add(l)
val b = new JButton("click me")
f.add(b)
b.addActionListener(new ActionListener {
def actionPerformed(e: ActionEvent) : Unit = {
clicks += 1
l.setText(s"${clicks} clicks")
}
})
f.setVisible(true)
So in practice you'll be using Scala wrappers, like this one.
How bad is it when you don't have a Scala wrapper depends on a library, but Java and Scala diverge semantically far more than let's say Java and Kotlin.
Should you use Scala?
I'd recommend against it.
If you don't need JVM, Scala is not for you. Scala is a ridiculously overcomplicated language, in particular with a ridiculously overcomplicated type system, somehow still lacking basic functionality like parsing JSON or modern package manager, with a terrible track record of breaking backwards compatibility (and Scala 3 coming soon to break it even harder), highly fragmented ecosystem, and apparently a lot of maintainer drama on top of it (I didn't look too closely at that).
If you specifically need something that runs on a JVM, it's a closer call, but I'd still not recommend it. Scala can use JVM libraries, but due to semantic mismatch, it will be quite awkward. And apparently the biggest JVM ecosystem - Android - isn't even really supported by Scala. For the "better Java" role Scala was aiming at, Kotlin just does it much better. If you're more flexible about your JVM language, one of Clojure, Groovy, or JRuby might be a better choice.
On the other hand, if you need something that runs on a JVM, but you don't care about Android, and not actually need to use too many JVM libraries (only the popular ones that mostly have Scala wrappers), and you actively want Scala's ridiculously overcomplicated type systems, and aren't terribly bothered by backwards compatibility issues, and so on - then Scala might actually work for you. I don't expect that to be too many people.
Code
All code examples for the series will be in this repository.
Posted on January 17, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.