This content originally appeared on DEV Community and was authored by Zelenya
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 Qouta = Qouta Int deriving Num
remainingQouta :: Qouta -> Qouta -> Qouta
remainingQouta 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…
This content originally appeared on DEV Community and was authored by Zelenya