Agent Skills: Haskell ↔ Scala Conversion

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.

UncategorizedID: arustydev/ai/convert-haskell-scala

Repository

aRustyDevLicense: AGPL-3.0
72

Install this agent skill to your local

pnpm dlx add-skill https://github.com/aRustyDev/agents/tree/HEAD/content/skills/convert-haskell-scala

Skill Files

Browse the full folder contents for convert-haskell-scala.

Download Skill

Loading file tree…

content/skills/convert-haskell-scala/SKILL.md

Skill Metadata

Name
convert-haskell-scala
Description
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.

Haskell ↔ 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

  • 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

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

  1. Analyze source thoroughly before writing target
  2. Map types first - create type equivalence table
  3. Preserve semantics over syntax similarity
  4. Adopt Scala idioms - don't write "Haskell code in Scala syntax"
  5. Handle edge cases - lazy evaluation, null safety, JVM interop
  6. 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:

  • Maybe and Option are semantically equivalent
  • Both use pattern matching for explicit handling
  • Scala's .getOrElse is more concise than fromMaybe
  • 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-notation maps to Scala's for-comprehension
  • ADT error types work similarly in both languages
  • Scala requires explicit yield at 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 yield keyword
  • Guards (filters) use if in 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 if in Scala match, | in Haskell function definitions
  • sealed trait ensures 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 LazyList for infinite sequences
  • Use lazy val for 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 to compose (right-to-left) or andThen (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-notation maps directly to Scala's for-comprehension
  • Both desugar to flatMap/map
  • Haskell uses return, Scala uses yield
  • guard becomes if in 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 try from Control.Exception maps to Scala's Try
  • 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
  • concurrently maps to for-comprehension with Futures
  • Avoid Await in 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-stm library
  • atomic block replaces atomically
  • retry works 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 val sparingly 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