So, you have some Haskell experience and want (or need to) write Scala code. The following is a rough map with the main differences, similarities, major gotchas, and most useful resources.
📹 Context: subjective-production-backend experience, web apps, microservices, shuffling jsons, and all that stuff.
🥈 Scala 2 and Scala 3. In 2024, some projects still haven’t upgraded to Scala 3. Unless it includes the company you’re working for, you don’t need to worry about Scala 2.
Most of the stuff here applies to both; we lean towards Scala 3 and explicitly describe both if there is a big difference.
📹 Hate reading articles? Check out the complementary video, which covers the same content.
Basic syntax and attitude
You should pick up syntax by yourself, but here are the things you should conquer first:
Driving on the wrong side of the road
Forget about reading code from right to left. You’re not in Haskell land.
-- read from right to left
foo err = pure $ Left $ errorMessage err
Scala, similar to Java, often chains from left to right:
// read from left to right
def foo(err: Error) = err.errorMessage().asLeft.pure[IO]
At the same time, you will often encounter code like this:
def fee(err: Error) = (Left(err.errorMessage())).pure[IO]
def faa(err: Error) = IO.delay(Left(err.errorMessage()))
The order and syntax are different due to using methods and not functions.
Methods
def fee(err: Error) = (Left(err.errorMessage())).pure[IO]
def faa(err: Error) = IO.delay(Left(err.errorMessage()))
-
errorMessage
is a normal method on a class (Error
) — typical OOP. -
Left
is a constructor (Either
). -
delay
is a static method on a singleton object (IO
). -
pure
is an extension method (for anything with anApplicative
).
The first variant particularly relies on extension methods to get this consistent syntax:
def foo(err: Error) = err.errorMessage().asLeft.pure[IO]
It’s not as complicated as it might appear.
Just one more thing.
Functions and methods
In Scala, there are Functions:
val function1: String => Int = _.length()
And we can convert methods to functions:
class Tmp:
def method(s: String): Int = s.length()
val function2: String => Int = Tmp().method
Tmp().method("test") // 4
function1("test") // 4
function2("test") // 4
🥈 In Scala 2, it’s slightly more verbose:
new Tmp().method _
Even if you miss functions, get used to declaring methods (and when needed use them as functions). The transitions are so seamless that I usually don’t think about them. And if you are new to OOP, good luck. See the official docs, write/read code, and give it some time.
Function and method signatures
Functions are expressions and their types can be (often) inferred:
val function2 = Tmp().method
val nope = _.length()
When you declare a non-zero-arity methods, you can’t omit the types of parameters:
def method(s: String): Int = s.length()
While the return type is optional (it’s usually a bad practice):
def method(s: String) = s.length()
The unit return type (doesn’t return any value):
def helloWorld(): Unit = {
println("Hello, World!")
}
⚠️ It’s not functional but something you might encounter.
In Scala, we can use default parameter values, named arguments…
def helloWorld(greet: String = "Hello", name: String = "World"): Unit = {
println(s"$greet, $name!")
}
helloWorld(name = "User")
def helloWorld(greet: String)(name: String, alias: String): Unit = ???
💡 The
???
is almost likeundefined
(only it’s not lazy and not type-inference friendly).
Names don’t matter?
Scala community has less tolerance for operators.
def foo(userId: UserId): IO[Result] = for
user <- fetchUser(userId)
subscription <- findSubscription(user.subscriptionId)
yield subscription.fold(_ => defaultSubscription, withDiscount)
There’re some in DSLs, but there no <$>
, there is map
; there is no >>=
or <*>
, but there are flatMap
and mapN
:
"1".some.flatMap(_.toIntOption) // Some(1)
(2.some, 1.some).mapN(_ + _) // Some(3)
There is traverse
but no mapM
, see either traverse
or foreach
. This depends on the library and we’ll talk about this later.
💡 Friendly advice: also get familiar with
flatTap
,tap
, and alike.
Basic concepts and fp
Function composition
Function composition isn’t common. There are ways to do it (e.g., there is compose
and andThen
), but nobody does it (e.g., currying and type inference aren’t helping).
Currying
Similar here. You can curry a function but rarely want to or need to.
def helloWorld(greet: String, name: String): String =
s"$greet, $name!"
val one = helloWorld.curried // : String => String => String
val two = one("Hello") // : String => String
val res = two("World") // Hello, World!
Because, for instance, you can partially apply “normal” scala functions/methods:
val two = helloWorld("Hello", _) // : String => String
val res = two("World") // Hello, World!
On a relevant note, get familiar with tuples and function parameters (and how they play and don’t play together).
val cache = Map.empty[String, Int]
def foo(t: (String, Int)): Int = ???
// This works in Scala 2 and Scala 3
cache.map(foo)
def bar(s: String, i: Int): Int = ???
// This works only in Scala 3
cache.map(bar)
🥈 In Scala 2, there are cases when you need to be explicit (see
tupled
) and use more explicit parenthesis.
Purity
Scala is not Haskell. For example, you can add a println
✨ anywhere ✨.
You should use a linter or an alternative way to enable recommended compiler options for fp scala. For example, sbt-tpolecat.
Laziness
If you want a lazy variable, see lazy val
. If you want a lazy list, see standard library LazyList
or alternatives in other libraries (for example, fs2).
However, if you want another kind of laziness: make some thunks, write recursion, or whatever, it’s not that straightforward — beware of stack (safety).
- See cats-effect
IO
andZIO
(we coverIO
later). - See cats
Eval
data type (or other alternatives). - See
@tailrec
. - See
tailRecM
and other ways to trampoline.
⚠️ Beware of the standard library Futures.
Modules and Imports
Good news: Scala has first-class modules and you don’t need to qualify imports or prefix fields. And you can nest them!
class One:
class Two:
val three = 3
Bad news: you have to get familiar with classes, objects, companion objects, and how to choose where to put your functions.
Terrible news: you still have to worry about imports because they can affect the program’s behavior (we talk about this (implicits
) later).
Standard library
Scala comes with a bundle of immutable collections (List
, Vector
, Map
, Set
, etc.). In the beginning, pay attention, ensure that you are using immutable versions, and stay away from overgeneralized classes, like Seq
.
Also, be open-minded — in many cases, Scala has better methods/combinators than you might expect.
Equality
In Scala 2, multiversal equality was hell for me:
4 == Some(4)
In Scala 3, it’s not allowed:
3 == Some(3)
^^^^^^^^^^^
Values of types Int and Option[Int] cannot be compared
But out of the box, you can still shoot yourself:
case class A(i: Int)
case class B(s: String)
A(1) == B("1") // false
You can disable it with the strictEquality
compiler flag:
import scala.language.strictEquality
case class A(i: Int) derives CanEqual
case class B(s: String) derives CanEqual
A(1) == A(2) // false
A(1) == B("1")
// ^^^^^^^^^^^
// Values of types A and B cannot be compared
Types
Type inference
When I was thinking about this guide a couple of years ago, I thought it was going to be the beefiest chapter. Luckily, Scala 3 is pretty good at type inference.
And yeah, it’s still not Haskell and sometimes you need to help the compiler:
val id = a => a
// error:
// Missing parameter type
// I could not infer the type of the parameter a
val id: [A] => A => A =
[A] => a => a
val idInt = (a: Int) => a
It could come up during refactoring. For example, this is fine
case class Foo(s: Set[Int])
val f1 = Foo(Set.empty)
And this doesn’t compile (empty
should be helped):
case class Foo(s: Set[Int])
val s1 = Set.empty
val f1 = Foo(s1)
But once again, don’t worry, it’s mostly good. For instance, using monad transformers used to be type-inference hell — in Scala 3, it’s fine:
def foo(userId: UserId): IO[Result] = (for
user <- EitherT.right(fetchUser(userId))
subscription <- EitherT(findSubscription(user.subscriptionId))
_ <- EitherT.right(IO.println("Log message"))
yield withDiscount(subscription)).valueOr(_ => defaultSubscription)
🥈 If you’re on Scala 2, you might need to get in the habit of annotating intermediate values. And just be gentler to the compiler!
Union types
Union types are great.
But even union types need an occasional help:
def foo(userId: UserId): EitherT[IO, MyError, String] = for
x <- thisThrowsA() // At least one needs to be explicitly casted
y <- thisThrowsB().leftWiden[MyError]
yield "result"
case class NotFound()
case class BadRequest()
type MyError = NotFound | BadRequest
def thisThrowsA(): EitherT[IO, NotFound, String] = ???
def thisThrowsB(): EitherT[IO, BadRequest, String] = ???
Product types
Product types aren’t bad either. You use case classes:
case class User(name: String, subscriptionId: SubscriptionId)
val user = User("Kat", SubscriptionId("paypal-7"))
user.name // Kat
Which come with a copy
method to modify fields:
val resu = user.copy(subscriptionId = SubscriptionId("apple-12"))
And 95% of the time it’s enough. When you need to, you can use optics (for example, via monocle) or data transformations via libraries like chimney.
Sum types
Sum types are a bit more awkward.
In Scala 2, we used to model sum types with sealed
trait
hierarchies:
sealed trait Role
case class Customer(userId: UserId) extends Role
case class Admin(userId: UserId) extends Role
case object Anon extends Role
We used to do the same for enums (with some boilerplate) or use the enumeratum library.
sealed trait Role
case object Customer extends Role
case object Admin extends Role
case object Anon extends Role
🤔 enumeratum was made as an alternative to Scala-2-built-in
Enumeration
.
In Scala 3, we have nicer enums:
enum Role:
case Customer, Admin, Anon
Which are general enough to support ADTs:
enum Role:
case Customer(userId: UserId)
case Admin
case Anon
Note: Some still use enumeratum with Scala 3.
Newtypes
Newtypes are even more awkward.
In Scala 2, we used Value Classes:
class SubscriptionId(val value: String) extends AnyVal
Scala 3 has Opaque Types:
opaque type UserId = String
But the thing is, by themselves, both aren’t ergonomic and require boilerplate. So, you need either embrace manual wrapping, unwrapping, and other boilerplate OR use one of the many newtype libraries.
Pattern matching
There shouldn’t be anything too surprising about pattern matching (just watch out for parenthesis):
def getName(user: Option[User]): String =
user match {
case Some(User(name, _)) if name.nonEmpty => name
case _ => "anon"
}
However, you should know where pattern matching comes from. Scala allows pattern matching on objects with an unapply
method. Case classes (like User
) and enums (like Role
) possess it out of the box. But if we need to, we can provide additional unapply
methods or implement unapply
for other classes.
class SubscriptionId(val value: String) extends AnyVal
object SubscriptionId:
def unapply(id: SubscriptionId): Option[String] =
id.value.split("-").lastOption
SubscriptionId("paypal-12") match {
case SubscriptionId(id) => id
case _ => "oops"
}
💡 See extractor objects.
Polymorphism
Type parameters are confined in square brackets:
def filter[A](list: List[A], p: A => Boolean): List[A] =
list.filter(p)
Square brackets are also used for type applications:
val x = List.empty[Int]
Type classes, implicits and givens
I don’t think it makes sense for me to go into too much detail here, especially, given the differences between Scala 2 and Scala 3. Just a few things you should put into your hippocampus:
In Scala 2, instance declarations are implicits:
implicit val example: Monad[Option] = new Monad[Option] {
def pure[A](a: A): Option[A] = Some(a)
def flatMap[A, B](ma: Option[A])(f: A => Option[B]): Option[B] =
ma match {
case Some(x) => f(x)
case None => None
}
}
In Scala 3, type classes are more integrated; you write given
instances:
given Monad[Option] with {
def pure[A](a: A): Option[A] = Some(a)
def flatMap[A, B](ma: Option[A])(f: A => Option[B]): Option[B] =
ma match {
case Some(x) => f(x)
case None => None
}
}
In Scala 2 and Scala 3, context looks something like this:
// ................
def foo[F[_]: Monad, A](fa: F[A]): F[(A, A)] =
for
a1 <- fa
a2 <- fa
yield (a1, a2)
💡
F[_]
andF[A]
in Scala is as conventional asm a
in Haskell.
Sometimes, in Scala 2, they look like this:
def foo[F[_], A](fa: F[A])(implicit Monad: Monad[F]): F[(A, A)] = ???
Instances
It’s common to put instances into companion objects:
case class User(name: String, subscriptionId: SubscriptionId)
object User:
implicit val codec: Codec[User] = deriveCodec[User]
Another place to look for instances is objects named implicits
, for example.
import my.something.implicits._
This means that you have to remember what to import, and imports technically affect the logic of the program. In application code, it used to be somewhat common but seems to be less popular. It’s still the way to get instances for libraries that integrate with other libraries. For example, iron + circe.
Also, in Scala 3, there is a special form of import for given instances:
import my.something.given // Scala 3
💡 Don’t forget to read more about implicits or givens on your own.
Deriving (from library user perspective)
In Scala 2, the most popular ways to get instances are automatic and semi-automatic derivations.
Automatic derivation is when you import something and “magically” get all the instances and functionality; for example, circe json decoders:
import io.circe.generic.auto._
Semi-automatic derivation is a bit more explicit, for example:
import io.circe.generic.semiauto._
implicit val codec: Codec[User] = deriveCodec[User]
Scala 3 has type class derivation:
case class User(name: String, subscriptionId: SubscriptionId)
derives ConfiguredCodec
Note that you can still use semi-auto derivations with Scala 3 (when needed):
object User:
given Codec[User] = deriveCodec[User]
Consult with the docs of concrete libraries.
Meta Programming
- There are “experimental” macros in Scala 2 and multiple features in Scala 3.
- There is shapeless (for scrap-your-boilerplate-like generic programming) for Scala 2 and built-in “shapeless” mechanism in Scala 3
Deriving (from library author perspective)
In Scala 2, it’s common to use shapeless and magnolia for typeclass derivation.
Scala 3 has built-in low level derivation. It’s still common to use shapeless and magnolia.
Best practices
Failure handling
- Idiomatic Scala code does not use
null
. Don’t worry. -
Either
isEither
,Maybe
is calledOption
. - You might see
Try
here and there. - See
Throwable
(cousin ofException
andSomeExceptions
)
For everything else, see what your stack/libraries of choice have in terms of failure handling.
Styles and flavors
There existed a lot of different scalas throughout the years. In 2024, fp-leaning industry-leaning scala converges into two: typelevel and zio stacks. Roughly speaking, both come with their own standard library (prelude), IO runtime, concurrency, libraries, and best practices.
🤔 Scala’s standard library is quite functional, but not functional enough. That’s why we have these auxiliary ecosystems.
Even more roughly speaking:
- If you’re leaning towards using mtl/transformers, see typelevel.
- If you’re leaning towards using app monads with “baked-in”
EitherT
andResourceT
, see zio.
There was a moment when people used free and effect system in production Scala code, but it sucked. So, I don’t think anyone uses either in prod these days. Some library authors still use free.
🤔 There is a hype train forming around “direct-style” Scala. Actually, there are multiple trains — because the term is so ambiguous — multiple styles packaged and sold under the same name. If you’re curious, look into it yourself.
If you get an “fp” scala job (around 2024), the newer services are going to be written using typelevel or zio (and there probably going to be legacy code in other styles).
Typelevel / cats
- cats and cats-effect are the core of the typelevel stack
- the other two pillars are fs2, and http4s
- the other gotos are circe for json and doobie for databases
- and many others: kittens, cats-mtl, etc.
🤔 Note that nothing stops you from using alternative libraries; especially, if they provide required instances or interoperability/conversions. For example, it seems common to use
tapir
instead (on top of)http4s
for writing http servers in the last years. Tapir integrates with all major Scala stacks.
It’s common to organize code via “tagless final”:
trait Subscription[F[_]] {
def fetchSubscription(subscriptionId: SubscriptionId): F[Subscription]
def revokeSubscription(subscriptionId: SubscriptionId): F[Unit]
def findSubscription(userId: UserId): F[Option[UserId]]
}
It’s common to tie your app together via Resource
:
def server: Resource[IO, Resource[IO, Server]] =
for
config <- Config.resource
logger <- Tracing.makeLogger[IO](config.logLevel)
client <- HttpClientBuilder.build
redis <- Redis.resource(config.redis)
kafka <- KafkaConsumer.resource(config.kafka)
db <- Postgres.withConnectionPool(config.db).resource
httpApp = Business.make(config, client, redis, kafka, db)
yield HttpServer.make(config.port, config.host, httpApp)
It’s common to write business logic and organize control flows via fs2 streams:
fs2.Stream
.eval(fetchSubscriptions(baseUri))
.evalTap(_ => log.debug("Fetched subscriptions..."))
.map(parseSubscriptions).unNone
.filter(isValid)
.parEvalMapUnordered(4)(withMoreData(baseUri))
...
ZIO
ZIO ecosystem comes with a lot of batteries. ZIO[R, E, A]
is a core data type. See usage (it’s the way to organize the code, deal with resources/scopes, and control flows).
Tooling
You can use coursier to install Scala tooling ghcup-style.
Build tools
I’ve never used anything but sbt with Scala. If it doesn’t work for you for some reason, I can’t be much of a help here.
- See
mill
- See other java build tools (like maven) or multi-language build tools (like pants or bazel)
Miscellaneous
- See Scala cli
- See Scala worksheets
- See Scastie
Libraries
Searching for functions
There’s nothing like hoogle. The dot-completion and the go-to-definition for libraries often work. But, honestly, I regularly search through the github looking for functions (and instances). I don’t know what normal people do.
Searching for libraries
- Search in the organization/project lists. Example 1 and Example 2.
- Search on scaladex.
- Search on github.
- Ask a friend.
- Just use a java library.
Managing dependency versions
There is no stackage.
- Some people use sbt-lock, sbt-dependency-lock to get lock (freeze) files.
- Some people use scala steward. A bot that helps keep dependencies up-to-date.
And you can always fall back to checking the maven repository to see what are the dependencies. An example.
Books and other resources
If you want to learn more, see docs.scala-lang.org, rockthejvm.com, and Foundations of Functional Programming in Scala.
To Haskell devs, I used to recommend underscore’s books: Essential Scala (to learn Scala) and Scala With Cats (to learn the FP side of Scala), but those are for Scala 2. The classic Functional Programming in Scala has a second edition updated for Scala 3 — it’s great if you want a deep dive into the FP side of Scala.
If you want to get weekly Scala content, see:
- Scala Times newsletter.
- This week in Scala.
If you want to chat, see scala-lang.org/community.
OOP and variance
When it comes to the OOP side of Scala, I think the most important one to get familiar with is variance. You might think you won’t need it if you don’t use inheritance to mix lists of cats and dogs. However, you’ll still see those +
and -
around and encounter related compilation errors.
More and more libraries deal with this to make your life easier, so you might not even need to worry about it…
💡 If you are familiar with functors, depending on how well you’re familiar, it might help or hurt you to apply the knowledge of variance here.
Few things you’ll need sooner or later
You might encounter different sub-typing-related type relationships. See (upper and lower) type bounds.
trait List[+A]:
def prepend[B >: A](elem: B): NonEmptyList[B] = ???
If you encounter something that looks like associated type, see abstract type members:
trait Buffer:
type T
val element: T
If you encounter *
, see varargs (for example, Array.apply())
val from = Array(0, 1, 2, 3) // in parameters
val newL = List(from*) // splices in Scala 3
val oldL = List(from: _*) // splices in Scala 2
End
Congrats, you’re one step closer to mastering Scala. Just a few hundred more to go.