Agents convert-haskell-scala
Bidirectional conversion between Haskell and Scala. Use when migrating projects between these languages in either direction. Extends meta-convert-dev with Haskell↔Scala specific patterns. 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.
git clone https://github.com/aRustyDev/agents
T=$(mktemp -d) && git clone --depth=1 https://github.com/aRustyDev/agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/content/skills/convert-haskell-scala" ~/.claude/skills/arustydev-agents-convert-haskell-scala && rm -rf "$T"
content/skills/convert-haskell-scala/SKILL.mdHaskell ↔ Scala Conversion
Bidirectional conversion between Haskell and Scala. This skill extends
meta-convert-dev with Haskell↔Scala specific type mappings, idiom translations, and tooling.
This Skill Extends
- Foundational conversion patterns (APTV workflow, testing strategies)meta-convert-dev
For general concepts like the Analyze → Plan → Transform → Validate workflow, testing strategies, and common pitfalls, see the meta-skill first.
This Skill Adds
- Type mappings: 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
Quick Reference
| Haskell | Scala | Notes |
|---|---|---|
| | Both are immutable strings |
| | 32-bit integers |
| | Arbitrary precision |
| | Floating point |
| | Boolean values |
| | Linked list |
| | Tuple |
| | Nullable values |
| | Sum type for errors |
| (Cats Effect) | Side effects |
| / | ADTs |
(type class) | + / | Type classes |
(function type) | (function type) | Function types |
| (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 |
|---|---|---|
| | 32-bit signed integer |
| | Arbitrary precision integer |
| | 64-bit floating point |
| | 32-bit floating point |
| | Boolean type |
| | Single character |
| | Immutable string |
| | Unit type (void) |
Collection Types
| Haskell | Scala | Notes |
|---|---|---|
| | Immutable linked list |
| | Tuple (up to 22 elements in Scala) |
| | 3-tuple |
| | Immutable map |
| | Immutable set |
| | Indexed sequence |
| | Generic sequence |
Composite Types
| Haskell | Scala | Notes |
|---|---|---|
| | Sum types |
| | Product types with named fields |
| | Zero-cost wrapper |
| | Type alias |
| | Optional values |
| | Sum type (Left is error by convention in both) |
Function Types
| Haskell | Scala | Notes |
|---|---|---|
| | Function from A to B |
| or | Curried vs uncurried |
| (Cats Effect) | Effectful computation |
| (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:
andMaybe
are semantically equivalentOption- Both use pattern matching for explicit handling
- Scala's
is more concise than.getOrElsefromMaybe - 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
maps to Scala'sdo-notationfor-comprehension - ADT error types work similarly in both languages
- Scala requires explicit
at the end of for-comprehensionyield
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
keywordyield - Guards (filters) use
in bothif - 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
in Scala match,if
in Haskell function definitions|
ensures exhaustiveness checking like Haskellsealed trait
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
for infinite sequencesLazyList - Use
for deferred computationlazy val - By-name parameters (
) for lazy function arguments=> A - 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 to.
(right-to-left) orcompose
(left-to-right)andThen - 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
maps directly to Scala'sdo-notationfor-comprehension - Both desugar to flatMap/map
- Haskell uses
, Scala usesreturnyield
becomesguard
in for-comprehensionif- IO monad requires Cats Effect library in Scala
Error Handling
Haskell Maybe/Either → Scala Option/Either
| Haskell Pattern | Scala Equivalent | Notes |
|---|---|---|
| | Absence of value |
| | Present value |
| | Error case |
| | Success case |
| | Provide default |
| | Map with default |
| | 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
from Control.Exception maps to Scala'stryTry - 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 |
|---|---|---|
| | scala.concurrent |
| | cats-effect |
| | scala.concurrent |
| | scala.concurrent |
| | cats-effect |
| | scala.concurrent |
| | 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
maps to for-comprehension with Futuresconcurrently- Avoid
in production; use callbacks or IOAwait
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
libraryscala-stm
block replacesatomicatomically
works the same wayretry
Memory Model
Haskell Lazy Evaluation → Scala Strict Evaluation
| Concept | Haskell | Scala | Notes |
|---|---|---|---|
| Default evaluation | Lazy | Strict | Major difference |
| Force evaluation | , | N/A (already strict) | - |
| Defer evaluation | Default | , | Explicit in Scala |
| Infinite structures | | | Requires LazyList |
| Thunks | Automatic | (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
sparingly in Scalalazy val - 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:
- Foundational patterns with cross-language examplesmeta-convert-dev
- Similar functional programming conversionconvert-typescript-rust
- Haskell development patternslang-haskell-dev
- Scala development patternslang-scala-dev
Cross-cutting pattern skills:
- STM, async patterns across languagespatterns-concurrency-dev
- JSON, validation patternspatterns-serialization-dev
- Type classes, macros, genericspatterns-metaprogramming-dev