| name | convert-haskell-scala |
| description | Convert Haskell code to idiomatic Scala. Use when migrating Haskell projects to Scala, translating Haskell patterns to idiomatic Scala, or refactoring Haskell codebases. Extends meta-convert-dev with Haskell-to-Scala specific patterns. |
Convert Haskell to Scala
Convert Haskell code to idiomatic Scala. This skill extends meta-convert-dev with Haskell-to-Scala 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: Haskell types → Scala types
- Idiom translations: Haskell patterns → idiomatic Scala
- Error handling: Haskell Maybe/Either → Scala Option/Either
- Async patterns: Haskell IO/Async → Scala Future/IO
- Lazy evaluation: Haskell lazy by default → Scala strict with lazy vals
- Type classes: Haskell type classes → Scala implicits/given
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - Haskell language fundamentals - see
lang-haskell-dev - Scala language fundamentals - see
lang-scala-dev - Reverse conversion (Scala → Haskell) - see
convert-scala-haskell
Quick Reference
| Haskell | Scala | Notes |
|---|---|---|
String |
String |
Both are immutable strings |
Int |
Int |
32-bit integers |
Integer |
BigInt |
Arbitrary precision |
Double |
Double |
Floating point |
Bool |
Boolean |
Boolean values |
[a] |
List[A] |
Linked list |
(a, b) |
(A, B) |
Tuple |
Maybe a |
Option[A] |
Nullable values |
Either a b |
Either[A, B] |
Sum type for errors |
IO a |
IO[A] (Cats Effect) |
Side effects |
data |
case class / sealed trait |
ADTs |
class (type class) |
trait + implicit/given |
Type classes |
-> (function type) |
=> (function type) |
Function types |
Monad |
Monad (Cats) |
Requires Cats library |
When Converting Code
- Analyze source thoroughly before writing target
- Map types first - create type equivalence table
- Preserve semantics over syntax similarity
- Adopt Scala idioms - don't write "Haskell code in Scala syntax"
- Handle edge cases - lazy evaluation, null safety, JVM interop
- Test equivalence - same inputs → same outputs
Type System Mapping
Primitive Types
| Haskell | Scala | Notes |
|---|---|---|
Int |
Int |
32-bit signed integer |
Integer |
BigInt |
Arbitrary precision integer |
Double |
Double |
64-bit floating point |
Float |
Float |
32-bit floating point |
Bool |
Boolean |
Boolean type |
Char |
Char |
Single character |
String |
String |
Immutable string |
() |
Unit |
Unit type (void) |
Collection Types
| Haskell | Scala | Notes |
|---|---|---|
[a] |
List[A] |
Immutable linked list |
(a, b) |
(A, B) |
Tuple (up to 22 elements in Scala) |
(a, b, c) |
(A, B, C) |
3-tuple |
Map k v |
Map[K, V] |
Immutable map |
Set a |
Set[A] |
Immutable set |
Vector a |
Vector[A] |
Indexed sequence |
Seq a |
Seq[A] |
Generic sequence |
Composite Types
| Haskell | Scala | Notes |
|---|---|---|
data X = A | B |
sealed trait X; case object A extends X; case object B extends X |
Sum types |
data X = X { field :: Type } |
case class X(field: Type) |
Product types with named fields |
newtype X = X Type |
case class X(value: Type) extends AnyVal |
Zero-cost wrapper |
type X = Y |
type X = Y |
Type alias |
Maybe a |
Option[A] |
Optional values |
Either a b |
Either[A, B] |
Sum type (Left is error by convention in both) |
Function Types
| Haskell | Scala | Notes |
|---|---|---|
a -> b |
A => B |
Function from A to B |
a -> b -> c |
A => B => C or (A, B) => C |
Curried vs uncurried |
IO a |
IO[A] (Cats Effect) |
Effectful computation |
Monad m => m a |
F[A] (with Monad[F]) |
Higher-kinded types |
Idiom Translation
Pattern 1: Maybe/Option Handling
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")
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")
Why this translation:
MaybeandOptionare semantically equivalent- Both use pattern matching for explicit handling
- Scala's
.getOrElseis more concise thanfromMaybe - Method chaining is idiomatic in Scala
Pattern 2: Either for Error Handling
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
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)
}
Why this translation:
- Both use Either with Left for errors, Right for success
- Haskell's
do-notationmaps to Scala'sfor-comprehension - ADT error types work similarly in both languages
- Scala requires explicit
yieldat the end of for-comprehension
Pattern 3: List Comprehensions
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]]
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)
// Nested
val matrix = (1 to 5).map(n => (1 to n).toList)
Why this translation:
- Both desugar to map/flatMap/filter
- Scala's for-comprehension requires
yieldkeyword - Guards (filters) use
ifin both - Scala often prefers method chaining for simple transformations
Pattern 4: Pattern Matching on ADTs
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"
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"
}
Why this translation:
- Haskell uses data constructors, Scala uses case classes
- Pattern matching syntax is similar
- Guards use
ifin Scala match,|in Haskell function definitions sealed traitensures exhaustiveness checking like Haskell
Pattern 5: Type Classes to Implicits/Given
Haskell:
class Show a where
show :: a -> String
instance Show Int where
show = Prelude.show
instance Show User where
show (User name age) = name ++ " (" ++ Prelude.show age ++ ")"
printValue :: Show a => a -> IO ()
printValue x = putStrLn (show x)
Scala 2 (implicits):
trait Show[A] {
def show(a: A): String
}
object Show {
implicit val intShow: Show[Int] = new Show[Int] {
def show(a: Int): String = a.toString
}
implicit val userShow: Show[User] = new Show[User] {
def show(user: User): String = s"${user.name} (${user.age})"
}
}
def printValue[A](x: A)(implicit s: Show[A]): Unit = {
println(s.show(x))
}
Scala 3 (given/using):
trait Show[A] {
def show(a: A): String
}
given Show[Int] with {
def show(a: Int): String = a.toString
}
given Show[User] with {
def show(user: User): String = s"${user.name} (${user.age})"
}
def printValue[A](x: A)(using s: Show[A]): Unit = {
println(s.show(x))
}
Why this translation:
- Type classes map to traits with implicit/given instances
- Constraints become implicit/using parameters
- Scala 3 syntax is closer to Haskell's
- Both support type class derivation (Haskell with deriving, Scala with macros)
Pattern 6: Lazy Evaluation
Haskell:
-- Infinite lists (lazy by default)
naturals = [1..]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
-- Take first 10
take 10 naturals
take 10 fibs
-- Lazy evaluation of expressions
expensiveComputation = trace "Computing..." (sum [1..1000000])
result = if condition then expensiveComputation else 0 -- Only computed if condition is True
Scala:
// LazyList (Stream in Scala 2.12)
val naturals = LazyList.from(1)
val fibs: LazyList[Int] = 0 #:: 1 #:: fibs.zip(fibs.tail).map { case (a, b) => a + b }
// Take first 10
naturals.take(10).toList
fibs.take(10).toList
// Lazy val for deferred evaluation
lazy val expensiveComputation = {
println("Computing...")
(1 to 1000000).sum
}
val result = if (condition) expensiveComputation else 0 // Only computed if condition is true
// By-name parameters for lazy arguments
def ifThenElse[A](cond: Boolean)(thenBranch: => A)(elseBranch: => A): A = {
if (cond) thenBranch else elseBranch
}
Why this translation:
- Haskell is lazy by default, Scala is strict by default
- Use
LazyListfor infinite sequences - Use
lazy valfor deferred computation - By-name parameters (
=> A) for lazy function arguments - Scala 2.13+ renamed Stream to LazyList
Pattern 7: Function Composition and Application
Haskell:
-- Function composition
addThenDouble = (*2) . (+1)
process = filter even . map (*2) . filter (>0)
-- Function application
result = f $ g $ h x -- Equivalent to f (g (h x))
-- Point-free style
sumOfSquares = sum . map (^2)
Scala:
// Function composition
val addThenDouble = ((x: Int) => x + 1) andThen (_ * 2)
val addThenDouble2 = ((x: Int) => x * 2) compose ((x: Int) => x + 1)
// Method chaining (more idiomatic)
def process(list: List[Int]): List[Int] =
list.filter(_ > 0).map(_ * 2).filter(_ % 2 == 0)
// Infix notation for single-arg methods
val result = f(g(h(x))) // No special operator needed
// Function style with compose
val sumOfSquares = ((list: List[Int]) => list.map(x => x * x)).andThen(_.sum)
// More idiomatic Scala
def sumOfSquares(list: List[Int]): Int = list.map(x => x * x).sum
Why this translation:
- Haskell's
.maps tocompose(right-to-left) orandThen(left-to-right) - Scala prefers method chaining over function composition
- Haskell's
$isn't needed in Scala (no precedence issues) - Point-free style less common in Scala
Pattern 8: Monadic Composition
Haskell:
-- Do-notation
computation :: IO ()
computation = do
putStrLn "What's your name?"
name <- getLine
putStrLn $ "Hello, " ++ name
-- Maybe monad
safeDivision :: Maybe Int
safeDivision = do
a <- Just 10
b <- Just 2
result <- Just (div a b)
return (result * 2)
-- List monad
pairs :: [(Int, Int)]
pairs = do
x <- [1..3]
y <- [1..3]
guard (x < y)
return (x, y)
Scala:
// For-comprehension with IO (Cats Effect)
import cats.effect.IO
val computation: IO[Unit] = for {
_ <- IO.println("What's your name?")
name <- IO.readLine
_ <- IO.println(s"Hello, $name")
} yield ()
// Option monad
val safeDivision: Option[Int] = for {
a <- Some(10)
b <- Some(2)
result <- Some(a / b)
} yield result * 2
// List monad
val pairs: List[(Int, Int)] = for {
x <- (1 to 3).toList
y <- (1 to 3).toList
if x < y
} yield (x, y)
Why this translation:
- Haskell's
do-notationmaps directly to Scala'sfor-comprehension - Both desugar to flatMap/map
- Haskell uses
return, Scala usesyield guardbecomesifin for-comprehension- IO monad requires Cats Effect library in Scala
Error Handling
Haskell Maybe/Either → Scala Option/Either
| Haskell Pattern | Scala Equivalent | Notes |
|---|---|---|
Nothing |
None |
Absence of value |
Just x |
Some(x) |
Present value |
Left err |
Left(err) |
Error case |
Right val |
Right(val) |
Success case |
fromMaybe default |
.getOrElse(default) |
Provide default |
maybe defaultVal f |
.map(f).getOrElse(defaultVal) |
Map with default |
either errorHandler successHandler |
.fold(errorHandler, successHandler) |
Fold both cases |
Exception Handling
Haskell:
import Control.Exception
readFileSafe :: FilePath -> IO (Either IOException String)
readFileSafe path = try $ readFile path
-- Using try/catch
processFile :: FilePath -> IO ()
processFile path = do
result <- try (readFile path) :: IO (Either IOException String)
case result of
Left ex -> putStrLn $ "Error: " ++ show ex
Right content -> putStrLn content
Scala:
import scala.util.{Try, Success, Failure}
import java.io.IOException
def readFileSafe(path: String): Either[IOException, String] = {
Try(scala.io.Source.fromFile(path).mkString).toEither match {
case Right(content) => Right(content)
case Left(ex: IOException) => Left(ex)
case Left(ex) => Left(new IOException(ex))
}
}
// Using Try
def processFile(path: String): Unit = {
Try(scala.io.Source.fromFile(path).mkString) match {
case Success(content) => println(content)
case Failure(ex) => println(s"Error: ${ex.getMessage}")
}
}
Why this translation:
- Haskell's
tryfrom Control.Exception maps to Scala'sTry - Both can convert to Either for type-safe error handling
- Scala has nullable types from Java, so be careful with interop
- Use Try for exception handling, Either for domain errors
Concurrency Model
Haskell IO/Async → Scala Future/IO
| Haskell | Scala | Library |
|---|---|---|
IO a |
Future[A] |
scala.concurrent |
IO a |
IO[A] |
cats-effect |
async |
Future { ... } |
scala.concurrent |
forkIO |
Future { ... } |
scala.concurrent |
Async |
Async[F] |
cats-effect |
concurrently |
Future.sequence |
scala.concurrent |
race |
Future.firstCompletedOf |
scala.concurrent |
Basic Async Translation
Haskell:
import Control.Concurrent.Async
fetchData :: IO String
fetchData = do
threadDelay 1000000
return "data"
main :: IO ()
main = do
result <- fetchData
putStrLn result
-- Concurrent execution
main :: IO ()
main = do
(data1, data2) <- concurrently fetchData1 fetchData2
putStrLn $ data1 ++ data2
Scala:
import scala.concurrent.{Future, Await}
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
def fetchData: Future[String] = Future {
Thread.sleep(1000)
"data"
}
def main(): Unit = {
val result = Await.result(fetchData, 5.seconds)
println(result)
}
// Concurrent execution
def main(): Unit = {
val combined = for {
data1 <- fetchData1
data2 <- fetchData2
} yield data1 + data2
println(Await.result(combined, 5.seconds))
}
Why this translation:
- Haskell's IO is pure, Scala's Future is eager
- Use Cats Effect IO for pure functional effects in Scala
concurrentlymaps to for-comprehension with Futures- Avoid
Awaitin production; use callbacks or IO
Software Transactional Memory
Haskell:
import Control.Concurrent.STM
type Account = TVar Int
transfer :: Account -> Account -> Int -> STM ()
transfer from to amount = do
fromBalance <- readTVar from
when (fromBalance < amount) retry
modifyTVar from (subtract amount)
modifyTVar to (+ amount)
main :: IO ()
main = do
account1 <- newTVarIO 1000
account2 <- newTVarIO 0
atomically $ transfer account1 account2 500
Scala:
import scala.concurrent.stm._
type Account = Ref[Int]
def transfer(from: Account, to: Account, amount: Int): Unit = {
atomic { implicit txn =>
val fromBalance = from()
if (fromBalance < amount) retry
from() = fromBalance - amount
to() = to() + amount
}
}
def main(): Unit = {
val account1 = Ref(1000)
val account2 = Ref(0)
transfer(account1, account2, 500)
}
Why this translation:
- Both support STM with similar semantics
- Scala STM requires
scala-stmlibrary atomicblock replacesatomicallyretryworks the same way
Memory Model
Haskell Lazy Evaluation → Scala Strict Evaluation
| Concept | Haskell | Scala | Notes |
|---|---|---|---|
| Default evaluation | Lazy | Strict | Major difference |
| Force evaluation | seq, deepseq |
N/A (already strict) | - |
| Defer evaluation | Default | lazy val, LazyList |
Explicit in Scala |
| Infinite structures | [1..] |
LazyList.from(1) |
Requires LazyList |
| Thunks | Automatic | => A (by-name) |
Explicit in Scala |
Space Leaks and Strictness
Haskell:
-- Potential space leak (lazy accumulation)
badSum :: [Int] -> Int
badSum = foldl (+) 0 -- Builds up thunks
-- Strict version
goodSum :: [Int] -> Int
goodSum = foldl' (+) 0 -- Forces evaluation
-- Bang patterns
data Point = Point !Int !Int -- Strict fields
-- Forcing evaluation
result = x `seq` y -- Evaluate x, then return y
Scala:
// No space leak (strict by default)
def sum(list: List[Int]): Int =
list.foldLeft(0)(_ + _) // Always strict
// Lazy evaluation when needed
lazy val expensiveValue = {
println("Computing...")
1 + 1
}
// Strict fields by default
case class Point(x: Int, y: Int) // Already strict
// All evaluation is forced by default
val result = {
val _ = x // x is evaluated
y // y is returned
}
Why this matters:
- Haskell's laziness can cause space leaks
- Scala doesn't have this problem by default
- Use
lazy valsparingly in Scala - LazyList for infinite structures
Common Pitfalls
1. Assuming Lazy Evaluation
Problem: Expecting Scala to be lazy like Haskell.
Example:
// ❌ This will hang in Scala (strict evaluation)
val naturals = (1 to Int.MaxValue).toList // Tries to build entire list!
// ✓ Use LazyList for infinite sequences
val naturals = LazyList.from(1)
Solution: Use LazyList for potentially infinite sequences, lazy val for deferred computation.
2. Null Pointer Exceptions
Problem: Scala has null from Java interop, Haskell doesn't.
Example:
// ❌ Dangerous when calling Java code
val name: String = javaObject.getName // Could be null!
val length = name.length // NullPointerException
// ✓ Wrap in Option
val name: Option[String] = Option(javaObject.getName)
val length = name.map(_.length).getOrElse(0)
Solution: Always use Option(...) when calling Java code that might return null.
3. Uncurried Functions
Problem: Haskell functions are curried by default, Scala's are not.
Example:
// Haskell: add :: Int -> Int -> Int
// Scala: Either curried or uncurried
// ❌ Uncurried (not partial-application friendly)
def add(a: Int, b: Int): Int = a + b
// Can't do: val add5 = add(5, _) // Requires placeholder
// ✓ Curried (Haskell-style)
def add(a: Int)(b: Int): Int = a + b
val add5 = add(5) _ // Partial application
Solution: Use curried functions def f(a: A)(b: B) when partial application is needed.
4. Missing Type Classes
Problem: Haskell has many built-in type classes; Scala requires libraries.
Example:
// ❌ No built-in Functor, Monad, etc.
def map[F[_], A, B](fa: F[A])(f: A => B): F[B] = ??? // Can't implement generically
// ✓ Use Cats library
import cats.Functor
import cats.implicits._
def map[F[_]: Functor, A, B](fa: F[A])(f: A => B): F[B] =
fa.map(f)
Solution: Use Cats library for functional abstractions (Functor, Monad, Applicative, etc.).
5. Pattern Matching Exhaustiveness
Problem: Scala allows non-exhaustive matches without warnings by default.
Example:
// ❌ Non-exhaustive match (compiles but warns)
sealed trait Result
case class Success(value: Int) extends Result
case class Failure(error: String) extends Result
def handle(result: Result): String = result match {
case Success(value) => s"Got $value"
// Missing Failure case!
}
// ✓ Complete match
def handle(result: Result): String = result match {
case Success(value) => s"Got $value"
case Failure(error) => s"Error: $error"
}
Solution: Use sealed trait and enable -Xfatal-warnings compiler option to catch non-exhaustive matches.
6. Do-Notation vs For-Comprehension Differences
Problem: Subtle differences between Haskell do-notation and Scala for-comprehension.
Example:
// Haskell: do { return x }
// Scala: Must use yield
// ❌ Missing yield
val result = for {
x <- Some(1)
y <- Some(2)
x + y // Does nothing!
}
// ✓ Use yield
val result = for {
x <- Some(1)
y <- Some(2)
} yield x + y
Solution: Always use yield at the end of for-comprehensions (equivalent to Haskell's return).
7. Higher-Kinded Types
Problem: Scala 2 requires explicit kind annotation; Scala 3 is better.
Example:
// Haskell: class Functor f where
// fmap :: (a -> b) -> f a -> f b
// Scala 2: Requires explicit kind
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
}
// Usage requires type lambda for partially applied types
// ❌ Can't do: Functor[Either[String, ?]] in Scala 2
// ✓ Scala 2: Need kind-projector plugin
// ✓ Scala 3: Much better support
Solution: Use Scala 3 for better higher-kinded type support, or use kind-projector plugin in Scala 2.
Tooling
| Category | Haskell | Scala |
|---|---|---|
| Build Tool | Cabal, Stack | sbt, Mill |
| REPL | GHCi | scala, sbt console |
| Formatter | stylish-haskell, brittany | scalafmt |
| Linter | hlint | scalafix, wartremover |
| Testing | HSpec, QuickCheck | ScalaTest, ScalaCheck |
| Type Checker | GHC | scalac |
| Package Registry | Hackage | Maven Central |
| Documentation | Haddock | Scaladoc |
Helpful Libraries for Haskell Patterns
| Pattern | Haskell | Scala Library |
|---|---|---|
| Type classes | Prelude, base | cats, scalaz |
| Effects | transformers, mtl | cats-effect, zio |
| Optics | lens | monocle |
| JSON | aeson | circe, play-json |
| Parsing | parsec, megaparsec | cats-parse, fastparse |
| Testing | QuickCheck | scalacheck |
| STM | stm | scala-stm |
Examples
Example 1: Simple - List Processing
Before (Haskell):
-- Sum of squares of even numbers
sumOfEvenSquares :: [Int] -> Int
sumOfEvenSquares xs = sum [x^2 | x <- xs, even x]
-- Alternative with functions
sumOfEvenSquares' :: [Int] -> Int
sumOfEvenSquares' = sum . map (^2) . filter even
-- Pattern matching on lists
myLength :: [a] -> Int
myLength [] = 0
myLength (_:xs) = 1 + myLength xs
After (Scala):
// Sum of squares of even numbers
def sumOfEvenSquares(xs: List[Int]): Int = {
(for {
x <- xs
if x % 2 == 0
} yield x * x).sum
}
// Alternative with method chaining (more idiomatic)
def sumOfEvenSquares(xs: List[Int]): Int =
xs.filter(_ % 2 == 0).map(x => x * x).sum
// Pattern matching on lists
def myLength[A](xs: List[A]): Int = xs match {
case Nil => 0
case _ :: tail => 1 + myLength(tail)
}
Example 2: Medium - Type Classes and ADTs
Before (Haskell):
-- Type class
class Describable a where
describe :: a -> String
-- ADT
data Shape = Circle Double
| Rectangle Double Double
deriving (Show, Eq)
instance Describable Shape where
describe (Circle r) = "Circle with radius " ++ show r
describe (Rectangle w h) = "Rectangle " ++ show w ++ "x" ++ show h
-- Using type class
printDescription :: Describable a => a -> IO ()
printDescription x = putStrLn (describe x)
-- Higher-order function with type class
mapDescribe :: Describable a => [a] -> [String]
mapDescribe = map describe
After (Scala):
// Type class
trait Describable[A] {
def describe(a: A): String
}
object Describable {
def apply[A](implicit d: Describable[A]): Describable[A] = d
implicit class DescribableOps[A](val a: A) extends AnyVal {
def describe(implicit d: Describable[A]): String = d.describe(a)
}
}
// ADT
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
// Type class instance
object Shape {
implicit val describableShape: Describable[Shape] = new Describable[Shape] {
def describe(shape: Shape): String = shape match {
case Circle(r) => s"Circle with radius $r"
case Rectangle(w, h) => s"Rectangle ${w}x$h"
}
}
}
// Using type class
def printDescription[A: Describable](x: A): Unit = {
println(Describable[A].describe(x))
}
// Or with extension method
import Describable._
def printDescription[A: Describable](x: A): Unit = {
println(x.describe)
}
// Higher-order function with type class
def mapDescribe[A: Describable](xs: List[A]): List[String] =
xs.map(_.describe)
Example 3: Complex - Parser Combinator with Monadic Composition
Before (Haskell):
import Text.Parsec
import Text.Parsec.String (Parser)
-- Simple expression parser
data Expr = Num Int
| Add Expr Expr
| Mul Expr Expr
deriving (Show, Eq)
number :: Parser Expr
number = Num . read <$> many1 digit
expr :: Parser Expr
expr = term `chainl1` addOp
term :: Parser Expr
term = factor `chainl1` mulOp
factor :: Parser Expr
factor = number <|> parens expr
parens :: Parser a -> Parser a
parens p = char '(' *> p <* char ')'
addOp :: Parser (Expr -> Expr -> Expr)
addOp = Add <$ char '+'
mulOp :: Parser (Expr -> Expr -> Expr)
mulOp = Mul <$ char '*'
-- Evaluator
eval :: Expr -> Int
eval (Num n) = n
eval (Add e1 e2) = eval e1 + eval e2
eval (Mul e1 e2) = eval e1 * eval e2
-- Usage
parseAndEval :: String -> Either ParseError Int
parseAndEval input = do
expr <- parse expr "" input
return (eval expr)
After (Scala):
import cats.parse.{Parser, Numbers}
import cats.parse.Parser._
import cats.parse.Rfc5234.{char, digit}
// Simple expression parser
sealed trait Expr
case class Num(value: Int) extends Expr
case class Add(left: Expr, right: Expr) extends Expr
case class Mul(left: Expr, right: Expr) extends Expr
object ExprParser {
val number: Parser[Expr] =
Numbers.digits.map(s => Num(s.toInt))
lazy val expr: Parser[Expr] =
chainl1(term, addOp)
lazy val term: Parser[Expr] =
chainl1(factor, mulOp)
lazy val factor: Parser[Expr] =
number.orElse(parens(expr))
def parens[A](p: Parser[A]): Parser[A] =
(char('(') *> p <* char(')'))
val addOp: Parser[(Expr, Expr) => Expr] =
char('+').as((e1: Expr, e2: Expr) => Add(e1, e2))
val mulOp: Parser[(Expr, Expr) => Expr] =
char('*').as((e1: Expr, e2: Expr) => Mul(e1, e2))
// Helper for chainl1 (not built-in)
def chainl1[A](p: Parser[A], op: Parser[(A, A) => A]): Parser[A] = {
(p ~ (op ~ p).rep0).map {
case (initial, ops) =>
ops.foldLeft(initial) { case (acc, (f, next)) => f(acc, next) }
}
}
}
// Evaluator
def eval(expr: Expr): Int = expr match {
case Num(n) => n
case Add(e1, e2) => eval(e1) + eval(e2)
case Mul(e1, e2) => eval(e1) * eval(e2)
}
// Usage
def parseAndEval(input: String): Either[Parser.Error, Int] = {
ExprParser.expr.parseAll(input).map(eval)
}
See Also
For more examples and patterns, see:
meta-convert-dev- Foundational patterns with cross-language examplesconvert-typescript-rust- Similar functional programming conversionlang-haskell-dev- Haskell development patternslang-scala-dev- Scala development patterns
Cross-cutting pattern skills:
patterns-concurrency-dev- STM, async patterns across languagespatterns-serialization-dev- JSON, validation patternspatterns-metaprogramming-dev- Type classes, macros, generics