Agent Skills: Clojure ↔ Scala Conversion

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.

UncategorizedID: arustydev/ai/convert-clojure-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-clojure-scala

Skill Files

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

Download Skill

Loading file tree…

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

Skill Metadata

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

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

  • 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

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

  1. Analyze source thoroughly before writing target - understand Clojure semantics
  2. Map types first - create explicit type mapping table (dynamic → static)
  3. Preserve semantics over syntax similarity
  4. Adopt Scala idioms - don't write "Clojure code in Scala syntax"
  5. Handle edge cases - nil-safety, lazy evaluation, type erasure
  6. Test equivalence - same inputs → same outputs
  7. Embrace static typing - leverage compiler for correctness
  8. 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 → Scala None (explicit absence)
  • Clojure when-let → Scala .map() combinator
  • Clojure if-let with 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 :type key → 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 → Scala AtomicReference for 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 => A for 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 defrecord with 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

  1. 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
  2. Ignoring Static Types

    • Clojure: Dynamic typing
    • Scala mistake: Avoiding types with excessive Any
    • Fix: Embrace Scala's type system; let compiler help
  3. Missing nil vs. None

    • Clojure: nil is pervasive and safe
    • Scala mistake: Using null instead of Option
    • Fix: Always use Option[A] for nullable values
  4. 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
  5. Threading Macros → Complex Chains

    • Clojure: ->> for elegant pipelines
    • Scala mistake: Creating unreadable method chains
    • Fix: Break chains into intermediate vals; use for-comprehensions
  6. 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
  7. Keyword Keys → String Keys

    • Clojure: Keywords :key are optimized and fast
    • Scala mistake: Using strings for map keys in structured data
    • Fix: Use case classes or sealed traits for structured types
  8. 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
  9. Multimethod Dispatch → Pattern Matching

    • Clojure: Multimethods are open and extensible
    • Scala: Sealed traits are closed
    • Fix: Accept trade-off (extensibility vs. exhaustiveness checking)
  10. REPL-Driven → Compile-Driven

    • Clojure: REPL-first development
    • Scala: More emphasis on compilation and types
    • Fix: Use sbt ~compile for 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 examples
  • convert-python-scala - Similar dynamic → static conversion
  • convert-typescript-scala - Another typed target language
  • lang-clojure-dev - Clojure development patterns
  • lang-scala-dev - Scala development patterns

Cross-cutting pattern skills:

  • patterns-concurrency-dev - Async, channels, threads across languages
  • patterns-serialization-dev - JSON, validation across languages
  • patterns-metaprogramming-dev - Macros, compile-time code generation