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.