Claude-skill-registry convert-python-roc
Convert Python code to idiomatic Roc. Use when migrating Python projects to Roc, translating Python patterns to idiomatic Roc, or refactoring Python codebases for type safety, functional purity, and native performance. Extends meta-convert-dev with Python-to-Roc specific patterns.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/convert-python-roc" ~/.claude/skills/majiayu000-claude-skill-registry-convert-python-roc && rm -rf "$T"
skills/data/convert-python-roc/SKILL.mdConvert Python to Roc
Convert Python code to idiomatic Roc. This skill extends
meta-convert-dev with Python-to-Roc specific type mappings, idiom translations, and architectural guidance for transforming dynamic, garbage-collected Python code into statically-typed, pure functional Roc with platform-separated effects.
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 → Roc types (dynamic → static with inference)
- Idiom translations: Python patterns → idiomatic Roc
- Error handling: Exceptions → Result types
- Platform architecture: Python runtime → Roc platform/application model
- Async patterns: asyncio → Task-based effects
- Class hierarchy: OOP → records + functions
- Dev workflow: REPL-driven → expect-driven development
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - Python language fundamentals - see
lang-python-dev - Roc language fundamentals - see
lang-roc-dev - Reverse conversion (Roc → Python) - not recommended
Quick Reference
| Python | Roc | Notes |
|---|---|---|
| or | Roc uses fixed-size integers; Python has arbitrary precision |
| | 64-bit floating point |
| | Direct mapping |
| | UTF-8 strings |
| | Byte arrays become lists |
| | Immutable lists |
| or record | Tuples or named records |
| | Immutable dictionaries |
| | Immutable sets |
| | Use tag unions for optional |
| tag union | Tagged unions |
| | Optional values |
| | Functions |
| | Data + behavior separated |
| | Platform-provided effects |
| record | Structural records |
| | No exceptions in Roc |
When Converting Code
- Analyze source thoroughly before writing target
- Map types first - Python's dynamic types need explicit Roc types
- Separate pure from effectful - Roc strictly enforces purity
- Embrace immutability - no
keyword in Rocmutable - Adopt Roc idioms - don't write "Python code in Roc syntax"
- Think in pattern matching - replace if/elif chains
- Let types guide - use Roc's type inference
- Test equivalence - same inputs → same outputs
- Consider platform model - effects go through platform boundary
Paradigm Translation
Mental Model Shift: Python Runtime → Roc Platform Model
The biggest conceptual shift is how effects (I/O, async, state) are handled:
| Python Concept | Roc Approach | Key Insight |
|---|---|---|
| Python runtime with GC | Platform provides runtime | Runtime external to application |
| Direct I/O anywhere | Platform-provided Task | Application stays pure |
| via platform | Effects delegated to platform |
| Mutable by default | Immutable by default | No mutation operators |
| Duck typing | Static structural typing | Compiler infers types |
| Dynamic introspection | Compile-time types only | No runtime type inspection |
| Exception handling | Result type only | No runtime exceptions |
| Classes with state | Records + module functions | Data and behavior separated |
Architecture Mental Model
Python Roc (Platform Model) ┌─────────────────────┐ ┌─────────────────────┐ │ Your Python Code │ │ Your Roc Code │ │ (can do I/O) │ │ (pure only) │ │ ↓ │ │ ↓ │ │ Python stdlib │ │ Platform API │ │ ↓ │ │ ↓ │ │ CPython runtime │ │ Platform Host │ └─────────────────────┘ └─────────────────────┘ Everything in Clear separation same runtime between pure & effects
Key shift: In Python, you can call
print() anywhere. In Roc, all I/O goes through the platform's Task type. This enforces functional purity in your application code.
Type System Mapping
Primitive Types
| Python | Roc | Notes |
|---|---|---|
| | Python has arbitrary precision; Roc uses 64-bit signed |
| | For positive-only values |
| , , | Smaller sizes for memory efficiency |
| , , | Unsigned variants |
| | Default 64-bit floating point |
| | 32-bit for memory efficiency |
| | Direct mapping |
| | UTF-8 strings (both immutable) |
| | Byte arrays as lists |
| Tag in union | Use pattern |
Critical differences:
- Python
has arbitrary precision; Roc integers have fixed sizes (choose appropriately)int - Python strings are unicode; Roc strings are UTF-8 (compatible but mind encoding)
- Python
is a singleton; Roc uses tag unions for optional valuesNone
Collection Types
| Python | Roc | Notes |
|---|---|---|
| | Both are ordered; Roc is immutable |
| | Fixed-size tuples map directly |
| | Immutable dictionaries |
| | Immutable sets |
| | All Roc collections are immutable |
| | Use list; platform-specific for performance |
| | Map to counts |
| | Use default parameter |
Key insight: All Roc collections are immutable by default. Operations return new collections.
Composite Types
| Python | Roc | Notes |
|---|---|---|
| record | Structural records |
(data) | record | Data-only classes |
(behavior) | | Separate data and functions |
| record | Named fields |
| record | Named record preferred |
| tag union | Discriminated unions |
| tag | Tagged unions |
| | Optional pattern |
| tag union | Enumerated values |
| | Function types |
Type Annotations → Type Signatures
| Python | Roc | Notes |
|---|---|---|
| | Function type signature |
| | Generic type variable |
| (generic) | Use generics instead of Any |
| | List of integers |
| | Optional via tag union |
| | Discriminated union |
Idiom Translation
Pattern 1: None/Optional Handling
Python:
def find_user(user_id: int) -> Optional[dict]: for user in users: if user["id"] == user_id: return user return None # Usage with walrus operator if user := find_user(1): name = user["name"] else: name = "Unknown" # Or with ternary name = user["name"] if user else "Unknown"
Roc:
findUser : I64 -> [Some User, None] findUser = \userId -> when List.findFirst(users, \u -> u.id == userId) is Ok(user) -> Some(user) Err(_) -> None # Usage with pattern matching name = when findUser(1) is Some(user) -> user.name None -> "Unknown"
Why this translation:
- Python uses
and truthy checks; Roc uses tag unionsNone[Some a, None] - Python allows property access on potentially-None values (runtime error); Roc requires pattern matching (compile-time safety)
- Roc's exhaustive pattern matching prevents forgetting the None case
Pattern 2: Result for Error Handling
Python:
def divide(x: int, y: int) -> int: if y == 0: raise ValueError("Division by zero") return x // y # Exception handling try: result = divide(10, 2) print(f"Result: {result}") except ValueError as e: print(f"Error: {e}")
Roc:
divide : I64, I64 -> Result I64 [DivByZero] divide = \x, y -> if y == 0 then Err(DivByZero) else Ok(x // y) # Pattern matching when divide(10, 2) is Ok(result) -> Stdout.line!("Result: \(Num.toStr(result))") Err(DivByZero) -> Stdout.line!("Error: Division by zero")
Why this translation:
- Python uses exceptions; Roc uses Result type (compile-time error handling)
- Python
→ Roctry/except
pattern matchingwhen ... is - Roc errors are typed (DivByZero tag, not string), enabling exhaustive checking
Pattern 3: List Comprehensions
Python:
# List comprehension squared_evens = [x * x for x in numbers if x % 2 == 0] # Generator expression with sum total = sum(x * x for x in numbers if x % 2 == 0) # Nested comprehension matrix = [[i+j for j in range(3)] for i in range(3)]
Roc:
# List operations squaredEvens = numbers |> List.keepIf(\x -> x % 2 == 0) |> List.map(\x -> x * x) # Fold for aggregation total = numbers |> List.keepIf(\x -> x % 2 == 0) |> List.map(\x -> x * x) |> List.walk(0, Num.add) # Nested - use map matrix = List.range({ start: At(0), end: Before(3) }) |> List.map(\i -> List.range({ start: At(0), end: Before(3) }) |> List.map(\j -> i + j) )
Why this translation:
- Python comprehensions are concise but limited; Roc uses explicit pipeline of operations
- Roc's
(filter) +List.keepIf
composes clearlyList.map - For aggregation, use
(fold) instead of built-inList.walksum - Pipeline operator
provides left-to-right data flow like comprehensions|>
Pattern 4: Dictionary Operations
Python:
# Get with default value = config.get("timeout", 30) # Dictionary comprehension squared = {k: v * v for k, v in items.items()} # Update config["timeout"] = 60 # Mutable # Merge dictionaries merged = {**dict1, **dict2}
Roc:
# Get with default value = Dict.get(config, "timeout") |> Result.withDefault(30) # Map values (transform dictionary) squared = Dict.map(items, \_, v -> v * v) # Update (returns new dictionary) newConfig = Dict.insert(config, "timeout", 60) # Merge dictionaries merged = Dict.insertAll(dict1, dict2)
Why this translation:
- Python dicts are mutable; Roc dicts are immutable (operations return new dicts)
- Python
→ Rocdict.get(key, default)
returns Result, useDict.getwithDefault - Dict comprehensions →
for value transformationDict.map - Roc's functional style makes data flow explicit
Pattern 5: String Formatting
Python:
# f-strings (Python 3.6+) message = f"User {user['name']} has {count} items" # format method message = "User {} has {} items".format(user['name'], count) # % formatting message = "User %s has %d items" % (user['name'], count)
Roc:
# String interpolation message = "User \(user.name) has \(Num.toStr(count)) items" # String concatenation (avoid for complex strings) message = Str.concat([ "User ", user.name, " has ", Num.toStr(count), " items", ])
Why this translation:
- Python f-strings auto-convert to string; Roc requires explicit
for numbersNum.toStr - Roc uses
for interpolation (similar to f-string braces)\(expr) - String concatenation is available but interpolation is cleaner
Pattern 6: Class → Record + Module Functions
Python:
@dataclass class User: id: int name: str email: str active: bool = True def deactivate(self): self.active = False def get_display_name(self) -> str: return f"{self.name} ({self.email})" # Usage user = User(id=1, name="Alice", email="alice@example.com") user.deactivate() display = user.get_display_name()
Roc:
# Record type User : { id : I64, name : Str, email : Str, active : Bool, } # Module functions deactivate : User -> User deactivate = \user -> { user & active: Bool.false } getDisplayName : User -> Str getDisplayName = \user -> "\(user.name) (\(user.email))" # Usage user = { id: 1, name: "Alice", email: "alice@example.com", active: Bool.true } deactivatedUser = deactivate(user) display = getDisplayName(deactivatedUser)
Why this translation:
- Python classes combine data and behavior; Roc separates into records (data) and module functions (behavior)
- Python methods mutate
; Roc functions return new values (immutability)self - No
in Roc - pass the record explicitly as a parameterself - Roc's approach is more functional: data + pure functions
Pattern 7: Async/Await → Task
Python:
import asyncio async def fetch_user(user_id: int) -> dict: await asyncio.sleep(0.1) # Simulate I/O return {"id": user_id, "name": f"User {user_id}"} async def process_users(user_ids: list[int]) -> list[dict]: tasks = [fetch_user(uid) for uid in user_ids] return await asyncio.gather(*tasks) # Run result = asyncio.run(process_users([1, 2, 3]))
Roc:
import pf.Task exposing [Task] import pf.Http fetchUser : I64 -> Task User [HttpErr]* fetchUser = \userId -> # Platform handles I/O Http.get!("https://api.example.com/users/\(Num.toStr(userId))") processUsers : List I64 -> Task (List User) [HttpErr]* processUsers = \userIds -> # Platform may parallelize userIds |> List.map(fetchUser) |> Task.sequence # main is already a Task - no explicit run main : Task {} [] main = users = processUsers!([1, 2, 3]) Stdout.line!("Processed \(Num.toStr(List.len(users))) users")
Why this translation:
- Python
→ Rocasync def
(platform-provided)Task a err - Python
→ Rocawait
suffix (try operator)! - Python
→ Rocasyncio.gather
(platform controls parallelism)Task.sequence - Python needs
; Rocasyncio.run
is already a Task that platform executesmain - Roc's platform model separates pure code from effectful I/O
Pattern 8: Context Managers → Try with Cleanup
Python:
# with statement for automatic cleanup with open("file.txt") as f: content = f.read() # File automatically closed # Custom context manager from contextlib import contextmanager @contextmanager def timer(): start = time.time() yield print(f"Elapsed: {time.time() - start:.2f}s") with timer(): # Code to time expensive_operation()
Roc:
# File I/O with platform Task readFile : Str -> Task Str [FileReadErr]* readFile = \path -> File.readUtf8!(Path.fromStr(path)) # Platform handles file closing # No direct equivalent to context managers # Resource cleanup handled by platform # For custom timing, use explicit start/end processWithTiming : {} -> Task {} [] processWithTiming = \{} -> start = getCurrentTime! expensiveOperation! end = getCurrentTime! elapsed = end - start Stdout.line!("Elapsed: \(Num.toStr(elapsed))s")
Why this translation:
- Python context managers (
) → Roc platforms handle resource cleanupwith - Python's
/__enter__
protocol → No direct equivalent; platform manages resources__exit__ - File operations return Task; platform ensures proper cleanup
- For custom resource management, structure as explicit acquisition/release in Task chains
Pattern 9: Exception Hierarchy → Tagged Errors
Python:
class ValidationError(Exception): pass class InvalidName(ValidationError): pass class InvalidAge(ValidationError): pass def validate_person(name: str, age: int) -> dict: if not name: raise InvalidName("Name cannot be empty") if age < 0 or age > 120: raise InvalidAge("Age must be 0-120") return {"name": name, "age": age} try: person = validate_person("", 30) except InvalidName as e: print(f"Name error: {e}") except InvalidAge as e: print(f"Age error: {e}") except ValidationError as e: print(f"Validation error: {e}")
Roc:
ValidationError : [InvalidName Str, InvalidAge Str] Person : { name : Str, age : I64 } validatePerson : Str, I64 -> Result Person ValidationError validatePerson = \name, age -> if Str.isEmpty(name) then Err(InvalidName("Name cannot be empty")) else if age < 0 || age > 120 then Err(InvalidAge("Age must be 0-120")) else Ok({ name, age }) # Pattern matching on errors when validatePerson("", 30) is Ok(person) -> Stdout.line!("Valid: \(person.name)") Err(InvalidName(msg)) -> Stdout.line!("Name error: \(msg)") Err(InvalidAge(msg)) -> Stdout.line!("Age error: \(msg)")
Why this translation:
- Python exception hierarchies → Roc tag unions for error types
- Python
→ RocraiseErr(tag) - Python
with hierarchy → Roc pattern matching on all error variantstry/except - Roc's exhaustive matching ensures all error cases are handled at compile time
Pattern 10: Decorators → Higher-Order Functions
Python:
def log_calls(func): def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") result = func(*args, **kwargs) print(f"Result: {result}") return result return wrapper @log_calls def add(a: int, b: int) -> int: return a + b result = add(2, 3)
Roc:
# Higher-order function approach logCalls : (a -> b), Str -> (a -> b) logCalls = \fn, name -> \arg -> Stdout.line!("Calling \(name)") result = fn(arg) Stdout.line!("Result: \(Inspect.toStr(result))") result # Note: The above won't work because Stdout.line! is effectful # In Roc, decorators with side effects need Task wrapping # For pure logging (accumulated), use: loggedAdd : I64, I64 -> (I64, List Str) loggedAdd = \a, b -> result = a + b logs = ["Calling add", "Result: \(Num.toStr(result))"] (result, logs)
Why this translation:
- Python decorators can have side effects anywhere; Roc enforces purity
- For logging with I/O, wrap in Task (not shown above for simplicity)
- For pure transformations, use higher-order functions
- Roc has no built-in decorator syntax; use explicit function composition
Error Handling
Python Exceptions → Roc Result
Python relies heavily on exceptions for error handling. Roc has no exceptions - all errors are values via the Result type.
Conversion strategy:
- Identify all
statements → Convert toraise
returnsErr(tag) - Identify all
blocks → Convert totry/except
pattern matchingwhen ... is - Define error tag unions for related errors
- Use
(try operator) for error propagation up the call stack!
Python:
def parse_and_divide(a_str: str, b_str: str) -> int: try: a = int(a_str) b = int(b_str) if b == 0: raise ValueError("Division by zero") return a // b except ValueError as e: raise ValueError(f"Invalid input: {e}") try: result = parse_and_divide("10", "2") print(f"Result: {result}") except ValueError as e: print(f"Error: {e}")
Roc:
ParseError : [InvalidNumber Str, DivByZero] parseAndDivide : Str, Str -> Result I64 ParseError parseAndDivide = \aStr, bStr -> a = Str.toI64!(aStr) |> Result.mapErr(\_ -> InvalidNumber("Invalid number: \(aStr)")) b = Str.toI64!(bStr) |> Result.mapErr(\_ -> InvalidNumber("Invalid number: \(bStr)")) if b == 0 then Err(DivByZero) else Ok(a // b) # Usage when parseAndDivide("10", "2") is Ok(result) -> Stdout.line!("Result: \(Num.toStr(result))") Err(InvalidNumber(msg)) -> Stdout.line!("Error: \(msg)") Err(DivByZero) -> Stdout.line!("Error: Division by zero")
Key differences:
- Python exceptions can be raised from anywhere; Roc Result must be explicitly returned
- Python exception hierarchy (base/derived); Roc tag unions (flat structure with tags)
- Python
is runtime; Roc pattern matching is compile-time verified (exhaustiveness)try/except
Concurrency & Async Patterns
Python Threading/Asyncio → Roc Task
Python has multiple concurrency models (threading, multiprocessing, asyncio). Roc delegates all concurrency to the platform via Task.
Python (Threading):
import threading def worker(name: str, result_list: list): # Simulate work result = f"Worker {name} done" result_list.append(result) results = [] threads = [ threading.Thread(target=worker, args=(f"T{i}", results)) for i in range(3) ] for t in threads: t.start() for t in threads: t.join() print(results)
Python (Asyncio):
import asyncio async def worker(name: str) -> str: await asyncio.sleep(0.1) return f"Worker {name} done" async def main(): tasks = [worker(f"T{i}") for i in range(3)] results = await asyncio.gather(*tasks) print(results) asyncio.run(main())
Roc:
import pf.Task exposing [Task] worker : Str -> Task Str [] worker = \name -> # Platform handles concurrency Task.ok("Worker \(name) done") main : Task {} [] main = tasks = List.range({ start: At(0), end: Before(3) }) |> List.map(\i -> worker("T\(Num.toStr(i))")) results = Task.sequence!(tasks) # Platform may parallelize Stdout.line!(Inspect.toStr(results))
Why this translation:
- Python explicit threading/asyncio → Roc platform-controlled concurrency
- Python
→ Rocasyncio.gather
(platform decides how to execute)Task.sequence - Python threads share memory (GIL); Roc Task isolates effects via platform
- Roc applications don't manage threads - the platform does
GIL Considerations
Python's Global Interpreter Lock (GIL) limits true parallelism for CPU-bound tasks. Roc has no GIL - parallelism is platform-dependent.
| Python Limitation | Roc Advantage |
|---|---|
| Threading blocked by GIL | Platform can use true parallelism |
| Need multiprocessing for CPU-bound | Platform handles scheduling |
| Async only for I/O-bound | Task can be I/O or compute |
Key insight: When converting from Python, you may gain concurrency benefits if the Roc platform implements parallel task execution.
Dev Workflow Translation
REPL-Driven Development → Expect-Driven Development
Python has a strong REPL culture (IPython, Jupyter). Roc has
roc repl but it's more limited. The conversion requires adapting workflows.
| Python Workflow | Roc Equivalent | Notes |
|---|---|---|
| IPython REPL | | Limited compared to IPython |
| Jupyter notebooks | N/A | No notebook support |
| Interactive debugging | function | Print-style debugging |
| Hot reload | | Watches and rebuilds |
| | Direct execution |
| | Runs all expect statements |
Python (REPL-driven):
# IPython session >>> def double(x): ... return x * 2 >>> double(5) # Immediate feedback 10 >>> [double(x) for x in range(5)] # Experiment [0, 2, 4, 6, 8]
Roc (Expect-driven):
# In file double : I64 -> I64 double = \x -> x * 2 # Inline tests provide rapid feedback expect double(5) == 10 expect List.map([0, 1, 2, 3, 4], double) == [0, 2, 4, 6, 8] # Run with: roc test file.roc
Migration strategy:
- Convert interactive REPL experiments to
statementsexpect - Use
for rapid feedback (similar to running code in REPL)roc test - Use
for watch mode during developmentroc dev - For exploration, write small test files instead of REPL sessions
Common Pitfalls
-
Trying to mutate variables
- Python allows
on existing variablex = x + 1 - Roc has no mutation - you'd create new bindings in new scopes
- Fix: Embrace immutability; use recursion or fold for accumulation
- Python allows
-
Assuming exceptions work
- Python:
raise ValueError("message") - Roc has no exceptions
- Fix: Use
for all fallible operationsResult a err
- Python:
-
Expecting duck typing
- Python: "If it has a .read() method, it's file-like"
- Roc uses static structural typing
- Fix: Define explicit record types or use abilities
-
Forgetting to handle all error cases
- Python: Can catch broad
or miss casesException - Roc enforces exhaustive pattern matching
- Fix: Handle all variants in
blockswhen ... is
- Python: Can catch broad
-
Using mutable data structures
- Python:
,list.append()dict[key] = value - Roc collections are immutable
- Fix: Operations return new collections:
,List.appendDict.insert
- Python:
-
Mixing pure and effectful code
- Python allows I/O anywhere
- Roc enforces purity; I/O must be in
Task - Fix: Separate pure business logic from effectful I/O at platform boundaries
-
Expecting arbitrary precision integers
- Python
has unlimited precisionint - Roc integers have fixed sizes (I64, U64, etc.)
- Fix: Choose appropriate size or handle overflow explicitly
- Python
-
Assuming REPL workflow
- Python: IPython, interactive development
- Roc REPL is more limited
- Fix: Use
for inline tests,expect
for rapid feedbackroc test
-
Trying to use
directlyNone- Python:
,value = Noneif value is None: - Roc has no
typeNone - Fix: Use tag unions:
and pattern matching[Some a, None]
- Python:
-
Forgetting platform/application split
- Python code is all in same runtime
- Roc separates pure (app) from effects (platform)
- Fix: Keep business logic pure, delegate I/O to platform Task
Module System
Python Modules → Roc Interfaces
Python:
# user.py from dataclasses import dataclass @dataclass class User: id: int name: str email: str def create_user(name: str, email: str) -> User: return User(id=generate_id(), name=name, email=email) def get_name(user: User) -> str: return user.name
Roc:
# User.roc interface User exposes [User, createUser, getName] imports [] User : { id : I64, name : Str, email : Str, } createUser : Str, Str -> User createUser = \name, email -> { id: generateId(), name, email, } getName : User -> Str getName = \user -> user.name
Migration notes:
- Python modules → Roc interfaces (file-based modules)
- Python implicit exports → Roc explicit
exposes - Python imports → Roc
clauseimports
Build System
Python Project → Roc Application
Python (pyproject.toml):
[project] name = "myproject" version = "1.0.0" dependencies = [ "requests>=2.31.0", "pydantic>=2.0.0", ] [project.optional-dependencies] dev = [ "pytest>=7.4.0", "ruff>=0.1.0", ]
Roc:
# main.roc app [main] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br" } import pf.Task exposing [Task] import pf.Stdout main : Task {} [] main = Stdout.line!("Hello, Roc!")
Build commands:
# Python pip install . pip install .[dev] python -m myproject # Roc roc build main.roc roc run main.roc roc test main.roc
Key differences:
- Python uses pyproject.toml + pip/uv; Roc uses platform URLs
- Python has rich dependency ecosystem (PyPI); Roc uses platforms (more limited)
- Roc infers dependencies from imports; no separate manifest needed
Testing
pytest → expect
Python (pytest):
# test_math.py import pytest def add(a: int, b: int) -> int: return a + b def test_addition(): assert add(2, 2) == 4 def test_addition_negative(): assert add(-1, 1) == 0 @pytest.mark.parametrize("a,b,expected", [ (1, 1, 2), (2, 3, 5), (10, -5, 5), ]) def test_addition_parametrized(a, b, expected): assert add(a, b) == expected
Roc:
# math.roc add : I64, I64 -> I64 add = \a, b -> a + b # Inline tests expect add(2, 2) == 4 expect add(-1, 1) == 0 # Multiple test cases expect add(1, 1) == 2 expect add(2, 3) == 5 expect add(10, -5) == 5
Run tests:
# Python pytest # Roc roc test math.roc
Migration strategy:
- Convert pytest test functions to
statementsexpect - Place expects near the functions they test
- Parametrized tests become multiple
statementsexpect - Run with
roc test
Examples
Example 1: Simple - Optional Handling
Before (Python):
from typing import Optional def find_first_even(numbers: list[int]) -> Optional[int]: for n in numbers: if n % 2 == 0: return n return None numbers = [1, 3, 5, 6, 7, 8] result = find_first_even(numbers) if result is not None: print(f"Found: {result}") else: print("No even number found")
After (Roc):
findFirstEven : List I64 -> [Some I64, None] findFirstEven = \numbers -> when List.findFirst(numbers, \n -> n % 2 == 0) is Ok(n) -> Some(n) Err(_) -> None numbers = [1, 3, 5, 6, 7, 8] result = findFirstEven(numbers) when result is Some(n) -> Stdout.line!("Found: \(Num.toStr(n))") None -> Stdout.line!("No even number found")
Example 2: Medium - Result Error Handling with Chain
Before (Python):
from typing import Union def parse_int(s: str) -> Union[int, str]: try: return int(s) except ValueError: return f"Invalid number: {s}" def divide(a: int, b: int) -> Union[int, str]: if b == 0: return "Division by zero" return a // b def calculate(a_str: str, b_str: str) -> Union[int, str]: a = parse_int(a_str) if isinstance(a, str): # Error return a b = parse_int(b_str) if isinstance(b, str): # Error return b return divide(a, b) result = calculate("10", "2") if isinstance(result, int): print(f"Result: {result}") else: print(f"Error: {result}")
After (Roc):
CalcError : [ParseError Str, DivByZero] parseInt : Str -> Result I64 [ParseError Str] parseInt = \s -> Str.toI64(s) |> Result.mapErr(\_ -> ParseError("Invalid number: \(s)")) divide : I64, I64 -> Result I64 [DivByZero] divide = \a, b -> if b == 0 then Err(DivByZero) else Ok(a // b) calculate : Str, Str -> Result I64 CalcError calculate = \aStr, bStr -> a = parseInt!(aStr) b = parseInt!(bStr) divide!(a, b) main : Task {} [] main = when calculate("10", "2") is Ok(result) -> Stdout.line!("Result: \(Num.toStr(result))") Err(ParseError(msg)) -> Stdout.line!("Error: \(msg)") Err(DivByZero) -> Stdout.line!("Error: Division by zero")
Example 3: Complex - Async File Processing
Before (Python):
import asyncio from typing import List from pathlib import Path async def read_file(path: str) -> str: # Simulate async file read await asyncio.sleep(0.01) with open(path) as f: return f.read() async def process_line(line: str) -> str: # Simulate async processing await asyncio.sleep(0.01) return line.upper() async def process_file(input_path: str, output_path: str) -> None: # Read file content = await read_file(input_path) # Process lines concurrently lines = content.split('\n') tasks = [process_line(line) for line in lines] processed_lines = await asyncio.gather(*tasks) # Write result result = '\n'.join(processed_lines) with open(output_path, 'w') as f: f.write(result) print(f"Processed {len(lines)} lines") async def main(): try: await process_file("input.txt", "output.txt") except FileNotFoundError as e: print(f"File not found: {e}") except Exception as e: print(f"Error: {e}") if __name__ == "__main__": asyncio.run(main())
After (Roc):
app [main] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br" } import pf.File import pf.Path import pf.Task exposing [Task] import pf.Stdout # Pure function - process a line processLine : Str -> Str processLine = \line -> Str.toUpper(line) # Task-based file processing processFile : Str, Str -> Task {} [FileReadErr Path.ReadErr, FileWriteErr Path.WriteErr]* processFile = \inputPath, outputPath -> # Read file (Task) content = File.readUtf8!(Path.fromStr(inputPath)) # Process lines (pure) lines = Str.split(content, "\n") processedLines = List.map(lines, processLine) result = Str.joinWith(processedLines, "\n") # Write file (Task) File.writeUtf8!(Path.fromStr(outputPath), result) # Log completion Stdout.line!("Processed \(Num.toStr(List.len(lines))) lines") main : Task {} [] main = when processFile("input.txt", "output.txt") is Ok({}) -> Stdout.line!("Success!") Err(FileReadErr(_)) -> Stdout.line!("Error: Could not read file") Err(FileWriteErr(_)) -> Stdout.line!("Error: Could not write file")
Key conversions demonstrated:
- Python
→ Rocasync/await
withTask
operator! - Python
for concurrency → Roc uses pureasyncio.gather
(no async needed for CPU-bound processing)List.map - Python exception handling → Roc
with typed errorswhen ... is - Python mixes I/O and logic → Roc separates pure (
) from effectful (processLine
,File.read
)File.write
Limitations
Due to gaps in the
lang-roc-dev skill (6/8 pillars), external research and inference were used for:
Coverage Gaps
| Pillar | Python | Roc | Mitigation |
|---|---|---|---|
| Module | ✓ | ✓ | Both well-documented |
| Error | ✓ | ✓ | Result pattern clear |
| Concurrency | ✓ | ✓ | Task model documented |
| Metaprogramming | ~ | ✓ | Roc minimalist approach clear |
| Zero/Default | ✓ | ~ | Used tag union pattern |
| Serialization | ✓ | ~ | Inferred from abilities |
| Build | ✓ | ~ | Inferred from examples |
| Testing | ✓ | ✓ | Both covered |
| Dev Workflow | ✓ | ~ | Python REPL → Roc expect |
Combined Score: 14.5/18 (Good) - 8 pillars + dev workflow pillar
Known Limitations:
- Serialization: Roc Encode/Decode abilities exist but aren't fully documented in lang-roc-dev; patterns inferred from platform usage
- Build System: Roc build is simpler than Python's but emerging; limited packaging guidance
- Zero/Default: Roc handles via tag unions and pattern matching; no dedicated section in lang-roc-dev
External Resources Used
| Resource | What It Provided | Reliability |
|---|---|---|
| Roc Tutorial | Platform model, Task usage | High |
| lang-python-dev | Comprehensive Python patterns | High |
| lang-roc-dev | Type system, records, tags | High |
| convert-python-erlang | REPL → compiled workflow | High |
| convert-fsharp-roc | Functional → Roc patterns | High |
Tooling
| Tool | Purpose | Notes |
|---|---|---|
CLI | Build, run, test, format | All-in-one tool |
| Compile to binary | Fast incremental builds |
| Execute directly | Like |
| Run expect statements | Inline testing |
| Code formatting | Like or |
| Interactive shell | More limited than IPython |
| Watch mode | Rebuild on file changes |
| Roc LSP | Editor support | VS Code, vim integration |
See Also
For more examples and patterns, see:
- Foundational patterns with cross-language examplesmeta-convert-dev
- Python → Rust conversion for ownership focusconvert-python-rust
- Python → Erlang for BEAM runtime patternsconvert-python-erlang
- F# → Roc for .NET to native functional conversionconvert-fsharp-roc
- Python development patternslang-python-dev
- Roc development patternslang-roc-dev
Cross-cutting pattern skills:
- Async, Task, threading across languagespatterns-concurrency-dev
- JSON, validation across languagespatterns-serialization-dev
- Decorators, code generation across languagespatterns-metaprogramming-dev