Agents convert-clojure-scala
Bidirectional conversion between Clojure and Scala. Use when migrating projects between these languages in either direction. Extends meta-convert-dev with Clojure↔Scala specific patterns. 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.
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-clojure-scala" ~/.claude/skills/arustydev-agents-convert-clojure-scala && rm -rf "$T"
content/skills/convert-clojure-scala/SKILL.mdClojure ↔ Scala Conversion
Bidirectional conversion between Clojure and Scala. This skill extends
meta-convert-dev with Clojure↔Scala specific type mappings, idiom translations, and tooling for converting functional code between dynamic Lisp and statically-typed hybrid FP/OOP.
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: 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
Quick Reference
| Clojure | Scala | Notes |
|---|---|---|
| | Direct mapping (both JVM) |
(default int) | / | Scala Int more common; Long for large |
(default decimal) | | Direct mapping |
| | / in both |
| / | Prefer Option[A] over null |
list | | Immutable linked list |
vector | / | Vector for indexed, List for sequential |
map | | Immutable map |
set | | Immutable set |
| | Lambda/anonymous function |
| | Named function |
| | Data structure with methods |
| Multimethod | Trait + pattern matching | Polymorphic dispatch |
| Protocol | Trait | Behavior contract |
| Atom | / | Mutable reference |
| Agent | Akka actor / | Async computation |
| core.async channel | Akka Stream / FS2 | Stream processing |
| Macro | Inline / macro (limited) | Compile-time metaprogramming |
thread-last | | 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 |
|---|---|---|
(true/false) | | Direct mapping |
(default integer) | | Scala Int (32-bit) is more common |
(large integers) | | Use Long for 64-bit integers |
| | Arbitrary precision integers |
(default decimal) | | Direct mapping |
| | Arbitrary precision decimals |
| | Single character |
| | Immutable strings (JVM) |
| / | Prefer over null |
Keyword | / String | Use sealed traits or enums for tagged types |
Symbol | No direct equivalent | Use case objects or sealed traits |
Ratio | No direct equivalent | Use library or |
Collection Types
| Clojure | Scala | Notes |
|---|---|---|
list | | Immutable singly-linked list |
vector | | Indexed immutable sequence |
map | | Immutable hash map |
set | | Immutable hash set |
| Lazy seq | / | Lazy evaluation |
| Transient | Mutable collections | Use temporarily |
| Persistent | Immutable collections | Default in Scala |
| Java array | | Mutable, fixed-size |
Composite Types
| Clojure | Scala | Notes |
|---|---|---|
Plain map | | Prefer case classes for structured data |
| Plain map (open) | | When structure is truly dynamic |
| | Data structure with protocol implementations |
Tagged map | + case classes | ADT with pattern matching |
| Multimethod dispatch | Pattern matching | Type-based dispatch |
| Protocol | Trait | Interface with possible default implementations |
| + trait | Low-level performance-critical types |
| Namespace | (singleton) | Module-level functions/values |
Function Types
| Clojure | Scala | Notes |
|---|---|---|
| | Anonymous function |
| | Multi-parameter function |
| | Placeholder syntax |
Variadic | | Variable arguments |
| Multi-arity fn | Overloaded methods | Multiple parameter lists |
| Curried (manual) | Curried | Automatic currying in Scala |
Nil/Null Handling
| Clojure | Scala | Notes |
|---|---|---|
| | Absence of value |
| Value | | Present value |
Check | | Pattern matching preferred |
| | Check for presence |
| | Default value |
| | For-comprehension |
| | 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
→ Scalanil
(explicit absence)None - Clojure
→ Scalawhen-let
combinator.map() - Clojure
with default → Scalaif-let.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
→ Field access:key.field - Clojure
→ Scalaassoc
method.copy() - Case classes provide:
- Compile-time field checking
- Pattern matching support
- Automatic
,equals
,hashCodetoString - 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->
chaining for updates.copy() - 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
key → Scala case class type (compiler-checked):type - 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
→ Scalaatom
for thread-safe mutable stateAtomicReference - Clojure
→ Scalaswap!.updateAndGet() - Clojure
→ Scalareset!.set() - Clojure
→ Scala@atom.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
for lazy evaluation (similar to macro delay)=> A - 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
with protocol → Scala case class extending traitdefrecord - Clojure
→ Scala extension methods or implicit classesextend-type - 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 |
|---|---|---|
| | For exceptions |
maps | | Typed error handling |
| Nil for absence | | Safe nullability |
with data | Custom case classes | Structured errors |
| Error threading | , | 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
instead of case classesMap[String, Any] - 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:
is pervasive and safenil - Scala mistake: Using
instead ofnullOption - Fix: Always use
for nullable valuesOption[A]
- 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
are optimized and fast:key - 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
for fast feedback; leverage Metals or IntelliJ~compile
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:
- Foundational patterns with cross-language examplesmeta-convert-dev
- Similar dynamic → static conversionconvert-python-scala
- Another typed target languageconvert-typescript-scala
- Clojure development patternslang-clojure-dev
- Scala development patternslang-scala-dev
Cross-cutting pattern skills:
- Async, channels, threads across languagespatterns-concurrency-dev
- JSON, validation across languagespatterns-serialization-dev
- Macros, compile-time code generationpatterns-metaprogramming-dev