| name | convert-clojure-scala |
| description | Convert Clojure code to idiomatic Scala. Use when migrating Clojure projects to Scala, translating Clojure patterns to idiomatic Scala, or refactoring Clojure codebases. Extends meta-convert-dev with Clojure-to-Scala specific patterns. |
Convert Clojure to Scala
Convert Clojure code to idiomatic Scala. This skill extends meta-convert-dev with Clojure-to-Scala specific type mappings, idiom translations, and tooling for converting functional code between dynamic Lisp and statically-typed hybrid FP/OOP.
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: Clojure dynamic types → Scala static types with inference
- Idiom translations: Clojure Lisp-style → idiomatic Scala hybrid FP/OOP
- Error handling: Clojure exceptions/maps → Scala Option/Either/Try
- Async patterns: Clojure core.async/futures → Scala Futures/Akka/Cats Effect
- Concurrency models: STM/agents → actors/STM alternatives
- Platform translation: JVM (Clojure) → JVM (Scala) with better performance
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - Clojure language fundamentals - see
lang-clojure-dev - Scala language fundamentals - see
lang-scala-dev - Reverse conversion (Scala → Clojure) - see
convert-scala-clojure
Quick Reference
| Clojure | Scala | Notes |
|---|---|---|
String |
String |
Direct mapping (both JVM) |
Long (default int) |
Int / Long |
Scala Int more common; Long for large |
Double (default decimal) |
Double |
Direct mapping |
Boolean |
Boolean |
true/false in both |
nil |
None / null |
Prefer Option[A] over null |
'(...) list |
List[A] |
Immutable linked list |
[...] vector |
Vector[A] / List[A] |
Vector for indexed, List for sequential |
{...} map |
Map[K, V] |
Immutable map |
#{...} set |
Set[A] |
Immutable set |
(fn [x] ...) |
(x: A) => B |
Lambda/anonymous function |
defn |
def name(...) |
Named function |
defrecord |
case class |
Data structure with methods |
| Multimethod | Trait + pattern matching | Polymorphic dispatch |
| Protocol | Trait | Behavior contract |
| Atom | AtomicReference / Ref |
Mutable reference |
| Agent | Akka actor / Future |
Async computation |
| core.async channel | Akka Stream / FS2 | Stream processing |
| Macro | Inline / macro (limited) | Compile-time metaprogramming |
->> thread-last |
.map().filter() |
Method chaining |
When Converting Code
- Analyze source thoroughly before writing target - understand Clojure semantics
- Map types first - create explicit type mapping table (dynamic → static)
- Preserve semantics over syntax similarity
- Adopt Scala idioms - don't write "Clojure code in Scala syntax"
- Handle edge cases - nil-safety, lazy evaluation, type erasure
- Test equivalence - same inputs → same outputs
- Embrace static typing - leverage compiler for correctness
- Consider performance - Scala can be more performant with proper types
Type System Mapping
Primitive Types
| Clojure | Scala | Notes |
|---|---|---|
Boolean (true/false) |
Boolean |
Direct mapping |
Long (default integer) |
Int |
Scala Int (32-bit) is more common |
Long (large integers) |
Long |
Use Long for 64-bit integers |
BigInt |
BigInt |
Arbitrary precision integers |
Double (default decimal) |
Double |
Direct mapping |
BigDecimal |
BigDecimal |
Arbitrary precision decimals |
Character |
Char |
Single character |
String |
String |
Immutable strings (JVM) |
nil |
null / None |
Prefer Option[A] over null |
Keyword :key |
Symbol('key) / String |
Use sealed traits or enums for tagged types |
Symbol 'sym |
No direct equivalent | Use case objects or sealed traits |
Ratio 1/3 |
No direct equivalent | Use Rational library or Double |
Collection Types
| Clojure | Scala | Notes |
|---|---|---|
'(1 2 3) list |
List(1, 2, 3) |
Immutable singly-linked list |
[1 2 3] vector |
Vector(1, 2, 3) |
Indexed immutable sequence |
{:a 1 :b 2} map |
Map("a" -> 1, "b" -> 2) |
Immutable hash map |
#{1 2 3} set |
Set(1, 2, 3) |
Immutable hash set |
| Lazy seq | LazyList / Iterator |
Lazy evaluation |
| Transient | Mutable collections | Use scala.collection.mutable temporarily |
| Persistent | Immutable collections | Default in Scala |
| Java array | Array[A] |
Mutable, fixed-size |
Composite Types
| Clojure | Scala | Notes |
|---|---|---|
Plain map {:name "Alice"} |
case class User(name: String) |
Prefer case classes for structured data |
| Plain map (open) | Map[String, Any] |
When structure is truly dynamic |
defrecord |
case class |
Data structure with protocol implementations |
Tagged map {:type :circle} |
sealed trait + case classes |
ADT with pattern matching |
| Multimethod dispatch | Pattern matching | Type-based dispatch |
| Protocol | Trait | Interface with possible default implementations |
deftype |
class + trait |
Low-level performance-critical types |
| Namespace | object (singleton) |
Module-level functions/values |
Function Types
| Clojure | Scala | Notes |
|---|---|---|
(fn [x] body) |
(x: A) => B |
Anonymous function |
(fn [x y] body) |
(x: A, y: B) => C |
Multi-parameter function |
#(+ % 1) |
_ + 1 |
Placeholder syntax |
Variadic [& args] |
args: A* |
Variable arguments |
| Multi-arity fn | Overloaded methods | Multiple parameter lists |
| Curried (manual) | Curried def f(x: A)(y: B) |
Automatic currying in Scala |
Nil/Null Handling
| Clojure | Scala | Notes |
|---|---|---|
nil |
None |
Absence of value |
| Value | Some(value) |
Present value |
Check (nil? x) |
x.isEmpty |
Pattern matching preferred |
(some? x) |
x.isDefined |
Check for presence |
(or x default) |
x.getOrElse(default) |
Default value |
(when-let [x ...] ...) |
for { x <- opt } yield ... |
For-comprehension |
(some-> x f g) |
x.map(f).map(g) |
Chained operations |
Idiom Translation
Pattern 1: nil → Option Type
Clojure:
(defn get-user [id]
;; Returns nil if not found
(get @users-db id))
(defn get-user-email [id]
(when-let [user (get-user id)]
(:email user)))
;; With default
(defn get-user-name [id]
(if-let [user (get-user id)]
(:name user)
"Unknown"))
Scala:
def getUser(id: Int): Option[User] = {
usersDb.get(id)
}
def getUserEmail(id: Int): Option[String] = {
getUser(id).map(_.email)
}
// With default
def getUserName(id: Int): String = {
getUser(id).map(_.name).getOrElse("Unknown")
}
// Pattern matching
def getUserNameMatch(id: Int): String = getUser(id) match {
case Some(user) => user.name
case None => "Unknown"
}
Why this translation:
- Clojure
nil→ ScalaNone(explicit absence) - Clojure
when-let→ Scala.map()combinator - Clojure
if-letwith default → Scala.getOrElse() - Pattern matching is more idiomatic in Scala than if/else chains
- Compiler enforces handling of both Some and None cases
Pattern 2: Maps → Case Classes
Clojure:
(def user {:name "Alice" :age 30 :email "alice@example.com"})
(defn greet [user]
(str "Hello, " (:name user)))
(defn is-adult? [user]
(>= (:age user) 18))
;; Update
(def updated-user (assoc user :age 31))
Scala:
case class User(name: String, age: Int, email: String)
val user = User("Alice", 30, "alice@example.com")
def greet(user: User): String = {
s"Hello, ${user.name}"
}
def isAdult(user: User): Boolean = {
user.age >= 18
}
// Update (immutable copy)
val updatedUser = user.copy(age = 31)
Why this translation:
- Clojure maps → Scala case classes for structured data
- Keyword access
:key→ Field access.field - Clojure
assoc→ Scala.copy()method - Case classes provide:
- Compile-time field checking
- Pattern matching support
- Automatic
equals,hashCode,toString - Better IDE support and refactoring
Pattern 3: Threading Macros → Method Chaining
Clojure:
(defn process-items [items]
(->> items
(filter :active)
(map :value)
(filter #(> % 10))
(reduce +)))
;; Thread-first
(defn transform-user [user]
(-> user
(assoc :normalized-name (clojure.string/lower-case (:name user)))
(update :age inc)
(dissoc :temp-field)))
Scala:
def processItems(items: List[Item]): Int = {
items
.filter(_.active)
.map(_.value)
.filter(_ > 10)
.sum
}
// Or with for-comprehension
def processItemsFor(items: List[Item]): Int = {
(for {
item <- items if item.active
value = item.value if value > 10
} yield value).sum
}
// Thread-first style
def transformUser(user: User): User = {
user
.copy(normalizedName = user.name.toLowerCase)
.copy(age = user.age + 1)
}
Why this translation:
- Clojure
->>(thread-last) → Scala method chaining (data flows left-to-right) - Clojure
->(thread-first) → Scala.copy()chaining for updates - Scala for-comprehensions are alternative for complex filter/map chains
- Method chaining is more natural in Scala due to OOP foundation
- Both styles maintain immutability
Pattern 4: Multimethods → Pattern Matching / Type Classes
Clojure:
(defmulti area :type)
(defmethod area :circle [{:keys [radius]}]
(* Math/PI radius radius))
(defmethod area :rectangle [{:keys [width height]}]
(* width height))
(defmethod area :triangle [{:keys [base height]}]
(* 0.5 base height))
;; Usage
(area {:type :circle :radius 5})
Scala:
// Approach 1: Sealed trait + pattern matching (most idiomatic)
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) 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
}
// Approach 2: Polymorphic method (OOP style)
sealed trait Shape {
def area: Double
}
case class Circle(radius: Double) extends Shape {
def area: Double = math.Pi * radius * radius
}
case class Rectangle(width: Double, height: Double) extends Shape {
def area: Double = width * height
}
case class Triangle(base: Double, height: Double) extends Shape {
def area: Double = 0.5 * base * height
}
// Usage
val circle = Circle(5)
area(circle) // Approach 1
circle.area // Approach 2
Why this translation:
- Clojure multimethods → Scala sealed traits + pattern matching (type-safe dispatch)
- Clojure
:typekey → Scala case class type (compiler-checked) - Pattern matching exhaustiveness checked at compile time
- Alternative: polymorphic methods for OOP-style dispatch
- Sealed traits ensure all cases are known at compile time
Pattern 5: Atoms → AtomicReference / Ref
Clojure:
(def counter (atom 0))
(swap! counter inc)
(swap! counter + 5)
(reset! counter 0)
@counter ;; Deref
;; With validation
(def validated-atom
(atom 0
:validator #(>= % 0)))
Scala:
import java.util.concurrent.atomic.AtomicReference
val counter = new AtomicReference(0)
counter.updateAndGet(_ + 1)
counter.updateAndGet(_ + 5)
counter.set(0)
counter.get() // Deref
// With Cats STM
import cats.effect.IO
import cats.effect.std.Ref
val program = for {
counter <- Ref[IO].of(0)
_ <- counter.update(_ + 1)
_ <- counter.update(_ + 5)
_ <- counter.set(0)
value <- counter.get
} yield value
// Or use synchronized for simple cases
class Counter {
private var count = 0
def increment(): Int = synchronized {
count += 1
count
}
def get: Int = synchronized(count)
}
Why this translation:
- Clojure
atom→ ScalaAtomicReferencefor thread-safe mutable state - Clojure
swap!→ Scala.updateAndGet() - Clojure
reset!→ Scala.set() - Clojure
@atom→ Scala.get() - For functional effects, use Cats Effect
Ref[IO] - Validation can be added through wrapper methods
Pattern 6: core.async Channels → Akka Streams / FS2
Clojure:
(require '[clojure.core.async :as async :refer [go <! >! chan]])
(defn process-messages []
(let [ch (chan 10)]
(go
(loop []
(when-let [msg (<! ch)]
(println "Processing:" msg)
(recur))))
ch))
(def ch (process-messages))
(go (>! ch "Hello"))
Scala:
// Approach 1: Akka Streams
import akka.stream._
import akka.stream.scaladsl._
import akka.actor.ActorSystem
implicit val system = ActorSystem()
implicit val materializer = ActorMaterializer()
val source = Source.queue[String](bufferSize = 10, OverflowStrategy.backpressure)
val (queue, _) = source
.map { msg =>
println(s"Processing: $msg")
msg
}
.toMat(Sink.ignore)(Keep.both)
.run()
queue.offer("Hello")
// Approach 2: FS2 (functional streams)
import cats.effect.IO
import fs2._
val stream = Stream.eval(IO(println("Processing: Hello")))
stream.compile.drain.unsafeRunSync()
// Approach 3: Simple Future-based
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
def processMessages(): String => Future[Unit] = { msg =>
Future {
println(s"Processing: $msg")
}
}
val processor = processMessages()
processor("Hello")
Why this translation:
- Clojure core.async channels → Akka Streams for back-pressure and complex flows
- Clojure go blocks → Scala Futures or FS2 streams
- Akka Streams provide more structure and operators
- FS2 integrates with Cats Effect for pure FP
- Choose based on complexity: Futures (simple), Akka (complex), FS2 (pure FP)
Pattern 7: Macros → Inline Methods / Compile-Time
Clojure:
(defmacro unless [condition & body]
`(if (not ~condition)
(do ~@body)))
(unless false
(println "This runs")
"result")
;; Infix macro
(defmacro infix [a op b]
`(~op ~a ~b))
(infix 3 + 5) ;; => 8
Scala:
// Scala 3: Inline methods (simpler than macros)
inline def unless(condition: Boolean)(body: => Unit): Unit = {
if (!condition) body
}
unless(false) {
println("This runs")
"result"
}
// Scala 2: By-name parameters
def unless2(condition: Boolean)(body: => Unit): Unit = {
if (!condition) body
}
// For infix, use operators
extension (a: Int) {
infix def plus(b: Int): Int = a + b
}
3 plus 5 // => 8
// Scala 3 macros (for complex metaprogramming)
import scala.quoted.*
inline def debug(inline expr: Any): Any = ${
debugImpl('expr)
}
def debugImpl(expr: Expr[Any])(using Quotes): Expr[Any] = {
import quotes.reflect.*
val tree = expr.asTerm
val code = tree.show
'{
println(s"$code => ${$expr}")
$expr
}
}
Why this translation:
- Clojure macros → Scala inline methods (Scala 3) for simple cases
- Complex metaprogramming → Scala 3 macros (quote/splice)
- By-name parameters
=> Afor lazy evaluation (similar to macro delay) - Extension methods for DSL-like syntax
- Scala macros are more restrictive but type-safe
- Prefer higher-order functions over macros when possible
Pattern 8: Protocols → Traits
Clojure:
(defprotocol Drawable
(draw [this]))
(defrecord Circle [radius]
Drawable
(draw [this]
(str "Drawing circle with radius " radius)))
(defrecord Rectangle [width height]
Drawable
(draw [this]
(str "Drawing rectangle " width "x" height)))
;; Extend to existing types
(extend-type String
Drawable
(draw [this]
(str "Drawing text: " this)))
Scala:
trait Drawable {
def draw: String
}
case class Circle(radius: Double) extends Drawable {
def draw: String = s"Drawing circle with radius $radius"
}
case class Rectangle(width: Double, height: Double) extends Drawable {
def draw: String = s"Drawing rectangle ${width}x${height}"
}
// Extension methods for existing types (Scala 3)
extension (s: String) {
def draw: String = s"Drawing text: $s"
}
// Or implicit class (Scala 2)
implicit class DrawableString(s: String) extends Drawable {
def draw: String = s"Drawing text: $s"
}
// Usage
val circle: Drawable = Circle(5)
circle.draw
"Hello".draw // Extension method
Why this translation:
- Clojure protocols → Scala traits (interfaces with implementations)
- Clojure
defrecordwith protocol → Scala case class extending trait - Clojure
extend-type→ Scala extension methods or implicit classes - Scala traits support default implementations
- Extension methods don't require wrapper types
- Traits can have type parameters and self-types
Error Handling
Clojure Error Model → Scala Error Model
Clojure primarily uses exceptions with ex-info for structured errors. Scala offers typed error handling with Option, Either, and Try.
Clojure exception pattern:
(defn divide [a b]
(if (zero? b)
(throw (ex-info "Division by zero" {:numerator a}))
(/ a b)))
(defn safe-divide [a b]
(try
{:ok (divide a b)}
(catch Exception e
{:error (.getMessage e) :data (ex-data e)})))
Scala typed error pattern:
// Option for simple presence/absence
def divide(a: Int, b: Int): Option[Int] = {
if (b == 0) None
else Some(a / b)
}
// Either for error details
def divideEither(a: Int, b: Int): Either[String, Int] = {
if (b == 0) Left("Division by zero")
else Right(a / b)
}
// Try for exception handling
import scala.util.{Try, Success, Failure}
def divideTry(a: Int, b: Int): Try[Int] = Try {
if (b == 0) throw new ArithmeticException("Division by zero")
a / b
}
// Custom error type (recommended for domain errors)
sealed trait DivisionError
case object DivisionByZero extends DivisionError
case class InvalidInput(msg: String) extends DivisionError
def divideTyped(a: Int, b: Int): Either[DivisionError, Int] = {
if (b == 0) Left(DivisionByZero)
else Right(a / b)
}
Error propagation:
| Clojure | Scala | Notes |
|---|---|---|
try/catch |
try/catch |
For exceptions |
{:ok/:error} maps |
Either[L, R] |
Typed error handling |
| Nil for absence | Option[A] |
Safe nullability |
ex-info with data |
Custom case classes | Structured errors |
| Error threading | .map(), .flatMap() |
Monadic composition |
For-comprehension error handling:
def compute(a: Int, b: Int, c: Int): Either[String, Int] = {
for {
x <- divideEither(a, b)
y <- divideEither(x, c)
z <- divideEither(y, 2)
} yield z
}
// Equivalent to nested flatMap/map
divideEither(a, b)
.flatMap(x => divideEither(x, c))
.flatMap(y => divideEither(y, 2))
Concurrency Patterns
STM (Software Transactional Memory)
Clojure:
(def account-a (ref 1000))
(def account-b (ref 2000))
(dosync
(alter account-a - 100)
(alter account-b + 100))
Scala:
// Scala STM (stm library)
import scala.concurrent.stm._
val accountA = Ref(1000)
val accountB = Ref(2000)
atomic { implicit txn =>
accountA -= 100
accountB += 100
}
// Or Cats Effect STM
import cats.effect.IO
import cats.effect.std.{Ref => CatsRef}
val program = for {
accountA <- CatsRef[IO].of(1000)
accountB <- CatsRef[IO].of(2000)
_ <- (
accountA.update(_ - 100),
accountB.update(_ + 100)
).parTupled
} yield ()
Agents → Akka Actors
Clojure:
(def logger (agent []))
(send logger conj "Log entry 1")
(send logger conj "Log entry 2")
(await logger)
@logger ;; => ["Log entry 1" "Log entry 2"]
Scala:
// Akka Typed Actors
import akka.actor.typed._
import akka.actor.typed.scaladsl.Behaviors
sealed trait LogMessage
case class AddEntry(entry: String) extends LogMessage
case class GetEntries(replyTo: ActorRef[List[String]]) extends LogMessage
def logger(entries: List[String]): Behavior[LogMessage] = {
Behaviors.receive { (context, message) =>
message match {
case AddEntry(entry) =>
logger(entries :+ entry)
case GetEntries(replyTo) =>
replyTo ! entries
Behaviors.same
}
}
}
val system = ActorSystem(logger(List.empty), "logger")
system ! AddEntry("Log entry 1")
system ! AddEntry("Log entry 2")
Futures
Clojure:
(def result (future
(Thread/sleep 1000)
42))
@result ;; Blocks until complete
Scala:
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
val result = Future {
Thread.sleep(1000)
42
}
// Await (blocking)
import scala.concurrent.Await
import scala.concurrent.duration._
Await.result(result, 2.seconds)
// Or use callbacks (non-blocking)
result.foreach(println)
// Or for-comprehension
val combined = for {
a <- Future(10)
b <- Future(20)
} yield a + b
Platform & Performance
JVM Optimization
Both Clojure and Scala run on JVM, but Scala can be more performant with proper type usage:
Performance considerations:
| Aspect | Clojure | Scala | Notes |
|---|---|---|---|
| Boxing overhead | Higher (dynamic) | Lower (primitives) | Scala specialization reduces boxing |
| Method dispatch | Dynamic (slower) | Static or dynamic | Scala pattern matching is faster than multimethods |
| Collection operations | Generic | Specialized | Scala collections can be optimized per type |
| Lazy evaluation | Lazy sequences | Streams/LazyList | Both are lazy, similar performance |
| Type erasure | N/A (dynamic) | Yes (generics) | Both have JVM type erasure |
Optimization patterns:
// Use specialized collections for primitives
val ints: Array[Int] = Array(1, 2, 3) // No boxing
val doubles: Vector[Double] = Vector(1.0, 2.0) // Specialized
// Use @specialized for generic code
def sum[@specialized(Int, Long, Double) A: Numeric](list: List[A]): A = {
list.sum
}
// Use tail recursion
@scala.annotation.tailrec
def factorial(n: Int, acc: Int = 1): Int = {
if (n <= 1) acc
else factorial(n - 1, n * acc)
}
// Use views for lazy evaluation
val result = (1 to 1000000).view
.map(_ * 2)
.filter(_ > 100)
.take(10)
.toList // Only 10 elements processed
Common Pitfalls
Treating Everything as Maps
- Clojure: Maps everywhere
- Scala mistake: Using
Map[String, Any]instead of case classes - Fix: Use case classes for structured data, sealed traits for variants
Ignoring Static Types
- Clojure: Dynamic typing
- Scala mistake: Avoiding types with excessive
Any - Fix: Embrace Scala's type system; let compiler help
Missing nil vs. None
- Clojure:
nilis pervasive and safe - Scala mistake: Using
nullinstead ofOption - Fix: Always use
Option[A]for nullable values
- Clojure:
Over-using Mutable Collections
- Clojure: Immutable by default
- Scala mistake: Using mutable collections from
scala.collection.mutable - Fix: Prefer immutable collections; use mutable only for performance-critical code
Threading Macros → Complex Chains
- Clojure:
->>for elegant pipelines - Scala mistake: Creating unreadable method chains
- Fix: Break chains into intermediate vals; use for-comprehensions
- Clojure:
Macros Everywhere
- Clojure: Macros are common
- Scala mistake: Trying to replicate with complex macros
- Fix: Use higher-order functions, inline methods, or accept language differences
Keyword Keys → String Keys
- Clojure: Keywords
:keyare optimized and fast - Scala mistake: Using strings for map keys in structured data
- Fix: Use case classes or sealed traits for structured types
- Clojure: Keywords
Lazy Sequences → Streams Without Forcing
- Clojure: Lazy seqs can cause holding onto head
- Scala: Similar with LazyList
- Fix: Force realization when needed or use strict collections
Multimethod Dispatch → Pattern Matching
- Clojure: Multimethods are open and extensible
- Scala: Sealed traits are closed
- Fix: Accept trade-off (extensibility vs. exhaustiveness checking)
REPL-Driven → Compile-Driven
- Clojure: REPL-first development
- Scala: More emphasis on compilation and types
- Fix: Use sbt
~compilefor fast feedback; leverage Metals or IntelliJ
Tooling
| Tool | Purpose | Notes |
|---|---|---|
| sbt | Build tool | Standard Scala build tool |
| Mill | Build tool | Modern alternative to sbt |
| IntelliJ IDEA | IDE | Best Scala IDE with refactoring support |
| Metals | Language server | For VS Code, Emacs, Vim |
| Scalafmt | Formatter | Code formatting |
| Scalafix | Linter | Automated refactoring and linting |
| ScalaTest | Testing | Most popular test framework |
| ScalaCheck | Property testing | Like Clojure test.check |
| Wartremover | Linter | Detect unsafe patterns |
| Akka | Concurrency | Actor model for concurrency |
| Cats / Cats Effect | FP library | Functional programming abstractions |
| FS2 | Streaming | Functional streams (replaces core.async) |
Examples
Example 1: Simple - List Processing
Before (Clojure):
(def numbers [1 2 3 4 5])
(defn process [nums]
(->> nums
(filter even?)
(map #(* % 2))
(reduce +)))
(process numbers) ;; => 12
After (Scala):
val numbers = List(1, 2, 3, 4, 5)
def process(nums: List[Int]): Int = {
nums
.filter(_ % 2 == 0)
.map(_ * 2)
.sum
}
process(numbers) // => 12
Example 2: Medium - Error Handling with Either
Before (Clojure):
(defn parse-int [s]
(try
{:ok (Integer/parseInt s)}
(catch NumberFormatException e
{:error "Invalid number"})))
(defn divide [a b]
(if (zero? b)
{:error "Division by zero"}
{:ok (/ a b)}))
(defn compute [a-str b-str]
(let [a-result (parse-int a-str)]
(if (:error a-result)
a-result
(let [b-result (parse-int b-str)]
(if (:error b-result)
b-result
(let [a (:ok a-result)
b (:ok b-result)]
(divide a b)))))))
(compute "10" "2") ;; => {:ok 5}
(compute "10" "0") ;; => {:error "Division by zero"}
(compute "abc" "2") ;; => {:error "Invalid number"}
After (Scala):
def parseInt(s: String): Either[String, Int] = {
try {
Right(s.toInt)
} catch {
case _: NumberFormatException => Left("Invalid number")
}
}
def divide(a: Int, b: Int): Either[String, Int] = {
if (b == 0) Left("Division by zero")
else Right(a / b)
}
def compute(aStr: String, bStr: String): Either[String, Int] = {
for {
a <- parseInt(aStr)
b <- parseInt(bStr)
result <- divide(a, b)
} yield result
}
compute("10", "2") // => Right(5)
compute("10", "0") // => Left("Division by zero")
compute("abc", "2") // => Left("Invalid number")
Example 3: Complex - ADT with Pattern Matching
Before (Clojure):
;; Tagged map representation
(defn circle [radius]
{:type :circle :radius radius})
(defn rectangle [width height]
{:type :rectangle :width width :height height})
(defn triangle [base height]
{:type :triangle :base base :height height})
;; Multimethod dispatch
(defmulti area :type)
(defmethod area :circle [{:keys [radius]}]
(* Math/PI radius radius))
(defmethod area :rectangle [{:keys [width height]}]
(* width height))
(defmethod area :triangle [{:keys [base height]}]
(* 0.5 base height))
(defmulti perimeter :type)
(defmethod perimeter :circle [{:keys [radius]}]
(* 2 Math/PI radius))
(defmethod perimeter :rectangle [{:keys [width height]}]
(* 2 (+ width height)))
(defmethod perimeter :triangle [{:keys [base height]}]
;; Simplified - assuming right triangle
(+ base height (Math/sqrt (+ (* base base) (* height height)))))
;; Usage
(def shapes
[(circle 5)
(rectangle 4 6)
(triangle 3 4)])
(defn total-area [shapes]
(reduce + (map area shapes)))
(defn describe-shape [shape]
(let [a (area shape)
p (perimeter shape)]
(str "Area: " (format "%.2f" a)
", Perimeter: " (format "%.2f" p))))
;; Results
(total-area shapes) ;; => ~113.54
(map describe-shape shapes)
;; => ("Area: 78.54, Perimeter: 31.42"
;; "Area: 24.00, Perimeter: 20.00"
;; "Area: 6.00, Perimeter: 12.00")
After (Scala):
sealed trait Shape {
def area: Double
def perimeter: Double
}
case class Circle(radius: Double) extends Shape {
def area: Double = math.Pi * radius * radius
def perimeter: Double = 2 * math.Pi * radius
}
case class Rectangle(width: Double, height: Double) extends Shape {
def area: Double = width * height
def perimeter: Double = 2 * (width + height)
}
case class Triangle(base: Double, height: Double) extends Shape {
def area: Double = 0.5 * base * height
def perimeter: Double = {
// Simplified - assuming right triangle
base + height + math.sqrt(base * base + height * height)
}
}
// Usage
val shapes = List(
Circle(5),
Rectangle(4, 6),
Triangle(3, 4)
)
def totalArea(shapes: List[Shape]): Double = {
shapes.map(_.area).sum
}
def describeShape(shape: Shape): String = {
val a = shape.area
val p = shape.perimeter
f"Area: $a%.2f, Perimeter: $p%.2f"
}
// Results
totalArea(shapes) // => 113.53981633974483
shapes.map(describeShape)
// => List(
// "Area: 78.54, Perimeter: 31.42",
// "Area: 24.00, Perimeter: 20.00",
// "Area: 6.00, Perimeter: 12.00"
// )
// Pattern matching variant
def describeShapeMatch(shape: Shape): String = shape match {
case Circle(r) =>
s"Circle with radius $r"
case Rectangle(w, h) =>
s"Rectangle ${w}x$h"
case Triangle(b, h) =>
s"Triangle with base $b and height $h"
}
See Also
For more examples and patterns, see:
meta-convert-dev- Foundational patterns with cross-language examplesconvert-python-scala- Similar dynamic → static conversionconvert-typescript-scala- Another typed target languagelang-clojure-dev- Clojure development patternslang-scala-dev- Scala development patterns
Cross-cutting pattern skills:
patterns-concurrency-dev- Async, channels, threads across languagespatterns-serialization-dev- JSON, validation across languagespatterns-metaprogramming-dev- Macros, compile-time code generation