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 examples - •
convert-haskell-scala- Reverse conversion (Haskell → Scala) - •
lang-scala-dev- Scala development patterns - •
lang-haskell-dev- Haskell development patterns
Cross-cutting pattern skills (for areas not fully covered by lang-*-dev):
- •
patterns-concurrency-dev- Async, threads, STM across languages - •
patterns-serialization-dev- JSON, validation across languages - •
patterns-metaprogramming-dev- Template Haskell, Scala macros comparison