DEV Community

Cover image for What Scala does better than Haskell and vice versa
Zelenya
Zelenya

Posted on • Edited on

What Scala does better than Haskell and vice versa

šŸ“¹Ā 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 nulls, 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


Enter fullscreen mode Exit fullscreen mode

Sure, it requires getting used to, some people abuse it, and so on. But function composition can be so elegant!



map (UserId . strip) optionalRawString


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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] = ???


Enter fullscreen mode Exit fullscreen mode


enrichUserInfo(user)
// Found: User
// Required: PaymentInfo


Enter fullscreen mode Exit fullscreen mode


enrichUserInfo(paymentInfo)
// missing argument for parameter user ...


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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)


Enter fullscreen mode Exit fullscreen mode


newtype Quota = Quota Int deriving Num

remainingQuota :: Quota -> Quota -> Quota
remainingQuota balance cost = balance - cost


Enter fullscreen mode Exit fullscreen mode

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 Strings and Booleansā€¦

šŸ™Ā 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


Enter fullscreen mode Exit fullscreen mode

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")


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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}


Enter fullscreen mode Exit fullscreen mode


ā€¢ 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


Enter fullscreen mode Exit fullscreen mode

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

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]


Enter fullscreen mode Exit fullscreen mode

Hoogle

And for more specific things, for example, we have an ExceptT, how can we use it?



IO (Either e a) -> ExceptT e IO a


Enter fullscreen mode Exit fullscreen mode

Hoogle

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 maps 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ā€¦


Top comments (0)