Agents convert-python-haskell

Convert Python code to idiomatic Haskell. Use when migrating Python projects to Haskell, translating Python patterns to idiomatic Haskell, or refactoring Python codebases for type safety, pure functional programming, and advanced type system features. Extends meta-convert-dev with Python-to-Haskell specific patterns.

install
source · Clone the upstream repo
git clone https://github.com/aRustyDev/agents
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/aRustyDev/agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/content/skills/convert-python-haskell" ~/.claude/skills/arustydev-agents-convert-python-haskell && rm -rf "$T"
manifest: content/skills/convert-python-haskell/SKILL.md
source content

Convert Python to Haskell

Convert Python code to idiomatic Haskell. This skill extends

meta-convert-dev
with Python-to-Haskell specific type mappings, idiom translations, and tooling for transforming imperative, dynamically-typed Python code into pure functional, statically-typed Haskell with advanced type system features.

This Skill Extends

  • meta-convert-dev
    - Foundational conversion patterns (APTV workflow, testing strategies)

For general concepts like the Analyze → Plan → Transform → Validate workflow, testing strategies, and common pitfalls, see the meta-skill first.

This Skill Adds

  • Type mappings: Python types → Haskell types (dynamic → static with type inference)
  • Idiom translations: Python patterns → idiomatic Haskell (imperative → pure functional)
  • Module system: Python packages → Haskell modules with explicit exports
  • Error handling: try/except → Maybe/Either monads with do-notation
  • Concurrency: threading/asyncio → async, STM, par monad, forkIO
  • Metaprogramming: decorators → Template Haskell, deriving strategies
  • Zero/Default: None/defaults → Maybe, Default typeclass, smart constructors
  • Serialization: Pydantic → Aeson with FromJSON/ToJSON, Generic deriving
  • Build/Deps: pip/poetry → cabal, stack, hpack
  • Testing: pytest → HSpec, QuickCheck, doctest-haskell
  • Dev Workflow: Python REPL → GHCi with :reload, :type, :kind
  • FFI: C extensions → Haskell FFI, inline-c, hsc2hs

This Skill Does NOT Cover

  • General conversion methodology - see
    meta-convert-dev
  • Python language fundamentals - see
    lang-python-dev
  • Haskell language fundamentals - see
    lang-haskell-dev
  • Reverse conversion (Haskell → Python) - see
    convert-haskell-python
  • Web frameworks - Django/Flask → Servant/Yesod (see framework-specific guides)

Paradigm Shift Overview

Converting from Python to Haskell requires a fundamental shift in thinking:

Python ParadigmHaskell ParadigmImpact
ImperativePure functional with effects in IO monadAll side effects explicit
Dynamic typingStrong static typing with inferenceErrors caught at compile time
Mutable stateImmutability, State monad, STRefNo accidental mutation
OOP (classes)Typeclasses, data types, functionsData and behavior separate
ExceptionsMaybe/Either monadsErrors as values
Duck typingPolymorphism via typeclassesExplicit interfaces
Arbitrary precision intsInteger (unbounded) or Int (bounded)Choose precision
Reference counting GCLazy evaluation + GCDifferent performance characteristics
Runtime flexibilityCompile-time guaranteesLess flexibility, more safety

Key Insight: Haskell forces you to make implicit Python behavior explicit. This initially feels verbose but provides powerful compile-time guarantees.


Quick Reference

PythonHaskellNotes
int
Int
,
Integer
Int is bounded, Integer is arbitrary precision
float
Double
,
Float
Double preferred
bool
Bool
Direct mapping
str
String
,
Text
String is [Char], Text is efficient
bytes
ByteString
Data.ByteString
list[T]
[a]
Linked list
tuple
(a, b, ...)
Fixed-size tuple
dict[K, V]
Map k v
Data.Map
set[T]
Set a
Data.Set
None
Nothing
From
Maybe a
Optional[T]
Maybe a
Nullable types
Union[T, U]
Either a b
or custom
data
Tagged unions
Callable[[Args], Ret]
(Args) -> Ret
Function types
async def
IO ()
or monadic actions
Side effects in IO monad
@dataclass
data
with record syntax
Data types
Exception
Either e a
,
ExceptT
Errors as values
class
data
+ typeclass instances
Separate data and behavior

When Converting Code

  1. Analyze source thoroughly - understand Python's implicit behavior
  2. Identify side effects - everything impure goes in IO monad
  3. Map types first - create comprehensive type table
  4. Embrace purity - separate pure logic from effects
  5. Use type inference - let Haskell deduce types where possible
  6. Leverage typeclasses - replace duck typing with explicit constraints
  7. Handle laziness - understand evaluation differences
  8. Test equivalence - QuickCheck for property-based testing

Type System Mapping

Primitive Types

PythonHaskellNotes
int
Int
Fixed-size (usually 64-bit), can overflow
int
Integer
Python default - arbitrary precision, no overflow
float
Double
IEEE 754 double precision (preferred)
float
Float
Single precision (rarely used)
bool
Bool
True
,
False
str
String
List of
Char
- inefficient for large text
str
Text
Preferred - efficient Unicode text from
Data.Text
bytes
ByteString
Efficient byte sequences from
Data.ByteString
None
Nothing
Part of
Maybe a
type
...
(Ellipsis)
-No direct equivalent

Critical Note on Integers: Python's

int
has arbitrary precision and never overflows. Haskell's
Int
is fixed-size (platform-dependent, usually 64-bit) and can overflow. Use
Integer
for Python-like behavior or validate ranges.

Collection Types

PythonHaskellNotes
list[T]
[a]
Linked list (prepend O(1), append O(n))
list[T]
Seq a
Sequence from
Data.Sequence
(better performance)
tuple
(a, b, ...)
Fixed-size, immutable
dict[K, V]
Map k v
Data.Map
- ordered map
dict[K, V]
HashMap k v
Data.HashMap.Strict
- hash-based
set[T]
Set a
Data.Set
- ordered set
set[T]
HashSet a
Data.HashSet
- hash-based
frozenset[T]
Set a
Immutable by default
collections.deque
Seq a
Data.Sequence
for double-ended queue
collections.OrderedDict
Map k v
Data.Map
maintains insertion order conceptually
collections.defaultdict
Map k v
with
findWithDefault
Use smart constructors
collections.Counter
Map a Int
Count occurrences

Composite Types

PythonHaskellNotes
class
(data)
data
with record syntax
Data containers
class
(behavior)
typeclass
Behavior contracts
@dataclass
data
with
deriving (Show, Eq, Generic)
Auto-derive instances
typing.Protocol
typeclass
Structural → nominal typing
typing.TypedDict
data
with record syntax
Named fields
typing.NamedTuple
data
with positional/record
Prefer record syntax
enum.Enum
data
(sum type)
Algebraic data types
typing.Literal["a", "b"]
data
with constructors
Literal types
typing.Union[T, U]
Either a b
or custom
data
Tagged union
typing.Optional[T]
Maybe a
Nullable types
typing.Callable[[Args], Ret]
(Args) -> Ret
Function types
typing.Generic[T]
Polymorphic typesGeneric types with type variables

Type Annotations → Type Signatures

PythonHaskellNotes
def f(x: T) -> T
f :: a -> a
Polymorphic type variable
def f(x: Iterable[T])
f :: [a] -> ...
or
Foldable t => t a -> ...
Typeclass constraints
x: Any
Avoid - use type variables
Any
defeats type safety
x: object
Avoid - use polymorphismNo universal base type
TypeVar('T')
Type variable
a
,
b
, etc.
Implicit in Haskell

Module System Translation

Python Packages → Haskell Modules

Python:

# myproject/utils/helpers.py
def greet(name: str) -> str:
    return f"Hello, {name}!"

def farewell(name: str) -> str:
    return f"Goodbye, {name}!"

# __init__.py exposes API
from .helpers import greet

# Usage in another file
from myproject.utils import greet

Haskell:

-- MyProject/Utils/Helpers.hs
module MyProject.Utils.Helpers
    ( greet      -- Explicitly export greet
    , farewell   -- Explicitly export farewell
    ) where

greet :: String -> String
greet name = "Hello, " ++ name ++ "!"

farewell :: String -> String
farewell name = "Goodbye, " ++ name ++ "!"

-- MyProject/Utils.hs (re-exports selected functions)
module MyProject.Utils
    ( greet
    ) where

import MyProject.Utils.Helpers (greet, farewell)

-- Usage in another module
import MyProject.Utils (greet)

Why this translation:

  • Python has implicit exports (everything is public); Haskell requires explicit export lists
  • Haskell module names match file paths hierarchically
  • No
    __init__.py
    equivalent - create a module that re-exports
  • Haskell's import system is more granular (import specific functions, qualified imports)

Import Patterns

PythonHaskellNotes
import module
import Module
Import everything
from module import func
import Module (func)
Import specific
from module import *
import Module
Discouraged in Haskell
import module as m
import qualified Module as M
Qualified import
from module import func as f
import Module (func)
then alias in code
No direct syntax

Haskell Import Best Practices:

-- Explicit import list (preferred)
import Data.Map (Map, empty, insert, lookup)

-- Qualified import for disambiguation
import qualified Data.Map as M
import qualified Data.Set as S

-- Import all but hide specific names
import Data.List hiding (head, tail)

-- Import type only (not constructors)
import Data.Map (Map)

Idiom Translation (10 Pillars)

Pillar 1: Module System & Imports

Python:

# myapp/models/user.py
from dataclasses import dataclass
from typing import Optional

@dataclass
class User:
    id: int
    name: str
    email: Optional[str] = None

def find_user(user_id: int, users: list[User]) -> Optional[User]:
    return next((u for u in users if u.id == user_id), None)

Haskell:

-- MyApp/Models/User.hs
module MyApp.Models.User
    ( User(..)    -- Export type and all constructors
    , findUser
    ) where

import Data.Maybe (listToMaybe)

data User = User
    { userId :: Int
    , userName :: String
    , userEmail :: Maybe String
    } deriving (Show, Eq)

findUser :: Int -> [User] -> Maybe User
findUser uid = listToMaybe . filter (\u -> userId u == uid)

Why this translation:

  • Python's
    @dataclass
    becomes
    data
    with record syntax
  • Explicit exports make API boundaries clear
  • Optional[T]
    directly maps to
    Maybe a
  • Generator expression with
    next()
    becomes
    filter
    +
    listToMaybe

Pillar 2: Error Handling (try/except → Maybe/Either)

Python:

def parse_age(s: str) -> int:
    """Parse age from string, raising ValueError on invalid input."""
    age = int(s)
    if age < 0:
        raise ValueError("Age must be non-negative")
    return age

def safe_divide(a: float, b: float) -> float:
    if b == 0:
        raise ZeroDivisionError("Cannot divide by zero")
    return a / b

# Usage with try/except
try:
    age = parse_age("25")
    result = safe_divide(10, age)
    print(f"Result: {result}")
except ValueError as e:
    print(f"Value error: {e}")
except ZeroDivisionError as e:
    print(f"Division error: {e}")

Haskell (Maybe):

import Text.Read (readMaybe)

parseAge :: String -> Maybe Int
parseAge s = do
    age <- readMaybe s
    if age >= 0
        then Just age
        else Nothing

safeDivide :: Double -> Double -> Maybe Double
safeDivide _ 0 = Nothing
safeDivide a b = Just (a / b)

-- Usage with do-notation
processAge :: String -> String
processAge input = case parseAge input of
    Nothing -> "Invalid age"
    Just age -> case safeDivide 10 (fromIntegral age) of
        Nothing -> "Cannot divide by zero"
        Just result -> "Result: " ++ show result

-- Or with monadic composition
processAge' :: String -> Maybe Double
processAge' input = do
    age <- parseAge input
    safeDivide 10 (fromIntegral age)

Haskell (Either for detailed errors):

data ParseError = InvalidFormat String | NegativeAge Int
    deriving (Show, Eq)

parseAge :: String -> Either ParseError Int
parseAge s = case readMaybe s of
    Nothing -> Left (InvalidFormat s)
    Just age -> if age >= 0
        then Right age
        else Left (NegativeAge age)

safeDivide :: Double -> Double -> Either String Double
safeDivide _ 0 = Left "Cannot divide by zero"
safeDivide a b = Right (a / b)

-- ExceptT monad transformer for combining error types
import Control.Monad.Except

processAge :: String -> ExceptT String IO ()
processAge input = do
    age <- case parseAge input of
        Left (InvalidFormat s) -> throwError $ "Invalid format: " ++ s
        Left (NegativeAge a) -> throwError $ "Negative age: " ++ show a
        Right a -> return a
    result <- case safeDivide 10 (fromIntegral age) of
        Left err -> throwError err
        Right r -> return r
    liftIO $ putStrLn $ "Result: " ++ show result

Why this translation:

  • Python exceptions become values:
    Maybe
    for simple success/failure,
    Either
    for detailed errors
  • do-notation
    provides imperative-style sequencing for monadic operations
  • Pattern matching replaces try/except blocks
  • Error information is preserved in the type system (compile-time checking)

Pillar 3: Concurrency (threading/asyncio → async/STM/par)

Python (threading):

import threading
import time
from queue import Queue

def worker(q: Queue, results: list):
    while True:
        item = q.get()
        if item is None:
            break
        # Simulate work
        time.sleep(0.1)
        results.append(item * 2)
        q.task_done()

# Multi-threaded processing
queue = Queue()
results = []
threads = []

for i in range(4):
    t = threading.Thread(target=worker, args=(queue, results))
    t.start()
    threads.append(t)

for item in range(10):
    queue.put(item)

queue.join()

for _ in range(4):
    queue.put(None)

for t in threads:
    t.join()

print(results)

Haskell (forkIO + TVar):

import Control.Concurrent (forkIO, threadDelay)
import Control.Concurrent.STM
import Control.Monad (replicateM_, forM_)

worker :: TQueue Int -> TVar [Int] -> IO ()
worker queue resultsVar = loop
  where
    loop = do
        maybeItem <- atomically $ do
            empty <- isEmptyTQueue queue
            if empty
                then return Nothing
                else Just <$> readTQueue queue
        case maybeItem of
            Nothing -> return ()  -- Queue empty, exit
            Just item -> do
                threadDelay 100000  -- 0.1 seconds
                atomically $ modifyTVar' resultsVar (++ [item * 2])
                loop

main :: IO ()
main = do
    queue <- newTQueueIO
    resultsVar <- newTVarIO []

    -- Spawn 4 worker threads
    workers <- replicateM 4 $ forkIO (worker queue resultsVar)

    -- Enqueue items
    forM_ [0..9] $ \item ->
        atomically $ writeTQueue queue item

    -- Wait for queue to drain (simplified)
    threadDelay 2000000  -- 2 seconds

    results <- readTVarIO resultsVar
    print results

Python (asyncio):

import asyncio

async def fetch_data(url: str) -> str:
    """Simulate async HTTP request."""
    await asyncio.sleep(0.1)
    return f"Data from {url}"

async def main():
    urls = [f"http://example.com/{i}" for i in range(10)]

    # Concurrent execution
    results = await asyncio.gather(*[fetch_data(url) for url in urls])

    for result in results:
        print(result)

asyncio.run(main())

Haskell (async library):

import Control.Concurrent.Async
import Control.Monad (forM)

fetchData :: String -> IO String
fetchData url = do
    threadDelay 100000  -- 0.1 seconds
    return $ "Data from " ++ url

main :: IO ()
main = do
    let urls = ["http://example.com/" ++ show i | i <- [0..9]]

    -- Concurrent execution with async
    results <- mapConcurrently fetchData urls

    mapM_ putStrLn results

-- Or using async/wait manually
mainManual :: IO ()
mainManual = do
    let urls = ["http://example.com/" ++ show i | i <- [0..9]]

    -- Fork all tasks
    asyncs <- mapM (async . fetchData) urls

    -- Wait for all results
    results <- mapM wait asyncs

    mapM_ putStrLn results

Haskell (STM for shared state):

import Control.Concurrent.STM
import Control.Concurrent (forkIO)
import Control.Monad (replicateM_)

-- Shared counter with STM
incrementCounter :: TVar Int -> Int -> IO ()
incrementCounter counter times = replicateM_ times $ atomically $ do
    current <- readTVar counter
    writeTVar counter (current + 1)

main :: IO ()
main = do
    counter <- newTVarIO 0

    -- 10 threads each incrementing 1000 times
    replicateM_ 10 $ forkIO (incrementCounter counter 1000)

    -- Wait and read final value
    threadDelay 1000000  -- 1 second
    finalValue <- readTVarIO counter
    print finalValue  -- Should be 10000

Why this translation:

  • Python's
    threading.Thread
    → Haskell's
    forkIO
    (lightweight threads)
  • Python's
    Queue
    → Haskell's
    TQueue
    (STM-based, composable)
  • Python's
    asyncio.gather
    → Haskell's
    mapConcurrently
    from
    async
    library
  • STM (Software Transactional Memory) provides composable, atomic state changes (superior to locks)
  • Haskell's green threads are cheap (can spawn millions)

Pillar 4: Metaprogramming (decorators → Template Haskell/deriving)

Python (decorators):

from functools import wraps
import time

def timer(func):
    """Decorator to time function execution."""
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} took {end - start:.4f}s")
        return result
    return wrapper

def memoize(func):
    """Decorator for memoization."""
    cache = {}
    @wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

@timer
@memoize
def fibonacci(n: int) -> int:
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# Class decorators
from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

Haskell (deriving strategies):

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}

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

-- Deriving common typeclasses
data Point = Point
    { x :: Double
    , y :: Double
    } deriving stock (Show, Eq, Generic)
      deriving anyclass (FromJSON, ToJSON)

-- Multiple deriving strategies
newtype UserId = UserId Int
    deriving stock (Show, Eq, Ord)
    deriving newtype (Num, Enum)

Haskell (Template Haskell for code generation):

{-# LANGUAGE TemplateHaskell #-}

import Language.Haskell.TH

-- Generate lenses (getter/setters) for record fields
import Control.Lens (makeLenses)

data User = User
    { _userId :: Int
    , _userName :: String
    , _userEmail :: Maybe String
    } deriving (Show, Eq)

makeLenses ''User  -- Generates lenses: userId, userName, userEmail

Haskell (manual memoization - no decorator syntax):

import Data.Function.Memoize (memoize)

-- Memoized fibonacci
fibMemo :: Int -> Integer
fibMemo = memoize fib
  where
    fib 0 = 0
    fib 1 = 1
    fib n = fibMemo (n - 1) + fibMemo (n - 2)

-- Manual timing wrapper (no decorator syntax)
timed :: IO a -> IO a
timed action = do
    start <- getCurrentTime
    result <- action
    end <- getCurrentTime
    putStrLn $ "Took: " ++ show (diffUTCTime end start)
    return result

import Data.Time.Clock

-- Usage
main :: IO ()
main = timed $ do
    print $ fibMemo 30

Why this translation:

  • Python decorators → Haskell deriving strategies for common patterns
  • @dataclass
    data
    with
    deriving (Show, Eq, Generic)
  • Template Haskell for compile-time code generation (e.g., lenses, JSON instances)
  • No decorator syntax for functions - use higher-order functions explicitly
  • Memoization via libraries or manual cache management

Pillar 5: Zero/Default Values (None → Maybe, Default typeclass)

Python (None and default arguments):

from typing import Optional

def greet(name: Optional[str] = None) -> str:
    """Greet user with optional name."""
    if name is None:
        name = "Guest"
    return f"Hello, {name}!"

def get_config(key: str, default: int = 0) -> int:
    """Get config value with default."""
    config = {"timeout": 30, "retries": 3}
    return config.get(key, default)

# None as sentinel value
def process_data(data: Optional[list[int]] = None) -> list[int]:
    if data is None:
        data = []
    return [x * 2 for x in data]

Haskell (Maybe):

import Data.Maybe (fromMaybe)

greet :: Maybe String -> String
greet maybeName = "Hello, " ++ name ++ "!"
  where
    name = fromMaybe "Guest" maybeName

-- Or with pattern matching
greet' :: Maybe String -> String
greet' Nothing = "Hello, Guest!"
greet' (Just name) = "Hello, " ++ name ++ "!"

Haskell (Default typeclass):

import Data.Default (Default(..))
import qualified Data.Map as M

data Config = Config
    { timeout :: Int
    , retries :: Int
    , maxSize :: Int
    } deriving (Show)

instance Default Config where
    def = Config
        { timeout = 30
        , retries = 3
        , maxSize = 1024
        }

getConfig :: String -> M.Map String Int -> Int
getConfig key configMap = M.findWithDefault 0 key configMap

-- Usage
main :: IO ()
main = do
    let config = def :: Config
    print config  -- Uses default values

Haskell (smart constructors):

-- Smart constructor with defaults
data User = User
    { userName :: String
    , userAge :: Int
    , userRole :: Role
    } deriving (Show)

data Role = Admin | User | Guest
    deriving (Show)

-- Smart constructor
makeUser :: String -> User
makeUser name = User
    { userName = name
    , userAge = 0       -- Default age
    , userRole = Guest  -- Default role
    }

-- Builder pattern for optional fields
data UserBuilder = UserBuilder
    { builderName :: Maybe String
    , builderAge :: Maybe Int
    , builderRole :: Maybe Role
    }

emptyBuilder :: UserBuilder
emptyBuilder = UserBuilder Nothing Nothing Nothing

withName :: String -> UserBuilder -> UserBuilder
withName n builder = builder { builderName = Just n }

withAge :: Int -> UserBuilder -> UserBuilder
withAge a builder = builder { builderAge = Just a }

build :: UserBuilder -> Maybe User
build (UserBuilder (Just name) maybeAge maybeRole) =
    Just $ User name (fromMaybe 0 maybeAge) (fromMaybe Guest maybeRole)
build _ = Nothing

Why this translation:

  • Python's
    None
    → Haskell's
    Nothing
    (explicit in type signature)
  • Default arguments → smart constructors or
    Default
    typeclass
  • Maybe
    makes nullable values explicit in type system
  • Builder pattern for complex defaults

Pillar 6: Serialization (Pydantic → Aeson)

Python (Pydantic):

from pydantic import BaseModel, Field, EmailStr, field_validator
from typing import Optional
from datetime import datetime

class User(BaseModel):
    id: int = Field(alias='user_id')
    name: str = Field(min_length=1, max_length=100)
    email: Optional[EmailStr] = None
    age: int = Field(ge=0, le=150)
    created_at: datetime

    @field_validator('name')
    @classmethod
    def name_not_empty(cls, v: str) -> str:
        if not v.strip():
            raise ValueError('name cannot be empty')
        return v

# Usage
import json

user_json = '{"user_id": 1, "name": "Alice", "email": "alice@example.com", "age": 30, "created_at": "2024-01-01T00:00:00"}'
user = User.model_validate_json(user_json)
print(user)

Haskell (Aeson with Generic deriving):

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE OverloadedStrings #-}

import Data.Aeson
import Data.Time (UTCTime)
import GHC.Generics (Generic)
import Data.Text (Text)
import qualified Data.Text as T

data User = User
    { userId :: Int
    , userName :: Text
    , userEmail :: Maybe Text
    , userAge :: Int
    , userCreatedAt :: UTCTime
    } deriving (Show, Generic)

-- Custom Aeson instances with field name mapping
instance FromJSON User where
    parseJSON = withObject "User" $ \v -> User
        <$> v .: "user_id"
        <*> v .: "name"
        <*> v .:? "email"
        <*> v .: "age"
        <*> v .: "created_at"

instance ToJSON User where
    toJSON (User uid name email age created) = object
        [ "user_id" .= uid
        , "name" .= name
        , "email" .= email
        , "age" .= age
        , "created_at" .= created
        ]

-- Validation
validateUser :: User -> Either String User
validateUser user
    | userAge user < 0 || userAge user > 150 =
        Left "Age must be between 0 and 150"
    | T.null (T.strip (userName user)) =
        Left "Name cannot be empty"
    | otherwise =
        Right user

-- Usage
import Data.Aeson (decode, encode)
import qualified Data.ByteString.Lazy as B

parseUser :: B.ByteString -> Either String User
parseUser json = do
    user <- maybe (Left "Invalid JSON") Right (decode json)
    validateUser user

Haskell (Generic deriving for simple cases):

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveAnyClass #-}

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

-- Automatic JSON instances
data Point = Point
    { x :: Double
    , y :: Double
    } deriving (Show, Generic, FromJSON, ToJSON)

-- Custom field naming strategy
import Data.Aeson.TH (deriveJSON, defaultOptions, fieldLabelModifier)
import Data.Char (toLower)

data Config = Config
    { configTimeout :: Int
    , configRetries :: Int
    } deriving (Show, Generic)

-- Drop "config" prefix and lowercase
$(deriveJSON defaultOptions{fieldLabelModifier = \s -> map toLower (drop 6 s)} ''Config)

Why this translation:

  • Pydantic's
    BaseModel
    → Haskell's
    data
    +
    FromJSON
    /
    ToJSON
    instances
  • Field aliases handled in custom JSON instances
  • Validation separate from parsing (parse then validate)
  • Generic deriving reduces boilerplate for simple cases
  • Template Haskell for automatic instance generation with naming strategies

Pillar 7: Build System & Dependencies (pip/poetry → cabal/stack)

Python (pip/poetry):

# pyproject.toml (Poetry)
[tool.poetry]
name = "myproject"
version = "0.1.0"
description = "My Python project"

[tool.poetry.dependencies]
python = "^3.11"
requests = "^2.31.0"
pydantic = "^2.0.0"
aiohttp = "^3.9.0"

[tool.poetry.dev-dependencies]
pytest = "^7.4.0"
mypy = "^1.5.0"
black = "^23.0.0"

# requirements.txt (pip)
requests==2.31.0
pydantic==2.0.0
aiohttp==3.9.0

Haskell (Cabal):

-- myproject.cabal
cabal-version:      3.0
name:               myproject
version:            0.1.0.0
synopsis:           My Haskell project
build-type:         Simple

library
    exposed-modules:  MyProject.Core
                    , MyProject.Utils
    build-depends:    base >=4.16
                    , text >=2.0
                    , aeson >=2.1
                    , http-client >=0.7
                    , http-client-tls >=0.3
    hs-source-dirs:   src
    default-language: GHC2021

executable myproject
    main-is:          Main.hs
    build-depends:    base
                    , myproject
    hs-source-dirs:   app
    default-language: GHC2021

test-suite myproject-test
    type:             exitcode-stdio-1.0
    main-is:          Spec.hs
    build-depends:    base
                    , myproject
                    , hspec >=2.10
                    , QuickCheck >=2.14
    hs-source-dirs:   test
    default-language: GHC2021

Haskell (Stack + package.yaml - simplified):

# package.yaml (hpack format)
name: myproject
version: 0.1.0.0
synopsis: My Haskell project

dependencies:
  - base >= 4.16
  - text >= 2.0
  - aeson >= 2.1
  - http-client >= 0.7
  - http-client-tls >= 0.3

library:
  source-dirs: src
  exposed-modules:
    - MyProject.Core
    - MyProject.Utils

executables:
  myproject:
    main: Main.hs
    source-dirs: app
    dependencies:
      - myproject

tests:
  myproject-test:
    main: Spec.hs
    source-dirs: test
    dependencies:
      - myproject
      - hspec
      - QuickCheck

Comparison:

AspectPython (pip/poetry)Haskell (cabal/stack)
Package definition
pyproject.toml
or
setup.py
.cabal
file or
package.yaml
Lock file
poetry.lock
or
requirements.txt
cabal.project.freeze
or
stack.yaml.lock
Install deps
poetry install
or
pip install -r requirements.txt
cabal build
or
stack build
Virtual env
poetry shell
or
venv
Not needed (isolated by default)
Version constraints
^2.0.0
(caret),
~=2.0
(tilde)
>=2.0 && <3.0
Build tool
poetry build
cabal build
or
stack build
Publish
poetry publish
cabal upload

Why this translation:

  • Cabal is the build system, stack is a build tool wrapping Cabal
  • package.yaml
    (hpack) generates
    .cabal
    files automatically
  • Haskell projects are isolated by default (no need for virtual environments)
  • Cabal supports multiple libraries/executables/test suites in one project
  • Stack uses curated package sets (Stackage) for reproducible builds

Pillar 8: Testing (pytest → HSpec/QuickCheck)

Python (pytest):

import pytest
from myproject.utils import add, divide

def test_add():
    assert add(2, 3) == 5
    assert add(-1, 1) == 0
    assert add(0, 0) == 0

def test_divide():
    assert divide(10, 2) == 5
    with pytest.raises(ZeroDivisionError):
        divide(10, 0)

@pytest.mark.parametrize("a,b,expected", [
    (2, 3, 5),
    (-1, 1, 0),
    (0, 0, 0),
])
def test_add_parametrized(a, b, expected):
    assert add(a, b) == expected

# Fixtures
@pytest.fixture
def sample_data():
    return [1, 2, 3, 4, 5]

def test_sum_with_fixture(sample_data):
    assert sum(sample_data) == 15

Haskell (HSpec):

-- test/Spec.hs
import Test.Hspec
import MyProject.Utils (add, safeDivide)

main :: IO ()
main = hspec $ do
    describe "add" $ do
        it "adds two positive numbers" $
            add 2 3 `shouldBe` 5

        it "adds negative and positive" $
            add (-1) 1 `shouldBe` 0

        it "adds zeros" $
            add 0 0 `shouldBe` 0

    describe "safeDivide" $ do
        it "divides two numbers" $
            safeDivide 10 2 `shouldBe` Just 5

        it "returns Nothing for division by zero" $
            safeDivide 10 0 `shouldBe` Nothing

        context "when using parametrized tests" $ do
            let testCases = [(2, 3, 5), (-1, 1, 0), (0, 0, 0)]
            mapM_ (\(a, b, expected) ->
                it ("adds " ++ show a ++ " and " ++ show b) $
                    add a b `shouldBe` expected
                ) testCases

Haskell (QuickCheck - property-based testing):

import Test.QuickCheck

-- Properties for add
prop_add_commutative :: Int -> Int -> Bool
prop_add_commutative x y = add x y == add y x

prop_add_associative :: Int -> Int -> Int -> Bool
prop_add_associative x y z = add (add x y) z == add x (add y z)

prop_add_identity :: Int -> Bool
prop_add_identity x = add x 0 == x

-- Properties for safeDivide
prop_divide_multiply_inverse :: Double -> Double -> Property
prop_divide_multiply_inverse x y = y /= 0 ==> case safeDivide x y of
    Nothing -> False
    Just result -> abs (result * y - x) < 0.0001

-- Running QuickCheck tests
main :: IO ()
main = do
    quickCheck prop_add_commutative
    quickCheck prop_add_associative
    quickCheck prop_add_identity
    quickCheck prop_divide_multiply_inverse

Haskell (HSpec + QuickCheck integration):

import Test.Hspec
import Test.QuickCheck

main :: IO ()
main = hspec $ do
    describe "add properties" $ do
        it "is commutative" $ property $
            \x y -> add x y == add (y :: Int) (x :: Int)

        it "has zero as identity" $ property $
            \x -> add x 0 == (x :: Int)

        it "is associative" $ property $
            \x y z -> add (add x y) z == add x (add (y :: Int) (z :: Int))

Why this translation:

  • pytest assertions → HSpec
    shouldBe
    ,
    shouldSatisfy
    , etc.
  • pytest fixtures → HSpec
    before
    hooks or local definitions
  • Parametrized tests →
    mapM_
    over test cases in HSpec
  • QuickCheck adds property-based testing (generates random inputs)
  • Properties express laws (commutativity, associativity, etc.)

Pillar 9: Dev Workflow & REPL (Python REPL → GHCi)

Python REPL:

$ python
>>> from myproject.utils import add, divide
>>> add(2, 3)
5
>>> divide(10, 2)
5.0
>>> # Reload module after changes
>>> import importlib
>>> import myproject.utils
>>> importlib.reload(myproject.utils)
>>> # Introspection
>>> help(add)
>>> type(add)
<class 'function'>
>>> add.__annotations__
{'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}

Haskell GHCi:

$ stack ghci
ghci> :load MyProject.Utils
[1 of 1] Compiling MyProject.Utils
Ok, one module loaded.

ghci> add 2 3
5

ghci> safeDivide 10 2
Just 5.0

-- Type inspection
ghci> :type add
add :: Int -> Int -> Int

ghci> :info add
add :: Int -> Int -> Int
        -- Defined at src/MyProject/Utils.hs:10:1

-- Reload after code changes
ghci> :reload
Ok, one module loaded.

-- Kind inspection (type of types)
ghci> :kind Maybe
Maybe :: * -> *

ghci> :kind Int
Int :: *

-- Browse module exports
ghci> :browse MyProject.Utils
add :: Int -> Int -> Int
safeDivide :: Double -> Double -> Maybe Double

-- Set language extensions
ghci> :set -XOverloadedStrings

-- Multi-line input
ghci> :{
ghci| let factorial 0 = 1
ghci|     factorial n = n * factorial (n - 1)
ghci| :}

ghci> factorial 5
120

-- Debugging
ghci> :break MyProject.Utils.add
Breakpoint 0 activated at src/MyProject/Utils.hs:10:1-15

ghci> :trace add 2 3
Stopped in MyProject.Utils.add, src/MyProject/Utils.hs:10:1-15
_result :: Int = _
[src/MyProject/Utils.hs:10:1-15] ghci> :continue
5

GHCi Commands:

CommandPurposeExample
:load
/
:l
Load module
:load Main.hs
:reload
/
:r
Reload after changes
:reload
:type
/
:t
Show type
:type map
:kind
/
:k
Show kind (type of type)
:kind Maybe
:info
/
:i
Show definition info
:info Functor
:browse
/
:b
List module exports
:browse Data.List
:set
Set options
:set -XOverloadedStrings
:quit
/
:q
Exit GHCi
:quit
:{
/
:}
Multi-line input
:{...}
:break
Set breakpoint
:break MyModule.myFunc
:trace
Trace execution
:trace myFunc args

Why this translation:

  • GHCi is more powerful for type exploration (
    :type
    ,
    :kind
    ,
    :info
    )
  • :reload
    is faster than Python's
    importlib.reload
  • Haskell's static types enable better IDE support (Haskell Language Server)
  • GHCi supports debugging with breakpoints and tracing
  • Multi-line input requires
    :{
    /
    :}
    delimiters

Pillar 10: FFI & Interoperability (C extensions → Haskell FFI)

Python (C extension via ctypes):

import ctypes

# Load shared library
libc = ctypes.CDLL("libc.so.6")

# Call C function
libc.printf(b"Hello from C: %d\n", 42)

# Wrapper for type safety
def c_strlen(s: bytes) -> int:
    libc.strlen.argtypes = [ctypes.c_char_p]
    libc.strlen.restype = ctypes.c_size_t
    return libc.strlen(s)

print(c_strlen(b"Hello"))  # 5

Haskell (FFI):

{-# LANGUAGE ForeignFunctionInterface #-}

import Foreign.C.String (CString, withCString, peekCString)
import Foreign.C.Types (CInt(..), CSize(..))

-- Import C function
foreign import ccall "strlen"
    c_strlen :: CString -> IO CSize

-- Wrapper for convenience
strlen :: String -> IO Int
strlen s = withCString s $ \cstr -> do
    len <- c_strlen cstr
    return (fromIntegral len)

main :: IO ()
main = do
    len <- strlen "Hello"
    print len  -- 5

-- Import with unsafe (no callback to Haskell)
foreign import ccall unsafe "strlen"
    c_strlen_unsafe :: CString -> CSize

strlen_pure :: String -> Int
strlen_pure s = fromIntegral $ c_strlen_unsafe (error "null pointer")

Haskell (inline-c for embedding C):

{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}

import qualified Language.C.Inline as C

C.include "<math.h>"

-- Inline C code
square :: Double -> IO Double
square x = [C.exp| double { pow($(double x), 2) } |]

main :: IO ()
main = do
    result <- square 5.0
    print result  -- 25.0

Haskell (hsc2hs for C headers):

-- File: Time.hsc
{-# LANGUAGE ForeignFunctionInterface #-}

#include <time.h>

import Foreign.C.Types (CTime(..))

type TimeT = CTime

foreign import ccall "time"
    c_time :: Ptr TimeT -> IO TimeT

getCurrentTime :: IO TimeT
getCurrentTime = c_time nullPtr

Comparison:

AspectPython (ctypes/cffi)Haskell (FFI)
DeclarationRuntime (ctypes)Compile-time (
foreign import
)
Type safetyManual (
argtypes
,
restype
)
Automatic (type signature)
PerformanceModerate overheadNear-zero overhead
Inline CLimited (cffi)Full support (inline-c, inline-c-cpp)
Header parsingManualhsc2hs, c2hs tools
Callback supportYes (
CFUNCTYPE
)
Yes (
foreign export
)

Why this translation:

  • Haskell FFI is compile-time checked (safer than ctypes)
  • inline-c
    allows embedding C directly in Haskell code
  • hsc2hs
    preprocessor extracts constants from C headers
  • foreign export
    allows calling Haskell from C
  • Performance is better due to compile-time integration

Common Pitfalls

1. Forgetting About Laziness

Problem:

-- Python: Eager evaluation
def process_data(items):
    results = [expensive_func(x) for x in items]
    print(f"Processed {len(results)} items")
    return results

-- Haskell: Lazy evaluation (different behavior!)
processData :: [Int] -> [Int]
processData items = results
  where
    results = map expensiveFunc items  -- NOT evaluated yet!
    -- length results would force evaluation

Solution:

import Control.DeepSeq (force)

-- Force strict evaluation when needed
processData :: [Int] -> [Int]
processData items = force results
  where
    results = map expensiveFunc items

-- Or use strict versions
import qualified Data.Map.Strict as M

2. Confusion Between String and Text

Problem:

-- String is [Char] - inefficient!
slowConcat :: String -> String -> String
slowConcat s1 s2 = s1 ++ s2  -- O(n) for each ++

-- Text is efficient
import Data.Text (Text)
import qualified Data.Text as T

fastConcat :: Text -> Text -> Text
fastConcat t1 t2 = t1 <> t2  -- Efficient

Solution:

{-# LANGUAGE OverloadedStrings #-}

import Data.Text (Text)

-- Use Text for production code
processText :: Text -> Text
processText input = T.toUpper input

3. Not Using Explicit Type Signatures

Problem:

-- Inferred type might be too general
add x y = x + y  -- Inferred: Num a => a -> a -> a

-- Might cause confusing errors later
result = add 1.5 (add 2 3)  -- Error: ambiguous type

Solution:

-- Always add type signatures for top-level functions
add :: Int -> Int -> Int
add x y = x + y

addDouble :: Double -> Double -> Double
addDouble x y = x + y

4. Ignoring Functor/Applicative/Monad

Problem:

-- Imperative style with explicit pattern matching (verbose)
getUserName :: Maybe User -> Maybe String
getUserName maybeUser = case maybeUser of
    Nothing -> Nothing
    Just user -> Just (userName user)

Solution:

-- Use Functor (fmap / <$>)
getUserName :: Maybe User -> Maybe String
getUserName maybeUser = userName <$> maybeUser

-- Or even simpler with point-free style
getUserName :: Maybe User -> Maybe String
getUserName = fmap userName

5. Misunderstanding IO Monad

Problem:

-- Trying to "escape" the IO monad
badGetLine :: String
badGetLine = getLine  -- ERROR: getLine :: IO String, not String

Solution:

-- IO is contagious - functions using IO return IO
goodGetLine :: IO String
goodGetLine = getLine

processInput :: IO ()
processInput = do
    line <- getLine  -- Extract value inside IO context
    putStrLn $ "You said: " ++ line

6. Partial Functions

Problem:

-- Partial functions can crash at runtime
headUnsafe :: [a] -> a
headUnsafe xs = head xs  -- Crashes on empty list!

result = headUnsafe []  -- Runtime error!

Solution:

-- Use total functions (return Maybe)
headSafe :: [a] -> Maybe a
headSafe [] = Nothing
headSafe (x:_) = Just x

-- Or use Data.List.NonEmpty for non-empty lists
import qualified Data.List.NonEmpty as NE

headNonEmpty :: NE.NonEmpty a -> a
headNonEmpty = NE.head  -- Type system guarantees non-empty

7. Integer Overflow

Problem:

-- Python: int has arbitrary precision
# x = 10 ** 100  # Works fine

-- Haskell: Int is bounded
badCompute :: Int
badCompute = 10 ^ 100  -- OVERFLOW! (wraps or crashes)

Solution:

-- Use Integer for arbitrary precision
goodCompute :: Integer
goodCompute = 10 ^ 100  -- Works correctly

-- Or explicitly handle overflow
import Data.Int (Int64)
import GHC.Num.Integer (integerToInt)

8. Space Leaks from Lazy Evaluation

Problem:

-- Lazy fold can build up large thunks
sumLazy :: [Integer] -> Integer
sumLazy = foldl (+) 0  -- Space leak! Builds up (+) thunks

Solution:

import Data.List (foldl')

-- Use strict fold
sumStrict :: [Integer] -> Integer
sumStrict = foldl' (+) 0  -- Evaluates eagerly, no leak

Tooling

Code Translation Tools

ToolPurposeNotes
Manual translationFull controlRecommended for production
No automatic Python→Haskell transpiler-Paradigm shift too large

Development Tools

PythonHaskellPurpose
python
ghci
REPL
mypy
ghc
(built-in)
Type checking
pylint
/
flake8
hlint
Linting
black
fourmolu
/
ormolu
Code formatting
isort
stylish-haskell
Import sorting
pdb
GHCi debuggerDebugging
venv
Not neededIsolation built-in

Build Tools

PythonHaskellPurpose
pip
cabal
Package manager
poetry
stack
Build tool + package manager
setuptools
cabal
Build configuration
wheel
-Package format (not needed)

Testing Frameworks

PythonHaskellPurpose
pytest
hspec
Unit testing
hypothesis
quickcheck
Property-based testing
unittest.mock
hspec-mock
/
HMock
Mocking
pytest-benchmark
criterion
Benchmarking
coverage.py
hpc
Code coverage

Common Library Equivalents

PythonHaskellPurpose
requests
http-client
/
http-conduit
HTTP client
aiohttp
http-client
(async via IO)
Async HTTP
flask
/
django
servant
/
yesod
/
scotty
Web frameworks
pydantic
aeson
+ validation
JSON + validation
click
/
argparse
optparse-applicative
CLI parsing
logging
monad-logger
/
katip
Logging
datetime
time
Date/time handling
pathlib
filepath
Path manipulation
re
regex
Regular expressions
sqlite3
sqlite-simple
SQLite
sqlalchemy
persistent
/
beam
ORM
asyncio
async
/
stm
Concurrency

Examples

Example 1: Simple - List Processing

Before (Python):

def process_numbers(numbers: list[int]) -> list[int]:
    """Filter even numbers, square them, and sum."""
    evens = [x for x in numbers if x % 2 == 0]
    squared = [x * x for x in evens]
    return sum(squared)

result = process_numbers([1, 2, 3, 4, 5, 6])
print(result)  # 56 (4 + 16 + 36)

After (Haskell):

processNumbers :: [Int] -> Int
processNumbers numbers =
    sum $ map square $ filter even numbers
  where
    square x = x * x

-- Or with function composition
processNumbers' :: [Int] -> Int
processNumbers' = sum . map (^2) . filter even

-- Or with list comprehension (less idiomatic)
processNumbers'' :: [Int] -> Int
processNumbers'' numbers = sum [x^2 | x <- numbers, even x]

main :: IO ()
main = print $ processNumbers [1, 2, 3, 4, 5, 6]  -- 56

Key changes:

  • List comprehension →
    filter
    +
    map
    (more functional)
  • Function composition with
    .
    preferred
  • Type signature required
  • sum
    works directly on lists

Example 2: Medium - JSON API Client

Before (Python):

import requests
from typing import Optional
from pydantic import BaseModel

class User(BaseModel):
    id: int
    name: str
    email: str

def fetch_user(user_id: int) -> Optional[User]:
    """Fetch user from API."""
    try:
        response = requests.get(f"https://api.example.com/users/{user_id}")
        response.raise_for_status()
        return User(**response.json())
    except requests.HTTPError as e:
        print(f"HTTP error: {e}")
        return None
    except Exception as e:
        print(f"Error: {e}")
        return None

# Usage
if user := fetch_user(123):
    print(f"User: {user.name}")
else:
    print("User not found")

After (Haskell):

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric #-}

import Network.HTTP.Simple
import Data.Aeson (FromJSON, decode)
import GHC.Generics (Generic)
import qualified Data.ByteString.Lazy as BL

data User = User
    { userId :: Int
    , userName :: String
    , userEmail :: String
    } deriving (Show, Generic, FromJSON)

fetchUser :: Int -> IO (Maybe User)
fetchUser userId = do
    let request = setRequestMethod "GET" $
                  setRequestHost "api.example.com" $
                  setRequestPath (fromString $ "/users/" ++ show userId) $
                  setRequestSecure True $
                  setRequestPort 443 $
                  defaultRequest

    response <- httpLBS request
    let body = getResponseBody response
    return $ decode body

-- Or with http-client-tls and aeson
import Network.HTTP.Client
import Network.HTTP.Client.TLS (newTlsManager)

fetchUser' :: Int -> IO (Either String User)
fetchUser' uid = do
    manager <- newTlsManager
    request <- parseRequest $ "https://api.example.com/users/" ++ show uid
    response <- httpLbs request manager
    case decode (responseBody response) of
        Nothing -> return $ Left "Failed to parse JSON"
        Just user -> return $ Right user

main :: IO ()
main = do
    maybeUser <- fetchUser 123
    case maybeUser of
        Nothing -> putStrLn "User not found"
        Just user -> putStrLn $ "User: " ++ userName user

Key changes:

  • Pydantic → Aeson with
    FromJSON
    deriving
  • requests
    http-simple
    or
    http-client
  • Exceptions →
    Maybe
    or
    Either
    for error handling
  • JSON parsing is type-safe at compile time
  • HTTP client requires explicit configuration

Example 3: Complex - Concurrent Web Scraper

Before (Python):

import asyncio
import aiohttp
from typing import List
from dataclasses import dataclass

@dataclass
class Article:
    title: str
    url: str

async def fetch_page(session: aiohttp.ClientSession, url: str) -> str:
    async with session.get(url) as response:
        return await response.text()

async def scrape_articles(urls: List[str]) -> List[Article]:
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_page(session, url) for url in urls]
        pages = await asyncio.gather(*tasks)

        articles = []
        for url, page in zip(urls, pages):
            # Simplified parsing
            title = page.split('<title>')[1].split('</title>')[0]
            articles.append(Article(title=title, url=url))

        return articles

# Usage
urls = [f"https://example.com/page{i}" for i in range(10)]
articles = asyncio.run(scrape_articles(urls))
for article in articles:
    print(f"{article.title}: {article.url}")

After (Haskell):

{-# LANGUAGE OverloadedStrings #-}

import Network.HTTP.Simple
import Control.Concurrent.Async (mapConcurrently)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Text.HTML.TagSoup (parseTags, Tag(..))

data Article = Article
    { articleTitle :: Text
    , articleUrl :: Text
    } deriving (Show)

fetchPage :: String -> IO Text
fetchPage url = do
    request <- parseRequest url
    response <- httpBS request
    return $ TE.decodeUtf8 (getResponseBody response)

parseTitle :: Text -> Text
parseTitle html =
    case dropWhile (not . isTitle) tags of
        (TagOpen "title" _:TagText title:_) -> title
        _ -> "No title"
  where
    tags = parseTags html
    isTitle (TagOpen "title" _) = True
    isTitle _ = False

scrapeArticle :: String -> IO Article
scrapeArticle url = do
    page <- fetchPage url
    let title = parseTitle page
    return $ Article title (T.pack url)

scrapeArticles :: [String] -> IO [Article]
scrapeArticles urls = mapConcurrently scrapeArticle urls

main :: IO ()
main = do
    let urls = ["https://example.com/page" ++ show i | i <- [1..10]]
    articles <- scrapeArticles urls
    mapM_ (\article -> putStrLn $ T.unpack (articleTitle article) ++ ": " ++ T.unpack (articleUrl article)) articles

Key changes:

  • asyncio.gather
    mapConcurrently
    from
    async
    library
  • aiohttp.ClientSession
    Network.HTTP.Simple
    (stateless)
  • HTML parsing with
    tagsoup
    library
  • Concurrency via lightweight threads (forkIO under the hood)
  • No need for async/await syntax (IO monad handles effects)

See Also

For more patterns and examples, see:

  • meta-convert-dev
    - Foundational conversion patterns (APTV workflow)
  • lang-python-dev
    - Python development patterns
  • lang-haskell-dev
    - Haskell development patterns
  • patterns-serialization-dev
    - Cross-language serialization patterns
  • patterns-concurrency-dev
    - Cross-language concurrency patterns
  • patterns-metaprogramming-dev
    - Cross-language metaprogramming patterns