Claude Code Plugins

Community-maintained marketplace

Feedback

convert-python-haskell

@aRustyDev/ai
0
0

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 Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name convert-python-haskell
description 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.

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 Paradigm Haskell Paradigm Impact
Imperative Pure functional with effects in IO monad All side effects explicit
Dynamic typing Strong static typing with inference Errors caught at compile time
Mutable state Immutability, State monad, STRef No accidental mutation
OOP (classes) Typeclasses, data types, functions Data and behavior separate
Exceptions Maybe/Either monads Errors as values
Duck typing Polymorphism via typeclasses Explicit interfaces
Arbitrary precision ints Integer (unbounded) or Int (bounded) Choose precision
Reference counting GC Lazy evaluation + GC Different performance characteristics
Runtime flexibility Compile-time guarantees Less 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

Python Haskell Notes
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

Python Haskell Notes
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

Python Haskell Notes
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

Python Haskell Notes
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 types Generic types with type variables

Type Annotations → Type Signatures

Python Haskell Notes
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 polymorphism No 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

Python Haskell Notes
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
  • @dataclassdata 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:

Aspect Python (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:

Command Purpose Example
: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:

Aspect Python (ctypes/cffi) Haskell (FFI)
Declaration Runtime (ctypes) Compile-time (foreign import)
Type safety Manual (argtypes, restype) Automatic (type signature)
Performance Moderate overhead Near-zero overhead
Inline C Limited (cffi) Full support (inline-c, inline-c-cpp)
Header parsing Manual hsc2hs, c2hs tools
Callback support Yes (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

Tool Purpose Notes
Manual translation Full control Recommended for production
No automatic Python→Haskell transpiler - Paradigm shift too large

Development Tools

Python Haskell Purpose
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 debugger Debugging
venv Not needed Isolation built-in

Build Tools

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

Testing Frameworks

Python Haskell Purpose
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

Python Haskell Purpose
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
  • requestshttp-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.gathermapConcurrently from async library
  • aiohttp.ClientSessionNetwork.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