What Scala does better than Haskell and vice versa
Tags: functional, haskell, scala
Published: 2024-06-25 04:04:00


📹 Hate reading articles? Check out [the complementary video](https://youtu.be/K76vrOzU53o), 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 (`.`):

```haskell
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!

```haskell
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:

```haskell
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:

```scala
def enrichUserInfo(paymentInfo: PaymentInfo, user: User): IO[UserInfo] = ???
```

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

```scala
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.

```haskell
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:

```haskell
data Role = User | Admin deriving (Eq, Show)
```

```haskell
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):

```scala
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:

```scala
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):

```haskell
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:

```haskell
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sdde0l2epi9omhi3c8mw.png)

*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](https://hoogle.haskell.org/?hoogle=(a%20-%3E%20Bool)%20-%3E%20%5Ba%5D%20-%3E%20%5Ba%5D) is.

We can search for generic things:

```haskell
(a -> Bool) -> [a] -> [a]
```

![Hoogle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nvnv2iodlbkkdp3fr3ik.png)

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

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

![Hoogle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/37k3wyh58dc6l37tb45g.png)


## 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](https://www.stackage.org/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](https://hackage.haskell.org/package/megaparsec-9.6.1/docs/Text-Megaparsec.html) 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](https://hackage.haskell.org/package/parser-combinators-1.3.0/docs/Control-Monad-Combinators.html#v:-60--124--62-), 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…

---

{% embed https://youtu.be/K76vrOzU53o %}