No sections found
We couldn't find anything matching your search query. Try adjusting your keywords.
Learn Modern Haskell for Python and Javascript Developers
In 2026, JavaScript/TypeScript is everywhere, and Python drives AI. But as systems grow, developers face a wall: unhandled state mutations, unpredictable side effects, and concurrency nightmares. Enter Haskell.
Haskell gives you absolute confidence. By enforcing pure functions, immutable data, and explicit side effects at the type level, entire classes of bugs vanish. With lightweight green threads and Software Transactional Memory (STM), Haskell turns concurrency from a nightmare into a breeze. Break through the statefulness ceiling.
2. The Rosetta Stone: Types Grid
Unlike Python and Javascript (which are dynamically typed), Haskell is statically and strongly typed with advanced type inference. You rarely have to write types, but when you do, they guarantee correctness. Notice how Maybe and Either handle nulls and errors without exceptions.
| Concept | Haskell (Statically Typed) | Python 3.12+ (Type Hints) | JS (ES2026) |
|---|---|---|---|
| Integer | x :: Int x = 42 |
x: int = 42 | const x = 42; |
| Float | piVal :: Double piVal = 3.14 |
pi: float = 3.14 | const pi = 3.14; |
| String | name :: String name = "Hi" |
name: str = "Hi" | const name = "Hi"; |
| List/Array | nums :: [Int] nums = [1, 2, 3] |
nums: list[int] = [1, 2, 3] | const nums = [1, 2, 3]; |
| Dictionary | map :: Map String Int map = Map.fromList [("k", 1)] |
map: dict[str, int] = {"k": 1} | const map = new Map(); map.set("k", 1); |
| Null/None | val :: Maybe Int val = Nothing |
val: int | None = None | const val = null; |
| Exceptions | res :: Either Error Int res = Right 5 |
Exception (Runtime) | Error (Runtime) |
null or undefined. Instead, it uses the Maybe a type (Just value or Nothing). It generally eschews throwing exceptions; it returns an Either e a (Right value or Left error). You must pattern match these explicitly.
3. Guess the Number Game
Haskell Features Introduced: The IO Monad, do notation, pattern matching, and recursion (instead of loops).
import System.Random (randomRIO)
import Text.Read (readMaybe)
import System.IO (hFlush, stdout)
-- In Haskell, side effects (like printing or random numbers) MUST be marked with the IO type.
main :: IO ()
main = do
-- Generate a random number. `<-` extracts the pure value from the IO action.
secretNumber <- randomRIO (1, 100)
putStrLn "Guess the number between 1 and 100!"
-- We use recursion instead of a `while` loop
gameLoop secretNumber
-- Functions that take an Int and perform IO
gameLoop :: Int -> IO ()
gameLoop secret = do
putStr "> "
hFlush stdout -- Forces output to appear before user types
guessStr <- getLine
-- readMaybe safely attempts to parse the string into an Int, returning a `Maybe Int`
case readMaybe guessStr of
Nothing -> do
putStrLn "Please type a valid number!"
gameLoop secret -- Loop back
Just guess -> case compare guess secret of
LT -> do
putStrLn "Higher!"
gameLoop secret
GT -> do
putStrLn "Lower!"
gameLoop secret
EQ -> putStrLn "You win!"
import random
def main():
secret_number = random.randint(1, 100)
print("Guess the number between 1 and 100!")
while True:
try:
guess = int(input("> ").strip())
except ValueError:
print("Please type a valid number!")
continue
if guess < secret_number:
print("Higher!")
elif guess > secret_number:
print("Lower!")
else:
print("You win!")
break
if __name__ == "__main__":
main()
import * as readline from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';
async function main() {
const rl = readline.createInterface({ input, output });
const secret = Math.floor(Math.random() * 100) + 1;
console.log("Guess the number between 1 and 100!");
while (true) {
const guess = parseInt((await rl.question('> ')).trim(), 10);
if (isNaN(guess)) {
console.log("Please type a valid number!");
continue;
}
if (guess < secret) console.log("Higher!");
else if (guess > secret) console.log("Lower!");
else {
console.log("You win!");
break;
}
}
rl.close();
}
main();
4. Arithmetic Command Line Game
Haskell Features Introduced: Let bindings inside do blocks, pure helper functions.
import System.Random (randomRIO)
import Text.Read (readMaybe)
import System.IO (hFlush, stdout)
main :: IO ()
main = do
putStrLn "Solve the addition problems! Type 'quit' to exit."
playLoop
playLoop :: IO ()
playLoop = do
a <- randomRIO (1, 10)
b <- randomRIO (1, 10)
putStr $ "What is " ++ show a ++ " + " ++ show b ++ "? "
hFlush stdout
input <- getLine
if input == "quit"
then putStrLn "Thanks for playing!"
else do
-- `let` is used inside `do` blocks to bind pure values (no IO)
let correctAnswer = a + b
case readMaybe input of
Just ans | ans == correctAnswer -> putStrLn "Correct!"
| otherwise -> putStrLn $ "Wrong! It was " ++ show correctAnswer
Nothing -> putStrLn "Please enter a number or 'quit'."
playLoop -- Recurse to play the next round
import random
def main():
print("Solve the addition problems! Type 'quit' to exit.")
while True:
a, b = random.randint(1, 10), random.randint(1, 10)
user_input = input(f"What is {a} + {b}? ").strip()
if user_input == "quit":
print("Thanks for playing!")
break
try:
if int(user_input) == a + b:
print("Correct!")
else:
print(f"Wrong! It was {a + b}.")
except ValueError:
print("Please enter a number or 'quit'.")
if __name__ == "__main__": main()
import * as readline from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';
async function main() {
const rl = readline.createInterface({ input, output });
console.log("Solve addition! Type 'quit' to exit.");
while (true) {
const a = Math.floor(Math.random() * 10) + 1;
const b = Math.floor(Math.random() * 10) + 1;
const userInput = (await rl.question(`What is ${a} + ${b}? `)).trim();
if (userInput === 'quit') break;
const answer = parseInt(userInput, 10);
if (isNaN(answer)) console.log("Enter a number or 'quit'.");
else if (answer === a + b) console.log("Correct!");
else console.log(`Wrong! It was ${a + b}.`);
}
rl.close();
}
main();
5. State Machine & Settings
Haskell Features Introduced: Algebraic Data Types (ADTs), Record Syntax, and state passing.
import System.Random (randomRIO)
import Text.Read (readMaybe)
import System.IO (hFlush, stdout)
-- Algebraic Data Types form the backbone of safe State Machines
data Operation = Add | Multiply deriving (Show, Eq)
data AppState = Menu | Playing | Quit deriving (Show, Eq)
-- Record syntax allows us to name fields in a complex structure
data Settings = Settings {
minVal :: Int,
maxVal :: Int,
op :: Operation
} deriving (Show)
defaultSettings :: Settings
defaultSettings = Settings { minVal = 1, maxVal = 10, op = Add }
main :: IO ()
main = runMachine Menu defaultSettings
-- We pass the State and Settings explicitly through recursion. No global mutable variables!
runMachine :: AppState -> Settings -> IO ()
runMachine Quit _ = return ()
runMachine Menu settings = do
putStr "1. Play 2. Set Multiply 3. Quit\n> "
hFlush stdout
input <- getLine
case input of
"1" -> runMachine Playing settings
-- We return a NEW settings object with the `op` changed
"2" -> do
putStrLn "Operation set to Multiplication."
runMachine Menu (settings { op = Multiply })
"3" -> runMachine Quit settings
_ -> do
putStrLn "Invalid option."
runMachine Menu settings
runMachine Playing settings = do
a <- randomRIO (minVal settings, maxVal settings)
b <- randomRIO (minVal settings, maxVal settings)
let (symbol, correct) = case op settings of
Add -> ("+", a + b)
Multiply -> ("*", a * b)
putStr $ "What is " ++ show a ++ " " ++ symbol ++ " " ++ show b ++ "? ('menu' to go back) "
hFlush stdout
input <- getLine
if input == "menu"
then runMachine Menu settings
else do
case readMaybe input of
Just ans | ans == correct -> putStrLn "Correct!"
| otherwise -> putStrLn $ "Wrong, it was " ++ show correct
Nothing -> putStrLn "Not a number."
runMachine Playing settings
import random
from enum import Enum, auto
from dataclasses import dataclass
class Operation(Enum): ADD = auto(); MULTIPLY = auto()
class AppState(Enum): MENU = auto(); PLAYING = auto(); QUIT = auto()
@dataclass
class Settings:
min: int = 1
max: int = 10
op: Operation = Operation.ADD
def main():
state, settings = AppState.MENU, Settings()
while True:
match state:
case AppState.MENU:
user_in = input("1. Play 2. Multiply 3. Quit\n> ").strip()
match user_in:
case "1": state = AppState.PLAYING
case "2": settings.op = Operation.MULTIPLY
case "3": state = AppState.QUIT
case _: print("Invalid option.")
case AppState.PLAYING:
a = random.randint(settings.min, settings.max)
b = random.randint(settings.min, settings.max)
sym, cor = ("+", a+b) if settings.op == Operation.ADD else ("*", a*b)
ans = input(f"What is {a} {sym} {b}? ('menu' to exit) ").strip()
if ans == "menu":
state = AppState.MENU
continue
try:
if int(ans) == cor: print("Correct!")
else: print(f"Wrong, it was {cor}")
except ValueError: pass
case AppState.QUIT: break
if __name__ == "__main__": main()
import * as readline from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';
const AppState = { MENU: 'MENU', PLAYING: 'PLAYING', QUIT: 'QUIT' };
const Operation = { ADD: 'ADD', MULTIPLY: 'MULTIPLY' };
async function main() {
const rl = readline.createInterface({ input, output });
let state = AppState.MENU;
let settings = { min: 1, max: 10, op: Operation.ADD };
while (true) {
switch (state) {
case AppState.MENU:
const menuIn = (await rl.question("1. Play 2. Set Multiply 3. Quit\n> ")).trim();
if (menuIn === "1") state = AppState.PLAYING;
else if (menuIn === "2") settings.op = Operation.MULTIPLY;
else if (menuIn === "3") state = AppState.QUIT;
break;
case AppState.PLAYING:
const a = Math.floor(Math.random() * 10) + 1;
const b = Math.floor(Math.random() * 10) + 1;
const symbol = settings.op === Operation.ADD ? '+' : '*';
const correct = settings.op === Operation.ADD ? a + b : a * b;
const ansStr = (await rl.question(`What is ${a} ${symbol} ${b}? `)).trim();
if (ansStr === 'menu') { state = AppState.MENU; continue; }
const ans = parseInt(ansStr, 10);
if (!isNaN(ans)) {
if (ans === correct) console.log("Correct!");
else console.log(`Wrong, it was ${correct}`);
}
break;
case AppState.QUIT:
rl.close();
return;
}
}
}
main();
6. Flashcards Quizzer: Higher Order Functions
Haskell Features Introduced: mapM_, Data structures, and dealing with list iterations functionally.
import System.Random.Shuffle (shuffleM) -- Requires the 'random-shuffle' package
import System.IO (hFlush, stdout)
-- Data types with named records
data Flashcard = Flashcard {
question :: String,
answer :: String
} deriving (Show)
main :: IO ()
main = do
let deck = [ Flashcard "Capital of France?" "Paris"
, Flashcard "2 ** 8?" "256"
, Flashcard "Haskell build tool?" "Cabal" ]
-- shuffleM shuffles the list within the IO monad
shuffledDeck <- shuffleM deck
-- Instead of `for` loops, we use `mapM_` to map an IO action over a list
mapM_ askCard shuffledDeck
putStrLn "Deck completed!"
askCard :: Flashcard -> IO ()
askCard card = do
putStr $ "Q: " ++ question card ++ " (Press Enter)"
hFlush stdout
_ <- getLine
putStrLn $ "A: " ++ answer card ++ "\n"
7. OS Downloads Folder Sorter
Haskell Features Introduced: File system interactions (System.Directory, System.FilePath), and mapping pure categorizations to IO.
import System.Directory (listDirectory, doesFileExist, createDirectoryIfMissing, renameFile)
import System.FilePath ((>), takeExtension)
import Control.Monad (forM_)
import Data.Char (toLower)
main :: IO ()
main = do
let downloadsDir = "./downloads_test"
createDirectoryIfMissing True downloadsDir
-- listDirectory reads the contents of the folder
entries <- listDirectory downloadsDir
-- forM_ is similar to a for-loop, it executes an IO action for each item
forM_ entries $ \fileName -> do
let path = downloadsDir > fileName
isFile <- doesFileExist path
if isFile
then do
-- takeExtension safely pulls out the extension (e.g., ".pdf")
let ext = drop 1 $ map toLower (takeExtension fileName)
let folderName = getCategory ext
let targetDir = downloadsDir > folderName
createDirectoryIfMissing True targetDir
let newPath = targetDir > fileName
renameFile path newPath
putStrLn $ "Moved " ++ fileName ++ " to " ++ targetDir
else return ()
-- A perfectly pure function to categorize files!
getCategory :: String -> String
getCategory ext = case ext of
"pdf" -> "Documents"
"docx" -> "Documents"
"txt" -> "Documents"
"jpg" -> "Media"
"png" -> "Media"
"mp4" -> "Media"
"zip" -> "Compressed"
"tar" -> "Compressed"
_ -> "Others"
8. The Haskell Superpower: Pure Immutability & Threading
Python and Javascript are fundamentally built on mutable state. This leads to unpredictable side effects and concurrency deadlocks (or forced single-threaded execution like the GIL). Haskell solves this structurally.
1. Pure Immutability
You cannot change variables in Haskell. Instead of loops mutating a counter, you pass state forward recursively. This entirely prevents "Data Races" across threads.
-- Python:
-- count = 0
-- for i in items: count += 1
-- Haskell: We don't mutate count. We use a fold.
let count = foldl (\acc _ -> acc + 1) 0 items
-- Or better yet, just use the built-in pure function:
let count = length items
2. Concurrency (MVar & STM)
Because 99% of Haskell code is pure (immutable), multi-threading is trivial. When you *do* need shared state, Haskell provides MVar or Software Transactional Memory (STM), which acts like a database transaction for memory.
import Control.Concurrent
main = do
-- Create an empty MVar (thread-safe mailbox)
mvar <- newEmptyMVar
-- forkIO spawns an extremely lightweight green thread
forkIO $ do
putStrLn "Thread 1 doing work..."
putMVar mvar "Result Data" -- Puts data in the box
-- The main thread blocks here until the data arrives
result <- takeMVar mvar
putStrLn $ "Main thread received: " ++ result
The Haskell Concurrency Model
How data moves between green threads
Immutable data can be shared among 100,000 threads simultaneously with zero locks. It is completely safe.
Explicitly marked thread-safe containers. If an STM transaction conflicts, the runtime automatically retries it.
"Fearless Concurrency" isn't a buzzword in Haskell, it's a mathematical guarantee.
9. Haskell Specific Tips & Tricks
Haskell has elegant syntax choices that can confuse newcomers. Here is your cheat sheet.
The Application Operator $
In Haskell, space means function application (e.g. f x). But sometimes you have lots of parentheses: putStrLn (show (length list)).
The $ operator simply means "evaluate everything on the right first". It lets you drop parentheses!
putStrLn $ show $ length list
Function Composition .
Mathematical composition! If you want to chain functions together without mentioning the data explicitly (Point-Free style), use the dot.
Instead of: process x = reverse (sort (map toLower x))
You write: process = reverse . sort . map toLower
Lazy Evaluation & Space Leaks
Haskell is Lazy by default. It won't compute x = 2 + 2 until you actually print x. This allows infinite lists like [1..]!
The Trap: If you build up a huge chain of math without evaluating it, it consumes memory (a thunk). To force strict evaluation, use the $! operator or strict data fields ({-# LANGUAGE StrictData #-}).
10. Standard Library & Ecosystem Essentials
Haskell's standard library is intentionally kept small (the base package). For day-to-day work, you will rely on battle-tested ecosystem packages from Hackage.
Requires aeson in your cabal file. Aeson is the absolute standard for JSON in Haskell. With GHC Generics, you get zero-boilerplate parsing.
{-# LANGUAGE DeriveGeneric #-}
import Data.Aeson
import GHC.Generics
import qualified Data.ByteString.Lazy.Char8 as B
-- DeriveGeneric enables Aeson to automatically write the parser!
data User = User {
id :: Int,
name :: String,
isActive :: Bool
} deriving (Show, Generic)
instance FromJSON User
instance ToJSON User
main :: IO ()
main = do
let jsonData = "{\"id\": 1, \"name\": \"Alice\", \"isActive\": true}"
-- decode returns a Maybe User
let parsed = decode jsonData :: Maybe User
print parsed
-- encode converts it back to ByteString
case parsed of
Just user -> B.putStrLn $ encode user
Nothing -> putStrLn "Failed to parse JSON"
Haskell's default String is actually a linked list of characters ([Char]), which is slow. For real applications, use the Text package and enable OverloadedStrings.
{-# LANGUAGE OverloadedStrings #-}
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
main :: IO ()
main = do
-- Thanks to OverloadedStrings, this string literal is created as Text
let myText = "Hello, High Performance Haskell!" :: T.Text
-- Write efficiently to a file
TIO.writeFile "output.txt" myText
-- Read back
content <- TIO.readFile "output.txt"
TIO.putStrLn content
The gold standard for command line interfaces in Haskell. It uses Applicative Functors to compose argument parsers elegantly.
import Options.Applicative
import Data.Semigroup ((<>))
data Args = Args { name :: String, count :: Int }
argsParser :: Parser Args
argsParser = Args
<$> strOption (long "name" <> short 'n' <> help "Name to greet")
<*> option auto (long "count" <> short 'c' <> value 1 <> showDefault <> help "Greet count")
main :: IO ()
main = do
let opts = info (argsParser <**> helper) (fullDesc <> progDesc "A simple greeter")
args <- execParser opts
-- _ means discard the result, we just want the side-effect (printing)
mapM_ (\_ -> putStrLn $ "Hello " ++ name args ++ "!") [1 .. count args]
The req package provides an easy-to-use, type-safe HTTP client.
{-# LANGUAGE OverloadedStrings #-}
import Network.HTTP.Req
import qualified Data.ByteString.Char8 as B
main :: IO ()
main = runReq defaultHttpConfig $ do
-- Types ensure we can't accidentally send a GET payload incorrectly
let url = https "api.github.com" /: "repos" /: "haskell" /: "haskell"
r <- req GET url NoReqBody bsResponse (header "User-Agent" "haskell-guide")
-- Lift the IO action so it runs inside the Req monad
liftIO $ B.putStrLn $ responseBody r
11. Software Engineering Best Practices (Tooling)
The Haskell ecosystem has matured incredibly in recent years. You no longer need to fight the setup. Use the modern stack.
GHCup & Cabal
GHCup is the universal installer for Haskell (think rustup or nvm). It installs GHC (the compiler), Cabal (the build tool/package manager), and HLS. Run cabal run and you are off to the races.
Haskell Language Server (HLS)
HLS provides world-class IDE support in VSCode or Neovim. It gives you instant type-hints, error checking, and code lenses (like Evaluate). It transforms Haskell development from "fighting the compiler" to "chatting with an assistant."
Hoogle
A search engine specifically for Haskell. Instead of searching by name, you can search by type signature. Want a function that turns a String into an Int? Search for String -> Int on hoogle.haskell.org.
Ormolu / Fourmolu
The standard code formatters. Like Prettier or Black, they format your Haskell code beautifully and deterministically. Integrate it into HLS to format-on-save.