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 replacesatomically - •
retryworks 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 examples - •
convert-typescript-rust- Similar functional programming conversion - •
lang-haskell-dev- Haskell development patterns - •
lang-scala-dev- Scala development patterns
Cross-cutting pattern skills:
- •
patterns-concurrency-dev- STM, async patterns across languages - •
patterns-serialization-dev- JSON, validation patterns - •
patterns-metaprogramming-dev- Type classes, macros, generics