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.
git clone https://github.com/aRustyDev/agents
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"
content/skills/convert-python-haskell/SKILL.mdConvert 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
- Foundational conversion patterns (APTV workflow, testing strategies)meta-convert-dev
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 is bounded, Integer is arbitrary precision |
| , | Double preferred |
| | Direct mapping |
| , | String is [Char], Text is efficient |
| | |
| | Linked list |
| | Fixed-size tuple |
| | |
| | |
| | From |
| | Nullable types |
| or custom | Tagged unions |
| | Function types |
| or monadic actions | Side effects in IO monad |
| with record syntax | Data types |
| , | Errors as values |
| + typeclass instances | Separate data and behavior |
When Converting Code
- Analyze source thoroughly - understand Python's implicit behavior
- Identify side effects - everything impure goes in IO monad
- Map types first - create comprehensive type table
- Embrace purity - separate pure logic from effects
- Use type inference - let Haskell deduce types where possible
- Leverage typeclasses - replace duck typing with explicit constraints
- Handle laziness - understand evaluation differences
- Test equivalence - QuickCheck for property-based testing
Type System Mapping
Primitive Types
| Python | Haskell | Notes |
|---|---|---|
| | Fixed-size (usually 64-bit), can overflow |
| | Python default - arbitrary precision, no overflow |
| | IEEE 754 double precision (preferred) |
| | Single precision (rarely used) |
| | , |
| | List of - inefficient for large text |
| | Preferred - efficient Unicode text from |
| | Efficient byte sequences from |
| | Part of 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 |
|---|---|---|
| | Linked list (prepend O(1), append O(n)) |
| | Sequence from (better performance) |
| | Fixed-size, immutable |
| | - ordered map |
| | - hash-based |
| | - ordered set |
| | - hash-based |
| | Immutable by default |
| | for double-ended queue |
| | maintains insertion order conceptually |
| with | Use smart constructors |
| | Count occurrences |
Composite Types
| Python | Haskell | Notes |
|---|---|---|
(data) | with record syntax | Data containers |
(behavior) | | Behavior contracts |
| with | Auto-derive instances |
| | Structural → nominal typing |
| with record syntax | Named fields |
| with positional/record | Prefer record syntax |
| (sum type) | Algebraic data types |
| with constructors | Literal types |
| or custom | Tagged union |
| | Nullable types |
| | Function types |
| Polymorphic types | Generic types with type variables |
Type Annotations → Type Signatures
| Python | Haskell | Notes |
|---|---|---|
| | Polymorphic type variable |
| or | Typeclass constraints |
| Avoid - use type variables | defeats type safety |
| Avoid - use polymorphism | No universal base type |
| Type variable , , 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
equivalent - create a module that re-exports__init__.py - Haskell's import system is more granular (import specific functions, qualified imports)
Import Patterns
| Python | Haskell | Notes |
|---|---|---|
| | Import everything |
| | Import specific |
| | Discouraged in Haskell |
| | Qualified import |
| 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
becomes@dataclass
with record syntaxdata - Explicit exports make API boundaries clear
directly maps toOptional[T]Maybe a- Generator expression with
becomesnext()
+filterlistToMaybe
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:
for simple success/failure,Maybe
for detailed errorsEither
provides imperative-style sequencing for monadic operationsdo-notation- 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
→ Haskell'sthreading.Thread
(lightweight threads)forkIO - Python's
→ Haskell'sQueue
(STM-based, composable)TQueue - Python's
→ Haskell'sasyncio.gather
frommapConcurrently
libraryasync - 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
withdataderiving (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
→ Haskell'sNone
(explicit in type signature)Nothing - Default arguments → smart constructors or
typeclassDefault
makes nullable values explicit in type systemMaybe- 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
→ Haskell'sBaseModel
+data
/FromJSON
instancesToJSON - 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 | or | file or |
| Lock file | or | or |
| Install deps | or | or |
| Virtual env | or | Not needed (isolated by default) |
| Version constraints | (caret), (tilde) | |
| Build tool | | or |
| Publish | | |
Why this translation:
- Cabal is the build system, stack is a build tool wrapping Cabal
(hpack) generatespackage.yaml
files automatically.cabal- 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
, etc.shouldSatisfy - pytest fixtures → HSpec
hooks or local definitionsbefore - Parametrized tests →
over test cases in HSpecmapM_ - 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 module | |
/ | Reload after changes | |
/ | Show type | |
/ | Show kind (type of type) | |
/ | Show definition info | |
/ | List module exports | |
| Set options | |
/ | Exit GHCi | |
/ | Multi-line input | |
| Set breakpoint | |
| Trace execution | |
Why this translation:
- GHCi is more powerful for type exploration (
,:type
,:kind
):info
is faster than Python's:reloadimportlib.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 () |
| Type safety | Manual (, ) | 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 () | Yes () |
Why this translation:
- Haskell FFI is compile-time checked (safer than ctypes)
allows embedding C directly in Haskell codeinline-c
preprocessor extracts constants from C headershsc2hs
allows calling Haskell from Cforeign export- 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 |
|---|---|---|
| | REPL |
| (built-in) | Type checking |
/ | | Linting |
| / | Code formatting |
| | Import sorting |
| GHCi debugger | Debugging |
| Not needed | Isolation built-in |
Build Tools
| Python | Haskell | Purpose |
|---|---|---|
| | Package manager |
| | Build tool + package manager |
| | Build configuration |
| - | Package format (not needed) |
Testing Frameworks
| Python | Haskell | Purpose |
|---|---|---|
| | Unit testing |
| | Property-based testing |
| / | Mocking |
| | Benchmarking |
| | Code coverage |
Common Library Equivalents
| Python | Haskell | Purpose |
|---|---|---|
| / | HTTP client |
| (async via IO) | Async HTTP |
/ | / / | Web frameworks |
| + validation | JSON + validation |
/ | | CLI parsing |
| / | Logging |
| | Date/time handling |
| | Path manipulation |
| | Regular expressions |
| | SQLite |
| / | ORM |
| / | 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
(more functional)map - Function composition with
preferred. - Type signature required
works directly on listssum
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
derivingFromJSON
→requests
orhttp-simplehttp-client- Exceptions →
orMaybe
for error handlingEither - 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
frommapConcurrently
libraryasync
→aiohttp.ClientSession
(stateless)Network.HTTP.Simple- HTML parsing with
librarytagsoup - 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:
- Foundational conversion patterns (APTV workflow)meta-convert-dev
- Python development patternslang-python-dev
- Haskell development patternslang-haskell-dev
- Cross-language serialization patternspatterns-serialization-dev
- Cross-language concurrency patternspatterns-concurrency-dev
- Cross-language metaprogramming patternspatterns-metaprogramming-dev