Haskell for OOP Developers

Tutorial · ~15 min read

Haskell, for people who already know how to program

You came up through C++, Java, Scala, and Python. You know interfaces, abstract classes, constructors, and generics in your bones. The bad news? Haskell's evangelists love to make it sound alien. The good news? ~90% of it is a vocabulary shift. Let's translate, not re-learn.

90%

is the same patterns you know, written with different syntax and new names.

1 idea

actually changed: Haskell splits data and behavior into separate buckets.

10%

is genuinely new: compiler-enforced purity and lock-free concurrency.

The one mental model that fixes the "brain fog"

OOP bundles data + behavior into one box (the class). Haskell keeps three things in separate boxes:

Box 1 · the fields

data

"What does this thing hold?" — your struct/fields.

Box 2 · the contract

class

"What can be done?" — the interface (called a typeclass).

Box 3 · the glue

instance

"This data satisfies that contract" — wired up separately.

Read any Haskell file as: "Here are my fields, here is my interface, and here are standalone functions processing them." That's the whole trick.

01

The "class" and the "constructor"

In Python a class declares the fields and the constructor (__init__) together. In Haskell you write a data type. The thing on the left of = is the type name; the thing on the right is a plain constructor function that takes arguments and hands you back a value.

Myth-buster: there are no objects to "construct." A constructor is just a function whose name happens to start with a capital letter.
Wait — why is field access "pattern matching" instead of user.name?
Because the data and the behavior are separate. There's no method living on the value to call .name on. Instead you write a standalone function that takes a User apart. The (MakeUser name _) on the left says "if you hand me something built with MakeUser, bind its first field to name and ignore the rest (_)." It's destructuring — like name, age = some_tuple in Python, but baked into the function signature.
02

Records — the part that looks like Scala case classes

Writing positional pattern matches gets old. Haskell's record syntax names the fields and auto-generates the getter functions for you. If you've used Scala case classes or Python @dataclass, this is the same convenience.

Note the flip: in Python user.name reads "value, then field." In Haskell name user reads "function applied to value." Same getter, mirrored word order.
03

Interfaces & abstract classes → typeclasses

You cringed at "Haskell doesn't have interfaces, it has typeclasses." You were right to. Structurally, a typeclass is an interface. The only real difference is where the wiring happens:

  • In Java/Python the data type inherits the interface — the contract is welded onto the class.
  • In Haskell you declare the interface (class), declare the data (data), then write a third block of glue (instance) that connects them.

The "a" is the implementer

In class JSONSerializable a where, the a is a placeholder for "whatever type implements this." It's literally the self type, written as a generic parameter.

Why separate the glue?

Because you can make a type you don't own implement an interface you don't own. That's like adding a Java interface to java.lang.String after the fact — impossible there, routine in Haskell.

Vocabulary landmine: Haskell's keyword class does not mean an OOP class. It means interface. Read class as "interface" every single time and the cringe disappears.
04

Generics — List<T> is just [a]

The angle brackets you know from Java/Scala (Box<T>) become lowercase type variables in Haskell. A "bounded generic" (<T extends Comparable>) becomes a typeclass constraint written with =>.

Translation key: lowercase a, b, c = type parameters (your T, U). The fat arrow => = "given these interface constraints, here's the real signature." Everything left of => is a bound; everything right of it is the function type.
05

Collections & transformations

Python reaches for comprehensions or map/filter with lambdas. Haskell passes functions natively — and offers a neat trick called partial application: (* 2) is "the function that multiplies by 2."

No loops, no mutation, no index variables — you describe the transformation, not the stepping. Same mental model as Python comprehensions or Java Streams / Scala collections, minus the ceremony.
06

Error handling — goodbye None, hello Maybe

Python returns None and trusts you to check it (you won't, and you'll ship an AttributeError). Haskell's Maybe type is Just value or Nothing — and the compiler refuses to build until you've handled the empty case. It's Scala's Option / Java's Optional, but mandatory.

And when you need an error message, not just "nothing"?
Use Either: Left error for the failure path, Right value for success. It's the typed, exhaustively-checked version of throwing an exception — the failure type is right there in the signature, so callers can't pretend it won't happen. Think Scala Either / Rust Result.
10%

The part that's genuinely new

You asked whether there's anything truly more advanced. Yes — two things that C++, Java, and Python can't cleanly do. This is where the academics earn their pedantry.

1 · Purity enforced by the compiler

In Python any function can secretly write a file, mutate a global, or print. In Haskell, if a signature says Int -> Int, it is mathematically impossible for it to do anything but compute an Int from an Int.

To touch the outside world, a function must advertise it by returning an IO type. Side effects become explicit, visible architecture — not hidden landmines.

2 · Software Transactional Memory (STM)

Haskell runs concurrent threads with no locks, mutexes, or race conditions by treating memory updates like database transactions: they commit atomically, or roll back and retry automatically.

Python's GIL and Java's synchronized blocks feel ancient by comparison. This is the one place where programming genuinely progressed.

Both fall out of the same root cause: because data and behavior are separated and effects are tracked in the type system, the compiler knows exactly which code is "dangerous" and can reason about it.

The Rosetta Stone — quick reference

Filter the table to find the OOP term you're hunting for.

OOP / Python concept Haskell equivalent
Class (fields)data T = ...
ConstructorMakeT a b
Field getterrecord syntax { f :: T }
Interface / abstract classclass C a where
implements / extendsinstance C T where
Method callfunc value
Generic <T>lowercase a
Bounded generic <T extends C>(C a) => ...
map / filter / reducemap / filter / foldr
None / nullMaybe (Just / Nothing)
Exception / typed errorEither (Left / Right)
Side effects (I/O, print)IO a
enum / sealed typesdata T = A | B | C

Self-check: did the fog clear?

Five quick questions. Pick an answer to see whether the mapping stuck.