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]
There are multiple ways to translate it into Haskell; the most straightforward:
-- read from right to left
foo err = pure (Left (extractErrorMessage err))
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
And we can avoid the rest of the plumbing by using function composition:
-- read from right to left
foo = pure . Left . extractErrorMessage
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
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
💡 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
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
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
userInfo' :: Config -> UserIds -> IO UserInfo
userInfo' c (UserIds userId subscriptionId) = undefined
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)
Note: because of laziness, it might not be as trivial (a value might never be evaluated).
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’
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"
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.
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')]
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)
List.nub [1, 2, 1, 3] -- [1,2,3]
There is no contains
, there is elem
:
elem 1 [1, 2, 1, 3] // True
Which is commonly written using infix form:
1 `elem` [1, 2, 1, 3] -- True
💡
elem
is not prefixed withList
, cause it comes fromFoldable
.
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’
And it’s annoying when you need to access or update nested records.
*I’m not even going to illustrate this in vanilla Haskell.*
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 checkoutgetField
.
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
dont Admin
-- *** Exception: No match in record selector internalId
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]
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]
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
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
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)
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
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
💡 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)
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
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 calledMaybe
. - See
Exception
andSomeExceptions
(cousins ofThrowable
). - See synchronous exceptions (throwing exceptions in
IO
). - See
safe-exceptions
package (throwing exceptions in~~F[_]~~
, I meanMonadThrow m => m a
). - Maybe see
UnliftIO.Exception
as asafe-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 frombracket
and functions prefixed withwith
(e.g.,withPool
). Also, see resourcet and managed.
Runtime and concurrency
- Green threads (provided by Haskell runtime system).
-
MVar
isMVar
,Ref
isIORef
. - 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
• 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
...
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
- You can use stackage snapshots (snapshot is a set of compatible Haskell libraries).
- 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).
- 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
- https://haskellweekly.news/ (newsletter)
- https://haskell.libhunt.com/ (newsletter)
- https://haskell.pl-a.net/ (aggregator)
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
...
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
If you encounter too many forall
s or suspiciously nested forall
s, 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
...
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).