| name | convert-scala-haskell |
| description | Convert Scala code to idiomatic Haskell. Use when migrating Scala projects to Haskell, translating Scala patterns to idiomatic Haskell, or refactoring Scala codebases. Extends meta-convert-dev with Scala-to-Haskell specific patterns. |
Convert Scala to Haskell
Convert Scala code to idiomatic Haskell. This skill extends meta-convert-dev with Scala-to-Haskell specific type mappings, idiom translations, and tooling.
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: Scala types → Haskell types
- Idiom translations: Scala patterns → idiomatic Haskell
- Error handling: Scala Option/Either/Try → Haskell Maybe/Either
- Async patterns: Scala Future/IO → Haskell IO/Async
- Lazy evaluation: Scala strict by default → Haskell lazy by default
- Type classes: Scala implicits/given → Haskell type classes
- Paradigm shift: JVM/strict/object-oriented → Pure FP/lazy/functional
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - Scala language fundamentals - see
lang-scala-dev - Haskell language fundamentals - see
lang-haskell-dev - Reverse conversion (Haskell → Scala) - see
convert-haskell-scala
Quick Reference
| Scala | Haskell | Notes |
|---|---|---|
String |
String |
Both are immutable strings |
Int |
Int |
32-bit integers |
BigInt |
Integer |
Arbitrary precision |
Double |
Double |
Floating point |
Boolean |
Bool |
Boolean values |
List[A] |
[a] |
Linked list |
(A, B) |
(a, b) |
Tuple |
Option[A] |
Maybe a |
Nullable values |
Either[A, B] |
Either a b |
Sum type for errors |
Future[A] / IO[A] |
IO a |
Side effects |
case class / sealed trait |
data |
ADTs |
trait + implicit/given |
class (type class) |
Type classes |
A => B |
a -> b |
Function types |
Monad[F] (Cats) |
Monad m |
Monads |
When Converting Code
- Analyze source thoroughly before writing target
- Map types first - create type equivalence table
- Preserve semantics over syntax similarity
- Adopt Haskell idioms - don't write "Scala code in Haskell syntax"
- Handle paradigm shifts - strict → lazy, JVM → pure FP, objects → data + functions
- Test equivalence - same inputs → same outputs
Type System Mapping
Primitive Types
| Scala | Haskell | Notes |
|---|---|---|
Int |
Int |
32-bit signed integer |
Long |
Int64 |
64-bit signed integer |
BigInt |
Integer |
Arbitrary precision integer |
Double |
Double |
64-bit floating point |
Float |
Float |
32-bit floating point |
Boolean |
Bool |
Boolean type |
Char |
Char |
Single character |
String |
String |
Immutable string (list of Char in Haskell) |
Unit |
() |
Unit type (void) |
Collection Types
| Scala | Haskell | Notes |
|---|---|---|
List[A] |
[a] |
Immutable linked list |
Vector[A] |
Vector a |
Indexed sequence (import Data.Vector) |
(A, B) |
(a, b) |
Tuple (Haskell supports larger tuples more naturally) |
(A, B, C) |
(a, b, c) |
3-tuple |
Map[K, V] |
Map k v |
Immutable map (import Data.Map) |
Set[A] |
Set a |
Immutable set (import Data.Set) |
Seq[A] |
Seq a |
Generic sequence (import Data.Sequence) |
Array[A] |
Array a |
Mutable array (import Data.Array) |
Composite Types
| Scala | Haskell | Notes |
|---|---|---|
sealed trait X; case object A extends X; case object B extends X |
data X = A | B |
Sum types |
case class X(field: Type) |
data X = X { field :: Type } |
Product types with named fields |
case class X(value: Type) extends AnyVal |
newtype X = X Type |
Zero-cost wrapper |
type X = Y |
type X = Y |
Type alias |
Option[A] |
Maybe a |
Optional values |
Either[A, B] |
Either a b |
Sum type (Left is error by convention in both) |
Try[A] |
Either SomeException a |
Exception handling |
Function Types
| Scala | Haskell | Notes |
|---|---|---|
A => B |
a -> b |
Function from A to B |
(A, B) => C |
a -> b -> c or (a, b) -> c |
Curried vs uncurried (currying is default in Haskell) |
A => B => C |
a -> b -> c |
Curried function (natural in Haskell) |
Future[A] / IO[A] (Cats Effect) |
IO a |
Effectful computation |
F[A] (with Monad[F]) |
m a (with Monad m) |
Higher-kinded types |
Idiom Translation
Pattern 1: Option/Maybe Handling
Scala:
def findUser(userId: String): Option[User] =
users.get(userId)
// Pattern matching
findUser("123") match {
case Some(user) => processUser(user)
case None => println("User not found")
}
// Option methods
findUser("123").getOrElse(defaultUser)
findUser("123").map(_.name).getOrElse("No user")
Haskell:
findUser :: String -> Maybe User
findUser userId = lookup userId users
-- Pattern matching
case findUser "123" of
Just user -> processUser user
Nothing -> putStrLn "User not found"
-- Maybe functions
fromMaybe defaultUser (findUser "123")
maybe "No user" userName (findUser "123")
Why this translation:
OptionandMaybeare semantically equivalent- Both use pattern matching for explicit handling
- Haskell's
fromMaybeis more concise than.getOrElse maybefunction takes extractor as argument (not method call)
Pattern 2: Either for Error Handling
Scala:
sealed trait AppError
case object NotFound extends AppError
case class ValidationError(message: String) extends AppError
def parseAge(str: String): Either[AppError, Int] = {
str.toIntOption match {
case Some(n) if n >= 0 => Right(n)
case Some(_) => Left(ValidationError("Age must be positive"))
case None => Left(ValidationError("Not a valid number"))
}
}
// Chaining with for-comprehension
def validateUser(ageStr: String, emailStr: String): Either[AppError, User] = {
for {
age <- parseAge(ageStr)
email <- validateEmail(emailStr)
} yield User(email, age)
}
Haskell:
data AppError = NotFound | ValidationError String
parseAge :: String -> Either AppError Int
parseAge str =
case reads str of
[(n, "")] -> if n >= 0
then Right n
else Left (ValidationError "Age must be positive")
_ -> Left (ValidationError "Not a valid number")
-- Chaining with do-notation
validateUser :: String -> String -> Either AppError User
validateUser ageStr emailStr = do
age <- parseAge ageStr
email <- validateEmail emailStr
return $ User email age
Why this translation:
- Both use Either with Left for errors, Right for success
- Scala's
for-comprehensionmaps to Haskell'sdo-notation - ADT error types work similarly in both languages
- Haskell requires explicit
returnat the end of do-notation - Scala's pattern matching on Option differs from Haskell's reads
Pattern 3: For-Comprehension to List Comprehension
Scala:
// For-comprehension
val squares = for {
x <- 1 to 10
if x % 2 == 0
} yield x * x
// With multiple generators
val pairs = for {
x <- 1 to 3
y <- 1 to 3
if x < y
} yield (x, y)
// Using map/filter (more idiomatic for simple cases)
val squares = (1 to 10).filter(_ % 2 == 0).map(x => x * x)
Haskell:
-- List comprehension
squares = [x^2 | x <- [1..10], even x]
-- With multiple generators
pairs = [(x, y) | x <- [1..3], y <- [1..3], x < y]
-- Nested comprehensions
matrix = [[1..n] | n <- [1..5]]
-- Using map/filter
squares = map (\x -> x^2) $ filter even [1..10]
Why this translation:
- Both desugar to map/flatMap/filter (or >>=)
- Haskell's list comprehension syntax is more concise
- Guards (filters) use
ifin Scala, just predicates in Haskell - Haskell comprehensions are natural for lists, Scala works with any monad
Pattern 4: Pattern Matching on ADTs
Scala:
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
case class Triangle(base: Double, height: Double, side: Double) extends Shape
def area(shape: Shape): Double = shape match {
case Circle(r) => math.Pi * r * r
case Rectangle(w, h) => w * h
case Triangle(b, h, _) => 0.5 * b * h
}
// Guards
def classify(n: Int): String = n match {
case n if n < 0 => "negative"
case 0 => "zero"
case _ => "positive"
}
Haskell:
data Shape = Circle Double
| Rectangle Double Double
| Triangle Double Double Double
area :: Shape -> Double
area (Circle r) = pi * r^2
area (Rectangle w h) = w * h
area (Triangle b h _) = 0.5 * b * h
-- Guards
classify :: Int -> String
classify n
| n < 0 = "negative"
| n == 0 = "zero"
| otherwise = "positive"
Why this translation:
- Scala uses case classes extending sealed trait, Haskell uses data constructors
- Pattern matching syntax is similar
- Guards use
ifin Scala match,|in Haskell function definitions - Haskell's pattern matching is built into function definitions, not just case expressions
Pattern 5: Type Classes from Implicits/Given
Scala (2.x with implicits):
trait Show[A] {
def show(a: A): String
}
object Show {
implicit val intShow: Show[Int] = (a: Int) => a.toString
implicit val stringShow: Show[String] = (a: String) => s"'$a'"
}
def print[A](a: A)(implicit s: Show[A]): Unit = {
println(s.show(a))
}
print(42) // "42"
print("hello") // "'hello'"
Scala (3.x with given/using):
trait Show[A] {
def show(a: A): String
}
given Show[Int] with {
def show(a: Int): String = a.toString
}
given Show[String] with {
def show(a: String): String = s"'$a'"
}
def print[A](a: A)(using s: Show[A]): Unit = {
println(s.show(a))
}
Haskell:
class Show a where
show :: a -> String
instance Show Int where
show = Prelude.show -- Using Prelude's show
instance Show String where
show s = "'" ++ s ++ "'"
print :: Show a => a -> IO ()
print a = putStrLn (show a)
Why this translation:
- Scala's implicit/given parameters map to Haskell's type class constraints
- Scala's trait + implicit instances = Haskell's type class + instances
- Haskell's type class syntax is cleaner and more established
- Both resolve instances at compile time
- Haskell's instance resolution is simpler (no implicit scope complexity)
Pattern 6: Monadic Composition
Scala:
// For-comprehension with Option
val result = for {
x <- Some(1)
y <- Some(2)
z <- Some(3)
} yield x + y + z
// Desugars to
val result = Some(1).flatMap { x =>
Some(2).flatMap { y =>
Some(3).map { z =>
x + y + z
}
}
}
// With Future
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
val futureResult = for {
a <- fetchDataA()
b <- fetchDataB(a)
c <- fetchDataC(b)
} yield c
Haskell:
-- Do-notation with Maybe
result = do
x <- Just 1
y <- Just 2
z <- Just 3
return $ x + y + z
-- Desugars to
result = Just 1 >>= \x ->
Just 2 >>= \y ->
Just 3 >>= \z ->
return (x + y + z)
-- With IO
ioResult :: IO Result
ioResult = do
a <- fetchDataA
b <- fetchDataB a
c <- fetchDataC b
return c
Why this translation:
- Scala's
for-comprehension= Haskell'sdo-notation - Both desugar to
flatMap/>>=andmap/fmap - Scala's
yield= Haskell'sreturn(for final value) - Haskell's do-notation works with any Monad
- No execution context needed in Haskell (purity by default)
Pattern 7: Lazy Evaluation
Scala (strict by default):
// Eager evaluation
val expensiveValue = computeExpensive() // Computed immediately
// Lazy val
lazy val lazyValue = computeExpensive() // Computed on first access
// By-name parameter (re-evaluated each time)
def repeat(n: Int)(action: => Unit): Unit = {
(1 to n).foreach(_ => action)
}
// Stream (lazy list) - Scala 2.x
val stream = Stream.from(1)
val first10 = stream.take(10).toList
// LazyList - Scala 3.x
val lazyList = LazyList.from(1)
val first10 = lazyList.take(10).toList
Haskell (lazy by default):
-- Lazy evaluation (default)
expensiveValue = computeExpensive -- Not computed until needed
-- Strict evaluation (with BangPatterns or seq)
strictValue = computeExpensive `seq` value
-- Infinite lists (natural due to laziness)
ones = 1 : ones
naturals = [1..]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
-- Take from infinite list
first10 = take 10 naturals -- [1,2,3,4,5,6,7,8,9,10]
first10Fibs = take 10 fibs -- [0,1,1,2,3,5,8,13,21,34]
Why this translation:
- Scala is strict by default, Haskell is lazy by default
- Scala's
lazy val= Haskell's default behavior - Scala's
Stream/LazyList= Haskell's regular lists (all lazy) - Haskell makes infinite data structures natural
- Performance: lazy can avoid unnecessary computation, but can cause space leaks
Paradigm Translation
Mental Model Shift: JVM/Strict/OOP → Pure FP/Lazy/Functional
| Scala Concept | Haskell Approach | Key Insight |
|---|---|---|
| Class with mutable state | Record + pure functions | Data and behavior separated, no mutation |
| Object (singleton) | Module-level definitions | No special singleton syntax needed |
| Inheritance | Type classes / ADTs | Favor type classes and composition |
| Mutable loops | Recursion / fold / map | Transformation over mutation |
| Side effects (println, etc.) | IO monad | Effects segregated in IO type |
| Strict evaluation | Lazy evaluation | Evaluation delayed until needed |
| var (mutable variable) | Pure functions with recursion | No mutation, thread parameters |
| null | Maybe | Explicit optional values |
Object-Oriented to Functional
Scala (OOP style):
class BankAccount(private var balance: Double) {
def deposit(amount: Double): Unit = {
balance += amount
}
def withdraw(amount: Double): Boolean = {
if (balance >= amount) {
balance -= amount
true
} else {
false
}
}
def getBalance: Double = balance
}
val account = new BankAccount(100.0)
account.deposit(50.0)
account.withdraw(30.0)
println(account.getBalance) // 120.0
Haskell (FP style):
data BankAccount = BankAccount { balance :: Double }
deriving (Show)
deposit :: Double -> BankAccount -> BankAccount
deposit amount account = account { balance = balance account + amount }
withdraw :: Double -> BankAccount -> Maybe BankAccount
withdraw amount account =
if balance account >= amount
then Just $ account { balance = balance account - amount }
else Nothing
-- Usage (threading state)
initialAccount = BankAccount 100.0
afterDeposit = deposit 50.0 initialAccount
afterWithdraw = case withdraw 30.0 afterDeposit of
Just acc -> acc
Nothing -> afterDeposit -- Withdrawal failed, keep previous state
Why this translation:
- Scala's mutable object → Haskell's immutable data + pure functions
- State changes → new values
- Methods → pure functions taking state as parameter
- Failed operations → Maybe to indicate success/failure
- No side effects in Haskell version (pure)
Concurrency Mental Model
| Scala Model | Haskell Model | Conceptual Translation |
|---|---|---|
| Future + ExecutionContext | IO + async | Async computation in IO |
| Promise | MVar / IORef | Mutable reference in IO |
| Akka Actors | STM / async | Message passing → software transactional memory |
| Parallel collections | Par monad / Strategies | Explicit parallelism markers |
| synchronized / locks | STM transactions | Lock-free concurrent updates |
Error Handling
Scala Error Model → Haskell Error Model
Scala has three main approaches:
- Option - Value may be absent (no error context)
- Either - Value or error with context
- Try - Catching exceptions
Haskell approaches:
- Maybe - Value may be absent (no error context)
- Either - Value or error with context
- ExceptT/MonadError - Exception-like behavior in pure code
Mapping:
| Scala | Haskell | Use Case |
|---|---|---|
Option[A] |
Maybe a |
Nullable values, simple absence |
Either[E, A] |
Either e a |
Errors with context |
Try[A] |
Either SomeException a |
Exception handling |
IO[A] |
IO a |
Side effects (can throw) |
Future[A] |
IO a / async |
Async operations |
Exception handling:
Scala:
import scala.util.{Try, Success, Failure}
def parseInt(s: String): Try[Int] = Try(s.toInt)
parseInt("123") match {
case Success(n) => println(s"Parsed: $n")
case Failure(e) => println(s"Error: ${e.getMessage}")
}
// Converting to Either
val eitherResult: Either[Throwable, Int] = parseInt("123").toEither
Haskell:
import Text.Read (readMaybe)
import Control.Exception (try, SomeException)
-- Pure version with Maybe
parseInt :: String -> Maybe Int
parseInt = readMaybe
case parseInt "123" of
Just n -> putStrLn $ "Parsed: " ++ show n
Nothing -> putStrLn "Error: not a valid number"
-- IO version catching exceptions
parseIntIO :: String -> IO (Either SomeException Int)
parseIntIO s = try $ return (read s)
Why this translation:
Tryin Scala ≈MaybeorEither SomeExceptionin Haskell- Haskell prefers pure error handling with Maybe/Either
- IO exceptions are segregated to IO monad
- Use
readMaybefor safe parsing (pure) - Use
tryfor catching IO exceptions
Concurrency Patterns
Scala Future → Haskell IO/Async
Scala:
import scala.concurrent.{Future, Await}
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
// Creating futures
val future1 = Future { expensiveComputation() }
val future2 = Future { anotherComputation() }
// Combining futures
val combined = for {
result1 <- future1
result2 <- future2
} yield result1 + result2
// Blocking (avoid in production)
val result = Await.result(combined, 5.seconds)
// Callbacks
future1.onComplete {
case Success(value) => println(s"Got: $value")
case Failure(error) => println(s"Failed: $error")
}
Haskell:
import Control.Concurrent.Async
-- Creating async computations
main = do
async1 <- async expensiveComputation
async2 <- async anotherComputation
-- Waiting for results
result1 <- wait async1
result2 <- wait async2
let combined = result1 + result2
print combined
-- Or use concurrently
main = do
(result1, result2) <- concurrently expensiveComputation anotherComputation
print (result1 + result2)
-- Race (first to complete)
result <- race computation1 computation2
case result of
Left a -> putStrLn $ "Left won: " ++ show a
Right b -> putStrLn $ "Right won: " ++ show b
Why this translation:
- Scala's
Future≈ Haskell'sIOwithasync - No execution context needed in Haskell (simpler model)
for-comprehensionon Future →do-notationon IO- Haskell's
asynclibrary provides similar abstractions - Haskell's concurrency is lighter weight (green threads)
Scala Cats Effect IO → Haskell IO
Scala (Cats Effect):
import cats.effect._
val program = for {
_ <- IO.println("What's your name?")
name <- IO.readLine
_ <- IO.println(s"Hello, $name!")
} yield ()
// Parallel execution
import cats.effect.syntax.parallel._
val parallel = (
fetchUser(1),
fetchUser(2),
fetchUser(3)
).parMapN((u1, u2, u3) => List(u1, u2, u3))
// Resource management
def useFile(path: String): IO[String] = {
Resource.make(
IO(scala.io.Source.fromFile(path))
)(source => IO(source.close()))
.use(source => IO(source.mkString))
}
Haskell:
import System.IO
program :: IO ()
program = do
putStrLn "What's your name?"
name <- getLine
putStrLn $ "Hello, " ++ name ++ "!"
-- Parallel execution with async
import Control.Concurrent.Async
parallel :: IO [User]
parallel = mapConcurrently fetchUser [1, 2, 3]
-- Resource management with bracket
useFile :: FilePath -> IO String
useFile path = bracket
(openFile path ReadMode) -- Acquire
hClose -- Release
hGetContents -- Use
Why this translation:
- Cats Effect IO ≈ Haskell's IO monad
- Both provide referentially transparent effects
- Haskell's IO is language-level, not a library
Resource.make→bracketfor resource safety- Parallel execution: Cats parMapN → async library
Memory & Ownership
Scala GC → Haskell GC
Both Scala and Haskell use garbage collection, so memory management translation is relatively straightforward:
| Aspect | Scala (JVM) | Haskell (GHC) |
|---|---|---|
| Memory model | Heap allocation, GC | Heap allocation, GC |
| Reference types | Objects on heap | Thunks and values on heap |
| Immutability | Opt-in (val, immutable collections) |
Default (all values immutable) |
| Space leaks | Rare (eager evaluation) | Possible (lazy evaluation, retained thunks) |
| Optimization | JVM JIT compilation | GHC strictness analysis, inlining |
Key differences:
- Laziness can cause space leaks in Haskell:
Problem:
-- Space leak: foldl builds large thunk
sum = foldl (+) 0 [1..1000000] -- BAD: stack overflow
-- Fix: use strict foldl'
import Data.List (foldl')
sum = foldl' (+) 0 [1..1000000] -- GOOD: tail recursive + strict
- Scala's strictness is safer for beginners:
// No space leak - eager evaluation
val sum = (1 to 1000000).foldLeft(0)(_ + _)
When converting:
- Watch for space leaks caused by lazy evaluation
- Use strict functions (
foldl',seq, BangPatterns) when needed - Profile memory usage for large data processing
Common Pitfalls
Laziness vs. Strictness
- Pitfall: Scala's strict evaluation → Haskell's lazy evaluation can cause unexpected behavior
- Example: In Scala,
val x = heavyComputation()runs immediately; in Haskell,x = heavyComputationdelays until used - Solution: Understand when evaluation happens; use strictness annotations if needed
Type Class Orphan Instances
- Pitfall: Scala allows defining implicits anywhere; Haskell forbids orphan instances
- Example: In Scala, you can define
implicit val show: Show[ThirdPartyType]in any file; Haskell requires instances in the module defining the type or the class - Solution: Use newtype wrappers for orphan instances in Haskell
Null vs. Maybe
- Pitfall: Scala can have null values (Java interop); Haskell has no null
- Example: Scala's
Option(nullableValue)wraps potential null; Haskell has no null concept - Solution: Always use Maybe for optional values in Haskell; no null checks needed
Mutable State
- Pitfall: Scala allows
varand mutable collections; Haskell has no mutation outside IO - Example: Scala's
var counter = 0; counter += 1; Haskell needs State monad or IORef - Solution: Rewrite using recursion, State monad, or IORef for true mutability
- Pitfall: Scala allows
Type Inference Differences
- Pitfall: Scala's type inference is more aggressive; Haskell sometimes needs annotations
- Example: Polymorphic recursion often needs type signatures in Haskell
- Solution: Add type signatures to top-level definitions (good practice anyway)
List Performance
- Pitfall: Scala's List and Haskell's list have different performance characteristics due to strictness
- Example: Scala's
List(1, 2, 3).lastis O(n); Haskell'slast [1, 2, 3]is also O(n), but laziness may help in some cases - Solution: Use appropriate data structures (Vector, Seq) for different access patterns
String Type
- Pitfall: Scala's String is Java String; Haskell's String is
[Char](linked list) - Example: Haskell String can be slow for large text processing
- Solution: Use Text or ByteString for performance-critical string operations in Haskell
- Pitfall: Scala's String is Java String; Haskell's String is
Type Class Constraints
- Pitfall: Scala's implicit resolution is complex; Haskell's is simpler but more restrictive
- Example: Scala allows multiple implicit parameters of same type; Haskell doesn't
- Solution: Use newtype wrappers to disambiguate type class instances
Limitations
Coverage Gaps
| Pillar | Source Skill (lang-scala-dev) | Target Skill (lang-haskell-dev) | Mitigation |
|---|---|---|---|
| Module | ~ (package objects) | ✓ (Module System) | Reference lang-haskell-dev |
| Serialization | ✗ (not covered) | ✓ (Serialization) | Reference lang-haskell-dev for Aeson patterns |
Known Limitations
- Module System: Scala's package objects and imports differ from Haskell's module system. Conversion patterns for module organization may be incomplete.
- Serialization: Scala's serialization patterns (Play JSON, Circe) have limited coverage in lang-scala-dev. Reference Haskell's Aeson library directly.
External Resources
For areas with limited coverage, consult:
- Haskell module system: https://wiki.haskell.org/Module
- Aeson (JSON): https://hackage.haskell.org/package/aeson
Tooling
| Tool | Purpose | Notes |
|---|---|---|
| Haskell Language Server (HLS) | IDE support | Type hints, refactoring, linting |
| GHC | Compiler | Glasgow Haskell Compiler |
| Cabal | Build tool | Package management, dependencies |
| Stack | Build tool | Curated package sets, reproducible builds |
| Hoogle | API search | Search by type signature |
| hlint | Linter | Suggests idiomatic Haskell improvements |
| ormolu / brittany | Formatter | Auto-format Haskell code |
| ghcid | File watcher | Fast reload on file changes |
Scala → Haskell tooling equivalents:
| Scala Tool | Haskell Equivalent | Notes |
|---|---|---|
| sbt | Cabal / Stack | Build and dependency management |
| IntelliJ IDEA | VSCode + HLS | IDE support |
| Scalafmt | Ormolu / Brittany | Code formatting |
| Scalafix | hlint | Linting and refactoring |
| Metals | HLS | Language server |
Examples
Example 1: Simple - Option/Maybe
Before (Scala):
def findUserById(id: Int, users: List[User]): Option[User] = {
users.find(_.id == id)
}
val user = findUserById(123, allUsers)
val name = user.map(_.name).getOrElse("Unknown")
println(name)
After (Haskell):
data User = User { userId :: Int, userName :: String }
findUserById :: Int -> [User] -> Maybe User
findUserById targetId users = find (\u -> userId u == targetId) users
main :: IO ()
main = do
let user = findUserById 123 allUsers
let name = maybe "Unknown" userName user
putStrLn name
Example 2: Medium - Either Error Handling with Chaining
Before (Scala):
case class User(email: String, age: Int)
sealed trait ValidationError
case class InvalidEmail(msg: String) extends ValidationError
case class InvalidAge(msg: String) extends ValidationError
def validateEmail(email: String): Either[ValidationError, String] = {
if (email.contains("@")) Right(email)
else Left(InvalidEmail("Email must contain @"))
}
def validateAge(age: Int): Either[ValidationError, Int] = {
if (age >= 0 && age <= 150) Right(age)
else Left(InvalidAge("Age must be between 0 and 150"))
}
def createUser(email: String, age: Int): Either[ValidationError, User] = {
for {
validEmail <- validateEmail(email)
validAge <- validateAge(age)
} yield User(validEmail, validAge)
}
// Usage
createUser("alice@example.com", 30) match {
case Right(user) => println(s"Created user: $user")
case Left(error) => println(s"Validation failed: $error")
}
After (Haskell):
data User = User { email :: String, age :: Int }
deriving (Show)
data ValidationError
= InvalidEmail String
| InvalidAge String
deriving (Show)
validateEmail :: String -> Either ValidationError String
validateEmail email =
if '@' `elem` email
then Right email
else Left (InvalidEmail "Email must contain @")
validateAge :: Int -> Either ValidationError Int
validateAge age =
if age >= 0 && age <= 150
then Right age
else Left (InvalidAge "Age must be between 0 and 150")
createUser :: String -> Int -> Either ValidationError User
createUser emailStr ageVal = do
validEmail <- validateEmail emailStr
validAge <- validateAge ageVal
return $ User validEmail validAge
-- Usage
main :: IO ()
main = do
case createUser "alice@example.com" 30 of
Right user -> putStrLn $ "Created user: " ++ show user
Left err -> putStrLn $ "Validation failed: " ++ show err
Example 3: Complex - Async HTTP Client with Error Handling
Before (Scala):
import scala.concurrent.{Future, ExecutionContext}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Try, Success, Failure}
case class ApiResponse(data: String, statusCode: Int)
case class User(id: Int, name: String, email: String)
sealed trait ApiError
case class NetworkError(msg: String) extends ApiError
case class ParseError(msg: String) extends ApiError
case object NotFound extends ApiError
class HttpClient {
def get(url: String): Future[ApiResponse] = Future {
// Simulated HTTP call
if (url.contains("users/123"))
ApiResponse("""{"id":123,"name":"Alice","email":"alice@example.com"}""", 200)
else
ApiResponse("", 404)
}
}
def parseUser(response: ApiResponse): Either[ApiError, User] = {
if (response.statusCode == 404) Left(NotFound)
else {
Try {
// Simplified parsing (in real code, use circe/play-json)
val pattern = """.*"id":(\d+).*"name":"([^"]+)".*"email":"([^"]+)".*""".r
response.data match {
case pattern(id, name, email) => User(id.toInt, name, email)
}
}.toEither.left.map(e => ParseError(e.getMessage))
}
}
def fetchUser(userId: Int)(implicit ec: ExecutionContext): Future[Either[ApiError, User]] = {
val client = new HttpClient
client.get(s"https://api.example.com/users/$userId")
.map(parseUser)
.recover {
case e: Exception => Left(NetworkError(e.getMessage))
}
}
// Usage
def main(args: Array[String]): Unit = {
import scala.concurrent.Await
import scala.concurrent.duration._
val result = Await.result(fetchUser(123), 5.seconds)
result match {
case Right(user) => println(s"Found user: ${user.name}")
case Left(NotFound) => println("User not found")
case Left(NetworkError(msg)) => println(s"Network error: $msg")
case Left(ParseError(msg)) => println(s"Parse error: $msg")
}
}
After (Haskell):
{-# LANGUAGE OverloadedStrings #-}
import Control.Exception (try, SomeException)
import Data.Aeson (FromJSON, parseJSON, withObject, (.:), eitherDecode)
import qualified Data.ByteString.Lazy.Char8 as BL
import Network.HTTP.Simple
( httpLBS, getResponseBody, getResponseStatusCode
, parseRequest, Response )
data User = User
{ userId :: Int
, userName :: String
, userEmail :: String
} deriving (Show)
instance FromJSON User where
parseJSON = withObject "User" $ \v -> User
<$> v .: "id"
<*> v .: "name"
<*> v .: "email"
data ApiError
= NetworkError String
| ParseError String
| NotFound
deriving (Show)
-- Fetch user with error handling
fetchUser :: Int -> IO (Either ApiError User)
fetchUser uid = do
result <- try $ do
request <- parseRequest $ "https://api.example.com/users/" ++ show uid
response <- httpLBS request
let statusCode = getResponseStatusCode response
let body = getResponseBody response
if statusCode == 404
then return $ Left NotFound
else case eitherDecode body of
Right user -> return $ Right user
Left err -> return $ Left (ParseError err)
case result of
Left (e :: SomeException) -> return $ Left (NetworkError (show e))
Right userOrError -> return userOrError
-- Usage
main :: IO ()
main = do
result <- fetchUser 123
case result of
Right user -> putStrLn $ "Found user: " ++ userName user
Left NotFound -> putStrLn "User not found"
Left (NetworkError msg) -> putStrLn $ "Network error: " ++ msg
Left (ParseError msg) -> putStrLn $ "Parse error: " ++ msg
Key translation points:
- Scala Future → Haskell IO (no execution context needed)
- Scala circe/play-json → Haskell Aeson
- Pattern matching on sealed traits → pattern matching on ADTs
- Exception handling with try/recover → Control.Exception.try
- Both versions segregate IO effects and use Either for domain errors
See Also
For more examples and patterns, see:
meta-convert-dev- Foundational patterns with cross-language examplesconvert-haskell-scala- Reverse conversion (Haskell → Scala)lang-scala-dev- Scala development patternslang-haskell-dev- Haskell development patterns
Cross-cutting pattern skills (for areas not fully covered by lang-*-dev):
patterns-concurrency-dev- Async, threads, STM across languagespatterns-serialization-dev- JSON, validation across languagespatterns-metaprogramming-dev- Template Haskell, Scala macros comparison