š¹Ā Hate reading articles? Check out the complementary video, which covers the same content.
There is this meme that Haskell is better than Scala or that Scala is just a gateway drug to Haskell. A long time ago, I used to believe this.
I saw how 4 different companies use Scala.
I saw how 4 different companies use Haskell
After years of using both in multiple companies and meeting people who āwent back to Scalaā, itās kind of sad to see people bring up these stereotypes repeatedly. Sure, the Haskell language influenced and inspired Scala, but there are so many things that I miss from Scala when I use Haskellā¦ as well as vice versa.
ā ļøĀ Disclaimer
When talking about Scala or Haskell here ā itās not just about the languages themselves, but also about standard libraries and overall ecosystems.
āAll happy families are alike; each production is unhappy in its own way.ā
Weāll be talking from subjective-production-backend experience ā from somebody dealing with web apps, microservices, shuffling jsons, and all that stuff. The emphasis is on the production setting (not academic, theoretical, or blog post).
For example, Scala is built on top of Java and has null
s, oh no! While in a day-to-day code, I rarely encounter it. The last time I saw a NullPointerException
was more than 8 months ago. Not even in the Scala code. It was in the http response body ā the vendorās API returned internal errors in case of malformed input š¤·Ā (they used Spring).
With this in mindā¦
FP dial
One of the biggest things that separates Scala from Haskell is the ability to choose the level of FP or level of purity.
I know how to use trace (and friends) to debug in Haskell, but itās pretty convenient to sneak in an occasional println
anywhere I want.
And Iām happy to admit, that I used a couple of mutable variables a few of months ago and it was great. I was migrating some convoluted functionality from legacy Ruby to Scala, and it was simpler to translate the core almost as is (in a non-fp way), add tests, remove unnecessary code, fix some edge cases, and only after rewrite in a functional style with a little State
.
Sure, it wouldnāt be the end of the world to rewrite that in Haskell as well ā instead of intermediate representation, I would have to plan on paper or somethingā¦
Fp-dial is also great for learning/teaching, occasional straightforward performance tweaks, and so onā¦
Laziness
Another big difference is laziness.
When writing Haskell, laziness allows us not to think or worry about stuff like stack safety; for example, we donāt have to worry about *>
vs >>
, we can look at the code of any Monad and itās going to be just two functions ā no tailRecM
or other tricksā¦ (it still doesnāt mean itās going to be easy to read though)
And laziness gives more room for the compiler to be free and optimize whatever it wants.
On the other hand, when writing Scala, itās pretty nice not to worry about laziness. Like Iāve mentioned before, I can println
(or see in the debugger) pretty much any variable and know that I will see it (and it will be evaluated). On top of that, no worrying about accumulating the thunksā¦
šĀ Donāt worry, there are other ways to leak memory on JVM.
Function Composition and Currying
Probably the biggest stylistic thing I miss from Haskell is function composition.
Starting with a concise composition operator (.
):
pure . Left . extractErrorMessage -- Note: read from right to left
Sure, it requires getting used to, some people abuse it, and so on. But function composition can be so elegant!
map (UserId . strip) optionalRawString
What also helps Haskellās elegance is currying ā Haskell functions are curried, which makes function composition and reuse even more seamless:
traverse (enrichUserInfo paymentInfo . extractUser) listOfEntities
enrichUserInfo :: PaymentInfo -> User -> IO UserInfo
extractUser :: Entry -> User
At the same time, not having currying by default is to Scalaās advantage ā it can notably improve error messages (which is also more beginner-friendly). When you miss an argument, the compiler tells you if you passed a wrong number of parameters
or which exact parameter is wrong:
def enrichUserInfo(paymentInfo: PaymentInfo, user: User): IO[UserInfo] = ???
enrichUserInfo(user)
// Found: User
// Required: PaymentInfo
enrichUserInfo(paymentInfo)
// missing argument for parameter user ...
Where clause
Another style- or formatting-related thing that I really miss in Scala is having the ability to write things in the where
clauses.
foo = do
putStrLn "Some other logic"
traverse (enrichUserInfo paymentInfo . extractUser) listOfEntities
where
enrichUserInfo :: PaymentInfo -> User -> IO UserInfo
enrichUserInfo = undefined
extractUser :: Entry -> User
extractUser = undefined
Itās not the same as declaring variables (before using them) and not the same as using private or nested functions. I like to have primary logic first and secondary ā after (below) and be explicit that functions arenāt used anywhere else.
Types
Letās talk types.
Newtypes and Sum types
It feels like Haskell encourages us to make custom types, because of how uncluttered it is:
data Role = User | Admin deriving (Eq, Show)
newtype Quota = Quota Int deriving Num
remainingQuota :: Quota -> Quota -> Quota
remainingQuota balance cost = balance - cost
Itās just so neat and ergonomic! When Iām writing Scala, I might think about making a custom type but then give up and keep using String
s and Boolean
sā¦
šĀ Sure. One can use a library. Sure. Itās better with Scala 3. Stillā¦
Product Types
Funnily enough, Scala is way better at product types (records/case classes):
case class User(name: String)
User("Peach").name
We donāt need to go into more details. If you have used Haskell, you know.
šĀ Sure. One can use lenses. Sure. Itās better with the latest extensions. Stillā¦
Union Types
On a related note, Scala 3 introduced union types:
val customer: Either[NotFound | MissingScope | DBisDead, CustomerId] = ???
customer match
case Right(customerId) => as200(customerId)
case Left(NotFound(message)) => notFound(message)
case Left(MissingScope(_)) => unauthorized
case Left(DBisDead(internal)) =>
Logger.error("Some useful information, {}", internal.getErrorMessage()) >>
internalServerError("Some nice message")
Finally, introducing new error types doesnāt feel like a chore ā we donāt need to build hierarchies or convert between different ones. I miss those in Haskell and Scala 2.
š¤Ā The type could be even
CustomerId | NotFound | MissingScope | DBisDead
Type inference
Letās keep it short: Haskell has great type inference. It works when you need it ā I never feel like I have to help the compiler to do its job
šĀ Not talking about more complicated type-level stuff ā just normal fp code.
For example, we can compose monad transformers without annotating a single one (or even the last one):
program :: IO (Either Error Result)
program = runExceptT do
user <- ExceptT $ fetchUser userId
subscription <- liftIO $ findSubscription user
pure $ Result{user, subscription}
fetchUser :: UserId -> IO (Either Error User)
findSubscription :: User -> IO Subscription
And when we use the wrong thing, the compiler has our back:
program = runExceptT do
user <- _ $ fetchUser userId
subscription <- liftIO $ findSubscription user
pure $ Result{user, subscription}
ā¢ Found hole: _ :: IO (Either Error User) -> ExceptT Error IO User
ā¢ ...
Valid hole fits include
ExceptT :: forall e (m :: * -> *) a. m (Either e a) -> ExceptT e m a
with ExceptT @Error @IO @User
(imported from āControl.Monad.Exceptā ...
(and originally defined in transformers
Modules and dot completion
On the other side of the coin, Scala has a great module system ā we can design composable programs, donāt worry about things like naming conflicts, and alsoā¦ look what we can do:
dot completionā¦
Hoogle
To be fair, the dot-completion is good and all, and occasionally I miss it in Haskell. Itās, however, only useful when I already have a specific object or already know where to look. When we just start using the library, have a generic problem, or donāt even know what library to use yet; then the dot-completion wonāt help us ā but Haskellās hoogle is.
We can search for generic things:
(a -> Bool) -> [a] -> [a]
And for more specific things, for example, we have an ExceptT
, how can we use it?
IO (Either e a) -> ExceptT e IO a
Libraries
If we look at the bigger picture, Scala has a better library situation ā when I need to pick a library to solve some things, itās usually easier to do in Scala.
šĀ Keep in mind the context. I know, for instance, Scala has nothing that comes close to Haskellās parser libraries, but this is not what weāre talking about right now.
It's most notable in companies where many other teams use different stacks; we have to keep up with them (new serialization formats, new monitoring systems, new aws services, and so on).
We rarely have to start from scratch in Scala because, at least, we can access the sea of java libraries.
The opposite issue ā when there are too many libraries for the same use-case ā is just a bit less common in Scala. Mostly, when there are multiple libraries, itās because each exists for a different Scala flavor (weāll talk about this soon), but itās often fine because itās easy to pick one based on your style (maybe not as easy for beginners š¤·)
And then Scala libraries themselves are usually more production-ready and polished. Essentially, there are more Scala developers and more Scala in production, so the libraries go through more iterations and testing.
Library versions / Stackage
However, when it comes to picking versions of the libraries I prefer Haskell because it has Stackage ā a community project, which maintains sets or snapshots of compatible Haskell libraries.
We donāt need to brute-force which library versions are compatible or go through bunch of github readmes or release notes. The tools can pick the versions for us: either explicitly, if we choose a specific resolver/snapshot (for example, lts-22.25); or implicitly, by using loose version bounds (base >= 4.7 && < 5
) and relying on the fact that Stackage incentivizes libraries to stay up-to-date and be compatible with others (or something like that).
Best practices
As I mentioned, there are various flavors of Scala (some say different stacks): java-like Scala, python-like Scala, actor-based Scala, ā¦ many others, and two fp Scalas: typelevel/cats-based and zio-based. Most of the time, they come with their core set of libraries and best practices.
Itās easy to get onboarded at a new code base or a company ā no need to bike-shade every time about basic things like resource management or error handling. Of course, there are hype cycles and new whistles every few years, but Scala communities usually settle on a few things and move on.
On the other hand, there is no consensus on writing Haskell. Whatsoever. On any topic. And Iām going to contradict what Iāve just said, but I like it too ā it can be really fun and rewarding as well. I have seen 4 production usages of Haskell: each company used a different effect system or ways to structure programs (actually, half of them used even multiple different ones inside the same company), and it was enjoyable to learn, experiment, and compare.
Abstractions
In a nutshell, all those (Scala/Haskell) effect systems are just monads with different boilerplate ā if you used one, you used them all. Itās not a big deal to switch between them.
And itās another great thing about Haskell ā the use or reuse of abstractions and type classes.
Itās typical for libraries to provide instances for common type classes. For example, if there is something to ācombineā, there are probably semigroup and/or monoid instances. So, when Haskell developers pick up a new library, they already have some intuition on how to use it even without much documentation (maybe not as easy for beginners š¤·).
Take, for instance, the Megaparsec parser library ā most of the combinators are based on type classes; for example, we can use applicativeās pure to make a parser thatĀ succeedsĀ without consuming input, alternativeās <|>
that implements choice, and so on.
Blitz round
Letās quickly cover a few other topics. We wonāt give them too much time, because they are even more nuanced or niched (or I was too lazy to come up with good examples).
Documentation, books, and other resources
Speaking of documentation, originally, when I sketched out this guide, I was going to say that Scala is better at teaching (documentation, books, courses, and whatever), but after sleeping on it (more than a couple nights), I donāt think itās the case ā I donāt think one is doing strictly better than the other on this front (as of 2024).
Type classes
Probably the first and the most common topic people bring up when comparing Scala to Haskell is type classes: in Haskell, there's (guaranteed to be) one instance of a type class per type (Scala allows multiple implicit instances of a type).
There are a lot of good properties as a consequence, but honestly, the best one is that there is no need to remember what to import to get instances.
Type-level programming
If you like it when your language allows you to do āa lotā of type-level programming, itās an extra point for Haskell.
If you donāt like it when your colleagues spend too much time playing on the type-level or donāt like complex error messages, itās an extra point for Scala.
Build times
Scala compiles faster.
Runtime and concurrency
I think, in theory, Haskell has a strong position here: green thread, STM, and other great concurrency primitives.
However, in practice, I prefer writing concurrent code in Scala. Maybe itās because Iām scared of Haskellās interruptions and async exceptions, maybe itās because occasionally I can just replace map
s with āpragmaticā parMap
, mapAsync
, or even parTraverse
and call it a day, or maybe itās because Scala library authors, among other things, built on top of Haskellās findings.
Take-aways
So, is there a lesson here? On one hand, I wish people would stop dumping on other languages and recite the same things.
On the other hand, I, for instance, hate Ruby so much that if someone told me to learn something from Ruby, Iād tell them toā¦