DEV Community

Cover image for Haskell for Scala developers
Zelenya
Zelenya

Posted on

Haskell for Scala developers

Imagine you’re my colleague: you have functional Scala experience and must start contributing to the Haskell code. These are the things I would share with you (over some period of time).

⚠️ Context: subjective-production-backend experience, web apps, microservices, shuffling jsons, and all that stuff.

We will cover the main differences, similarities, major gotchas, and most useful resources. Consider this a rough map — just looking at this won’t make you a professional Haskell developer, but it should certainly save you (and the rest of the team) plenty of time.

⚠️ I’ll use Haskell and GHC interchangeably, which is “technically” incorrect. But it’s 2024. Who cares.


📹 Hate reading articles? Check out the complementary video, which covers the same content.

Basic syntax and attitude

You can (and should) pick up syntax by yourself, but here are the things you should conquer first:

Driving on the wrong side of the road

You have to get used to reading chains of operations “in the opposite direction”. Take this Scala code:

def foo(err: Error) = err.extractErrorMessage().asLeft.pure[F]
Enter fullscreen mode Exit fullscreen mode

There are multiple ways to translate it into Haskell; the most straightforward:

-- read from right to left
foo err = pure (Left (extractErrorMessage err))
Enter fullscreen mode Exit fullscreen mode

Notice the order of functions and the way we call functions (e.g., no parentheses after extractErrorMessage). We can avoid even more parentheses by the using function application operator (f $ x is the same as f x):

-- read from right to left
foo err = pure $ Left $ extractErrorMessage err
Enter fullscreen mode Exit fullscreen mode

And we can avoid the rest of the plumbing by using function composition:

-- read from right to left
foo = pure . Left . extractErrorMessage
Enter fullscreen mode Exit fullscreen mode

In the wild, you will see all of these styles as well as their mix.

Function signatures

Did you notice that Haskell’s functions had no type signatures?

💡 Haskell is pretty good at inferring types. We can omit type signatures even in the top-level definitions. We can, but it doesn’t mean that we should!

foo :: Error -> IO (Either Text a)
foo err = pure $ Left $ extractErrorMessage err
Enter fullscreen mode Exit fullscreen mode

The type signature goes above the function definition. It might feel awkward to connect a name to a type, but among other things, it allows you to focus on types (while ignoring the names).

foo :: User -> Password -> Token -> IO (Either Error Credentials)
foo user password token = undefined
Enter fullscreen mode Exit fullscreen mode

💡 The undefined value is like a type-inference-friendlier cousin of ???.

Names don’t matter?

The last thing I want to cover before we move on to more interesting things is about common function names and operators.

For reasons, Haskell has a map function that only works on lists. Functor has fmap.

There’s no flatMap, there is >>= (bind operator), and in general Haskell codebases (and developers) are usually more operator tolerable (some are too tolerable):

fetchUser someUserId >>= \case 
  Right user -> withDiscount <$> findSubscription user.subscriptionId
  Left _ -> pure defaultSubscription
Enter fullscreen mode Exit fullscreen mode

traverse is still traverse, and everything else you can pick up as you go.

Basic concepts and fp

Function composition and Currying

As I’ve mentioned in the first Haskell vs. Scala video, in Haskell, function composition and currying are first-class citizens. Both are powerful enough to make the code tidier and more elegant, as well as the opposite. You certainly have to practice to get better at writing and reading.

If you find some code confusing (or yourself wrote some code that the compiler doesn’t accept), try rewriting it: make it more verbose, introduce intermediate variables, and so on. It’s ok. It’s not a one-liner competition.

fetchUser someUserId >>= subscription
  where 
    subscription (Right user) = userSubscription user
    subscription (Left _ )    = pure defaultSubscription

    userSubscription :: User -> IO Subscription
    userSubscription user = withDiscount <$> findSubscription user.subscriptionId
Enter fullscreen mode Exit fullscreen mode

Also, if it feels like number of arguments is getting out of control, try introducing records.

userInfo :: UserId -> SubscriptionId -> BundleServiceUrl -> Cache -> IO UserInfo
userInfo u s b c  = undefined 
Enter fullscreen mode Exit fullscreen mode
userInfo' :: Config -> UserIds -> IO UserInfo
userInfo' c (UserIds userId subscriptionId) = undefined
Enter fullscreen mode Exit fullscreen mode

Purity

When writing Haskell, you can not just use a quick var or a temporary prinltn. Depending on your experience and workflows, it can be or not be a big deal.

You might need to get used to it. You can start by getting familiar with Debug.Trace:

userSubscription user = 
  withDiscount <$> findSubscription (traceShowId user.subscriptionId)
Enter fullscreen mode Exit fullscreen mode

Note: because of laziness, it might not be as trivial (a value might never be evaluated).

💡 See Debugging without a “real” debugger.

Laziness

Otherwise, I think you shouldn’t worry about laziness at the beginning of your journey. Trust the compiler. If you’re too worried about performance and resources, keep an eye on the metrics.

When needed, use existing code as a reference; for example, if you see exclamation points (BangPatterns) or a StrictData extension where the data is defined, copy-paste first and ask questions later!

Two great series on laziness in Haskell (you can find even more online):

Modules and Imports

There are no classes and objects.

You might quickly run into a problem with naming conflicts:

import Data.Text -- (Text, strip)
import Data.List 

foo :: Text
foo = filter (/= 'a') "abc"
--    ^^^^^^
-- Ambiguous occurrence ‘filter’
-- It could refer to
--   either ‘Data.List.filter’
--       or ‘Data.Text.filter’
Enter fullscreen mode Exit fullscreen mode

You have to get into a habit of using qualified imports:

import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.List as L -- stylistic choice

foo :: Text
foo = Text.filter (/= 'a') "abc"
Enter fullscreen mode Exit fullscreen mode

Some libraries suggest the users to qualify their imports, some people prefer to qualify all their imports, but, at the end of the day, it’s a personal/team choice.

📹 bytestrings

import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
-- import qualified Data.HashMap.Strict as HM

foo :: HashMap Int Char
foo = HashMap.fromList [(1, 'a'), (2, 'b')]
--         HM.fromList [(1, 'a'), (2, 'b')]
Enter fullscreen mode Exit fullscreen mode

I’d say, start using qualified imports for collections (containers) and strings (texts, bytestrings), and see how it goes.

Standard library

💡 Standard library (std module) is called Prelude. It’s imported by default into all Haskell modules unless explicitly imported or disabled by the NoImplicitPrelude extension.

You should pay attention when using Haskell’s standard library, (at least) for two reasons.

It provides things that you shouldn’t use in production; for example, head (that crashes a program on an empty list), String (see text / bytestring), and lazy IO operations (see streaming libraries).

💡 Also see the OverloadedStrings extension.

It doesn’t provide things you might expect or provides them in unexpected ways. For example, there is no distinct to ignore duplicate elements from a list, but there is nub:

List(1, 2, 1, 3).distinct // List(1,2,3)
Enter fullscreen mode Exit fullscreen mode
List.nub [1, 2, 1, 3] -- [1,2,3]
Enter fullscreen mode Exit fullscreen mode

There is no contains, there is elem:

elem 1 [1, 2, 1, 3] // True
Enter fullscreen mode Exit fullscreen mode

Which is commonly written using infix form:

1 `elem` [1, 2, 1, 3] -- True
Enter fullscreen mode Exit fullscreen mode

💡 elem is not prefixed with List, cause it comes from Foldable.

And stuff like that. Be more open-minded (and careful).

🥈 Some companies use alternative preludes (existing or custom) not to deal with the standard prelude.

Types

Product Types

No easy way to put this. Records in Haskell can be irritating. You’ll find out really fast.

It’s annoying when you use records with the same field names:

data User = User {name :: Text, subscriptionId :: SubscriptionId}
data UserIds = UserIds {userId :: UserId, subscriptionId :: SubscriptionId}
--                                        ^^^^^^^^^^^^^^
-- Multiple declarations of ‘subscriptionId’
Enter fullscreen mode Exit fullscreen mode

And it’s annoying when you need to access or update nested records.

*I’m not even going to illustrate this in vanilla Haskell.*
Enter fullscreen mode Exit fullscreen mode

I wouldn’t recommend using vanilla records. You have to get familiar with a couple of extensions. You can start with OverloadedRecordDot (since: GHC 9.2.0) and DuplicateRecordFields. Additionally: NoFieldSelectors (incl. in GHC2021), NamedFieldPuns (incl. in GHC2021), and maybe…maybe RecordWildCards. Don’t expect a smooth ride.

💡 We cover GHC2021 in the extension section.

It’s also common to use lenses via some optic library. There are quite a few options. Pick you poison: lens, optics, generic-lens, lens-simple, microlens, profunctor-optics, prolens.

💡  If you can’t afford GHC 9.2.0 (or higher) or lenses, you can also checkout getField.

Sum types

It can be a bit unexpected, but Haskell allows partial field accessors (with a warning).

data Role = Role {internalId :: UserId} | Admin

dont :: Role -> IO ()
dont user = print user.internalId 
Enter fullscreen mode Exit fullscreen mode
dont Admin
-- *** Exception: No match in record selector internalId
Enter fullscreen mode Exit fullscreen mode

I’m mentioning this just in case. I’ve never cared or worried about this. Just use pattern matching like you would in Scala.

Polymorphism

What in the Java world is called generics, in the Haskell world is called parametric polymorphism.

-- a is a type parameter
filter :: (a -> Bool) -> [a] -> [a]
Enter fullscreen mode Exit fullscreen mode

Don’t want to go into details here, you shouldn’t see this that often, eventually, see Explicit universal quantification (forall).

-- this version is explicitly quantified
filter :: forall a . (a -> Bool) -> [a] -> [a]
Enter fullscreen mode Exit fullscreen mode

Other than that, in general practice, it’s not that different, so let’s talk about ad-hoc polymorphism.

Type classes

Unlike Scala, Haskell has built-in type classes — there's (guaranteed to be) at most one instance of a type class per type.

Good news: No need to worry about imports!

There are other things you’ll have to worry about at some point, but don’t worry about them right now. You can start by declaring the type class instances next to the type class (in the same module) or next to the data.

Sometimes, when you need a new (custom) instance, you can introduce a newtype:

-- User is declared in some other module

newtype InvalidUser = InvalidUser User 

instance Arbitrary InvalidUser where
  arbitrary = do
     Positive x <- arbitrary
     pure $ InvalidUser $ User undefined x undefined
Enter fullscreen mode Exit fullscreen mode

Sometimes, you can add an orphan instance and not use a newtype (it’s not encouraged, but still legal and sometimes unavoidable):

{-# OPTIONS_GHC -fno-warn-orphans #-}

instance Arbitrary User where
Enter fullscreen mode Exit fullscreen mode

And remember that you can get a lot for free with deriving.

Deriving

In Haskell, you can derive a lot (starting with Show, all the way to Traversable and beyond).

{-# LANGUAGE GeneralizedNewtypeDeriving #-}

newtype MyApp a = MyApp { unApp :: ReaderT Config IO a }
  deriving (Show, Functor, Applicative, Monad, MonadReader Config)
Enter fullscreen mode Exit fullscreen mode

Usually, a library will have some examples of how to derive required type class instances, including imports and extensions. And if it doesn’t include the extensions (or you copied a code from somewhere else), the compiler will give you a hint.

For example, aeson:

{-# LANGUAGE DeriveGeneric #-}

import Data.Aeson (FromJSON, ToJSON)
import GHC.Generics (Generic)

data Product = Product { name :: Text, tier :: Int } deriving (Generic, Show)

instance ToJSON Product
instance FromJSON Product
Enter fullscreen mode Exit fullscreen mode

To expand your vocabulary, start with GeneralizedNewtypeDeriving(incl. in GHC2021). Then checkout strategies, DerivingStrategies (incl. in GHC2021), DerivingVia, and maybe DeriveAnyClass.

-- DerivingStrategies is implied by DerivingVia
-- {-# LANGUAGE DerivingStrategies #-}

{-# LANGUAGE DerivingVia #-}

import Data.Aeson (FromJSON, ToJSON)

newtype Quota = Quota {getQuota :: Int}
  deriving (Eq, Ord)
  deriving (Show, ToJSON, FromJSON) via Int
Enter fullscreen mode Exit fullscreen mode

💡 The GHC docs are good. There are a couple of other guides online.

Meta Programming

The two most common ways to generate boilerplate in Haskell are Template Haskell (TH) and generic programming. Not java generics. Even more generic generics!

💡 That’s one of the reasons not to refer to parametric polymorphism as just (java) generic types.

Template Haskell

Let’s start with Template Haskell, cause it’s easier to explain (as an overview, I would claim that about the actual explanation).

You use the TemplateHaskell language extension and write template haskell or you use code that generates boilerplate via TH.

Libraries often provide a specific TH module (or even a package) with functions to derive necessary instances. For example, Data.Aeson.TH:

{-# LANGUAGE TemplateHaskell #-}

import Data.Aeson.TH (defaultOptions, deriveJSON) 

data Product = Product { name :: Text, tier :: Int } deriving (Show)

$(deriveJSON defaultOptions ''Product)
Enter fullscreen mode Exit fullscreen mode

Generic programming

Generic programming comes in different shapes and forms. I’d say the most relevant is GHC.Generics with the DeriveGeneric extension (incl. in GHC2021), because it’s the one that libraries usually use. Once again, let’s look at aeson:

{-# LANGUAGE DeriveGeneric #-}

import Data.Aeson (FromJSON, ToJSON)
import GHC.Generics (Generic)

data Product = Product { name :: Text, tier :: Int } 
  deriving (Generic, Show)

instance ToJSON Product
instance FromJSON Product
Enter fullscreen mode Exit fullscreen mode

There is also generics-sop, which is seemingly more accessible than GHC.Generics if you want to generate your own boilerplate (maybe in the future; probably, if you’re watching this, you should not be writing generic code like that).

There is also generic programming via Data.Typeable and Data.Data (see Scrap Your Boilerplate). It came before GHC.Generics, uses a different approach, and feels more straightforward. However, it’s usually not recommended any more because it’s less efficient. That’s what they say on the internets. I’ve never compared any of those.


🌯 So, Template Haskell is like macros, generics are like shapeless.


Best practices

As I’ve mentioned in the overview, there is no consensus on writing Haskell. Whatsoever. On any topic.

Abstractions and type classes

However, it’s essential to know your type classes. Those are a must: Functor, Applicative, Monad, Semigroup, Monoid, Foldable, Traversable, and Alternative. If the library has a functionality that can be provided by one of those (e.g., smooshing or mapping things), it’s likely that it’s going to be provided and won’t be very documented.

Failure handling

The situation is not much better than in Scala. I can’t help you here, you’re on your own.

  • Option is called Maybe.
  • See Exception and SomeExceptions (cousins of Throwable).
  • See synchronous exceptions (throwing exceptions in IO).
  • See safe-exceptions package (throwing exceptions in ~~F[_]~~, I mean MonadThrow m => m a).
  • Maybe see UnliftIO.Exception as a safe-exceptions surrogate.
  • See asynchronous exceptions.

You might be tempted to use error "this should never happen" in “pure” code — just a friendly reminder it eventually does happen. Don’t be lazy, don’t trust other teams to respect your contracts, and be cautious. Think about your future self.

Styles and flavors

From time to time, people bring up Simple Haskell, Boring Haskell, and stuff like that. But there are no actual rules, communities, or many guidelines for those.

The one way that Haskell (GHC) varies between companies and projects is via the language extensions. You need to start recognising extensions and what they change: notice which extensions are enabled per module and which for the whole project (or package). Also get familiar with language editions — such as GHC2021 and GHC2024 — how to enable those and what’s included.

Organizing code

There is plain IO, mtl and transformers, custom monads (on top of those), IO + Reader(T), RIO, unliftio, services and handle patterns, free monads, freer-simple, extensible-effects, fused-effects, eff, effectful, cleff, polysemy, bluefin, and other effect libraries (that I forgot to mention).

I could oversimplify and say, if you used cats-effect and tagless final use THIS, or if you used ZIO use THAT, but what’s the fun in that?

💡 A quick note: if you’re looking for something like Resource (for your library of choice) and can’t find it or make it work, try starting or searching from bracket and functions prefixed with with (e.g., withPool). Also, see resourcet and managed.

Runtime and concurrency

  • Green threads (provided by Haskell runtime system).
  • MVar is MVar, Ref is IORef.
  • See async package.
  • See STM.
  • See bracket.

📕 Bonus: See Parallel and Concurrent Programming in Haskell by Simon Marlow.

Tooling

📹 Okay, okay, let’s go back to stuff I can actually be a bit of help.

There’s no universal answer when it comes to tooling. If you’re using nix, do what you're doing. If not — install ghcup and install everything else through it (it’s kind of like cs setup).

There are two main build tools in Haskell: Cabal and Stack.

  • If the team or project that you’re using uses one — choose that one.
  • If you’re starting a new project for yourself — you can choose by throwing a coin (or partially depending on the preferred dependency workflow; see below).

Editor

You can use ghcup to install HLS (Haskell Language Server) – Haskell LSP support, so you can use whatever supports LSP.

I use vs code. Using Haskell with it is not that different from using Scala. You can also use IntelliJ via the LSP plugin — obviously don’t expect java-level IDE support.

REPL / GHCI

Try integrating ghci into your development workflow. It’s more than just a repl — it’s closer to Scala worksheets with abilities to debug and inspect; for example, get a type or a kind of expression and get available (type-class) instances for a data type.

🤷 In other words, if there is some Scala/IntelliJ functionality/workflow that is missing in the Haskell editor, you might substitute it by using GHCi.

Libraries

Searching for functions

If you’re looking for some function or some data type, you can still use dot completion on a module, but if you don’t know where to look, try hoogle (or local alternative).

You can also use typed holes, to get compiler suggestions (it also works in the editor).

foo = _ someUserId >>= subscription
Enter fullscreen mode Exit fullscreen mode
• Found hole: _ :: UserId -> IO (Either a0 User)
  Where: ‘a0’ is an ambiguous type variable
• In the first argument of ‘(>>=)’, namely ‘_ someUserId’
  In the expression ...
• Relevant bindings include
    ...
  Valid hole fits include fetchUser
  Valid refinement hole fits include
    ...
Enter fullscreen mode Exit fullscreen mode

Searching for libraries

If you’re looking for a library (a package), you can still poke around hoogle or search hackage (central package archive) by tags.

But here is the first catch: there could be too many options. For example, see my video on Haskell and Postgres. I don’t have a rule of thumb here. You can try asking around, but the more people you ask — the more answers you get.

🤔 Maybe the solution is to ask only one person or two, not more.

The opposite is also common, sometimes there are no options. Sometimes there are bindings on top of a C library. For example. So, if you can’t find anything, there is always C FFI.

Searching for dependency versions

You don’t have to search for (or use) specific library versions compatible with other libraries.

There are a few other ways to deal with dependency versions.

Managing dependency versions

  1. You can use stackage snapshots (snapshot is a set of compatible Haskell libraries).
  2. You can use loose version bounds or no bounds at all for your dependencies (if you don’t care about reproducibility or like living on the edge).
  3. You can use cabal freeze to pin down the dependencies, which ensures (more) reproducible builds. Something like sbt-lock, sbt-dependency-lock, or locks in other language ecosystems.

Books and other resources

If you want to learn more, haskell.org has a list of all sorts of learning resources.

If you want to get weekly Haskell content, see

If you want to chat, see https://discourse.haskell.org/. Or find your own echo chamber.

Few things you’ll need sooner or later

The last thing we cover. If you aren’t proactive enough with catching up with less-beginner Haskell, you might bump into unknown syntax. A few more pointers:

If you encounter @ followed by a type, see TypeApplications.

If you encounter | in a type class declaration, see functional dependencies.

class (Monad m) => MonadState s m | m -> s where
  ...
Enter fullscreen mode Exit fullscreen mode

If you encounter type family, data family, or an associated type (or data), see type families.

-- something like this
type family IsChar (a :: Type) :: Bool where
  ...

class Foo a where
  -- or something like this
  type Bar a :: Type
  f :: a -> Bar a
Enter fullscreen mode Exit fullscreen mode

If you encounter too many foralls or suspiciously nested foralls, see RankNTypes (or maybe ExistentialQuantification).

If you feel like there is too much happening on the type level, see DataKinds, Type-Level Literals, or something along those lines.

If you see where in a data declaration, see GADTs (or GADTSyntax)

data Option a where
  ...
Enter fullscreen mode Exit fullscreen mode

End

Congrats, you’re one step closer to mastering Haskell. Just a few hundred more to go.

🌱  Hopefully this is useful as a standalone resource (even if you can’t ask follow-up questions).


Top comments (0)