Skip to content
Back to skills

Convert Scala Elixir

ASecurity

Convert Scala code to idiomatic Elixir. Use when migrating Scala/JVM applications to Elixir/BEAM, translating Akka actor systems to OTP, or refactoring functional Scala patterns to BEAM-native concurrency. Extends meta-convert-dev with Scala-to-Elixir specific patterns.

  • 8 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added February 8, 2026
developmentgoexpresstestingrefactoringapidatabase

Works with

  • cli
  • api

Security analysis

A100/100

Scanned February 12, 2026

npx -y skills add aRustyDev/ai --skill convert-scala-elixir --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Convert Scala Elixir?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Convert Scala Elixir
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/arustydev-convert-scala-elixir/badge)](https://www.skillsdirectory.com/skills/arustydev-convert-scala-elixir)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: convert-scala-elixir
description: Convert Scala code to idiomatic Elixir. Use when migrating Scala/JVM applications to Elixir/BEAM, translating Akka actor systems to OTP, or refactoring functional Scala patterns to BEAM-native concurrency. Extends meta-convert-dev with Scala-to-Elixir specific patterns.
---

# Convert Scala to Elixir

Convert Scala code to idiomatic Elixir. This skill extends `meta-convert-dev` with Scala-to-Elixir specific type mappings, idiom translations, and tooling for translating between JVM and BEAM ecosystems.

## 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**: Scala types → Elixir types with dynamic runtime
- **Idiom translations**: Case classes → Structs, Traits → Protocols
- **Error handling**: Try/Either → {:ok, result} tuples and with construct
- **Concurrency**: Futures/Akka actors → Processes and GenServer
- **Paradigm shift**: JVM OOP+FP → BEAM pure FP with immutability
- **Build tools**: sbt → Mix project structure
- **REPL workflow**: Scala REPL/Ammonite → IEx with hot reload

## This Skill Does NOT Cover

- General conversion methodology - see `meta-convert-dev`
- Scala language fundamentals - see `lang-scala-dev`
- Elixir language fundamentals - see `lang-elixir-dev`
- Reverse conversion (Elixir → Scala) - see `convert-elixir-scala`
- Advanced Akka patterns - see `lang-scala-akka-dev` for source patterns
- Advanced Phoenix patterns - see `lang-elixir-phoenix-dev` for target patterns

---

## Quick Reference

| Scala | Elixir | Notes |
|-------|--------|-------|
| `val x = 42` | `x = 42` | Immutable by default in both |
| `Option[A]` | `{:ok, a} \| nil` | Optional values |
| `Either[L, R]` | `{:ok, r} \| {:error, l}` | Error handling |
| `case class User(name: String)` | `defstruct [:name]` | Data containers |
| `sealed trait` | `@type t ::` sum type | ADT definition |
| `trait Logger` | `defprotocol Logger` | Interface/protocol |
| `List[A]` | `[a]` | Immutable lists |
| `Map[K, V]` | `%{k => v}` | Hash maps |
| `Future[A]` | `Task` | Async computations |
| `for { x <- ... } yield` | `for x <- ..., do:` | Comprehensions |
| `implicit val` | `@spec` + callbacks | Type classes → protocols |
| `object Utils` | `defmodule Utils` | Singleton → module |

---

## When Converting Code

1. **Analyze source thoroughly** - Understand JVM semantics before BEAM translation
2. **Map types first** - Create type equivalence table, handle nullable → nil
3. **Preserve semantics** over syntax similarity
4. **Adopt Elixir idioms** - Don't write "Scala code in Elixir syntax"
5. **Embrace the actor model** - Threads/Futures → lightweight processes
6. **Handle supervision** - Convert error handling to OTP supervision trees
7. **Test equivalence** - Same inputs → same outputs across platforms

---

## Type System Mapping

### Primitive Types

| Scala | Elixir | Notes |
|-------|--------|-------|
| `Int` | `integer` | 32-bit signed → arbitrary precision |
| `Long` | `integer` | 64-bit signed → arbitrary precision |
| `Double` | `float` | 64-bit IEEE 754 in both |
| `Float` | `float` | Elixir only has `float` (64-bit) |
| `Boolean` | `boolean` (`:true`/`:false`) | Boolean atoms in Elixir |
| `Char` | `integer` (codepoint) | Unicode codepoint |
| `String` | `String.t()` (binary) | UTF-8 binary strings |
| `Unit` | `:ok` atom | Represents "no value" |
| `Nothing` | n/a | Bottom type (no equivalent) |

**Key difference**: Elixir integers are arbitrary precision by default, unlike JVM's fixed-width integers.

### Collection Types

| Scala | Elixir | Notes |
|-------|--------|-------|
| `List[A]` | `[a]` | Immutable linked lists |
| `Vector[A]` | `[a]` | Use lists (persistent) |
| `Array[A]` | `tuple` or `:array` module | Tuples for fixed size, `:array` for large collections |
| `Set[A]` | `MapSet.t(a)` | Immutable sets |
| `Map[K, V]` | `%{k => v}` | Hash maps |
| `Seq[A]` | `[a]` or `Stream` | Lists or lazy streams |
| `Stream[A]` | `Stream.t(a)` | Lazy sequences |
| `Option[A]` | `a \| nil` or `{:ok, a}` | Nullable handling |
| `(A, B)` | `{a, b}` | Tuples |
| `(A, B, C)` | `{a, b, c}` | Tuples (up to any size) |

### Composite Types

| Scala | Elixir | Notes |
|-------|--------|-------|
| `case class User(name: String, age: Int)` | `defstruct [:name, :age]` | Product types |
| `sealed trait` + `case class` | `@type t ::` union | Sum types (ADTs) |
| `trait Logger` | `defprotocol Logger` | Interfaces |
| `object Utils` | `defmodule Utils` | Singleton as module |
| `class MyClass` | `defmodule` + struct | OOP → module + data |
| `type alias` | `@type` | Type aliases |
| `implicit class` | Functions + pipe `\|>` | Extension methods |

### Error Handling Types

| Scala | Elixir | Notes |
|-------|--------|-------|
| `Try[A]` | `{:ok, a} \| {:error, reason}` | Exception → tuple |
| `Either[L, R]` | `{:ok, r} \| {:error, l}` | Error with context |
| `Success(value)` | `{:ok, value}` | Success case |
| `Failure(exception)` | `{:error, reason}` | Error case |
| `Option[A]` | `{:ok, a} \| nil` | Optional with nil |

---

## Idiom Translation

### Pattern: Case Class to Struct

**Scala:**
```scala
case class User(
  id: Int,
  name: String,
  email: String,
  age: Option[Int] = None
)

object User {
  def create(name: String, email: String): User =
    User(id = 0, name = name, email = email)
}

val user = User.create("Alice", "alice@example.com")
val updated = user.copy(age = Some(30))
```

**Elixir:**
```elixir
defmodule User do
  @enforce_keys [:id, :name, :email]
  defstruct [:id, :name, :email, :age]

  @type t :: %__MODULE__{
    id: integer(),
    name: String.t(),
    email: String.t(),
    age: integer() | nil
  }

  @spec create(String.t(), String.t()) :: t()
  def create(name, email) do
    %User{id: 0, name: name, email: email, age: nil}
  end
end

user = User.create("Alice", "alice@example.com")
updated = %{user | age: 30}
```

**Why this translation:**
- Structs in Elixir are named maps with compile-time guarantees
- `@enforce_keys` ensures required fields at construction
- Update syntax `%{struct | key: value}` replaces `copy`
- Nil is idiomatic for optional fields

---

### Pattern: Sealed Trait (ADT) to Sum Type

**Scala:**
```scala
sealed trait Result[+A]
case class Success[A](value: A) extends Result[A]
case class Failure(error: String) extends Result[Nothing]

def processResult[A](result: Result[A]): String = result match {
  case Success(value) => s"Got: $value"
  case Failure(error) => s"Error: $error"
}
```

**Elixir:**
```elixir
@type result(a) :: {:ok, a} | {:error, String.t()}

@spec process_result(result(term())) :: String.t()
def process_result(result) do
  case result do
    {:ok, value} -> "Got: #{inspect(value)}"
    {:error, error} -> "Error: #{error}"
  end
end
```

**Why this translation:**
- Elixir uses tuples with atom tags for sum types
- Pattern matching works similarly but with tuples
- Type specs describe union types with `|`
- `{:ok, value}` / `{:error, reason}` is the idiomatic convention

---

### Pattern: For-Comprehension to For-Comprehension

**Scala:**
```scala
def getUserData(userId: Int): Option[User] = ???
def getOrders(user: User): Option[List[Order]] = ???
def calculateTotal(orders: List[Order]): Double = ???

val total: Option[Double] = for {
  user <- getUserData(123)
  orders <- getOrders(user)
} yield calculateTotal(orders)
```

**Elixir:**
```elixir
def get_user_data(user_id), do: # returns {:ok, user} | {:error, reason}
def get_orders(user), do: # returns {:ok, orders} | {:error, reason}
def calculate_total(orders), do: # returns float

# Using with construct (preferred for error handling)
def get_total(user_id) do
  with {:ok, user} <- get_user_data(user_id),
       {:ok, orders} <- get_orders(user) do
    {:ok, calculate_total(orders)}
  end
end

# Or using for comprehension
def get_total_for(user_id) do
  for {:ok, user} <- [get_user_data(user_id)],
      {:ok, orders} <- [get_orders(user)] do
    calculate_total(orders)
  end
end
```

**Why this translation:**
- Elixir's `with` construct is more idiomatic for chaining {:ok, _} results
- `for` works but is less common for this pattern
- `with` automatically propagates errors (no explicit error handling needed)

---

### Pattern: Trait to Protocol

**Scala:**
```scala
trait Serializable[A] {
  def toJson(value: A): String
}

object Serializable {
  implicit val intSerializable: Serializable[Int] = new Serializable[Int] {
    def toJson(value: Int): String = value.toString
  }

  implicit val stringSerializable: Serializable[String] = new Serializable[String] {
    def toJson(value: String): String = s""""$value""""
  }
}

def serialize[A](value: A)(implicit s: Serializable[A]): String =
  s.toJson(value)

serialize(42)        // "42"
serialize("hello")   // "\"hello\""
```

**Elixir:**
```elixir
defprotocol Serializable do
  @spec to_json(t) :: String.t()
  def to_json(value)
end

defimpl Serializable, for: Integer do
  def to_json(value), do: Integer.to_string(value)
end

defimpl Serializable, for: BitString do
  def to_json(value), do: ~s("#{value}")
end

# Usage
Serializable.to_json(42)       # "42"
Serializable.to_json("hello")  # "\"hello\""
```

**Why this translation:**
- Protocols in Elixir provide similar polymorphism to Scala type classes
- `defimpl` replaces implicit instances
- No implicit resolution - explicit protocol calls
- Protocols are open (can be extended for any type)

---

### Pattern: Future to Task/Process

**Scala:**
```scala
import scala.concurrent.{Future, ExecutionContext}
import scala.concurrent.ExecutionContext.Implicits.global

def fetchUser(id: Int): Future[User] = Future {
  // Blocking database call
  Database.findUser(id)
}

def fetchOrders(user: User): Future[List[Order]] = Future {
  Database.findOrders(user.id)
}

val result: Future[(User, List[Order])] = for {
  user <- fetchUser(123)
  orders <- fetchOrders(user)
} yield (user, orders)

result.foreach { case (user, orders) =>
  println(s"User ${user.name} has ${orders.size} orders")
}
```

**Elixir:**
```elixir
def fetch_user(id) do
  Task.async(fn ->
    # Database call
    Database.find_user(id)
  end)
end

def fetch_orders(user_id) do
  Task.async(fn ->
    Database.find_orders(user_id)
  end)
end

# Parallel execution
user_task = fetch_user(123)
user = Task.await(user_task)

orders_task = fetch_orders(user.id)
orders = Task.await(orders_task)

IO.puts("User #{user.name} has #{length(orders)} orders")

# Or using Task.async_stream for collections
user_ids = [1, 2, 3, 4, 5]
results =
  user_ids
  |> Task.async_stream(&fetch_user/1, max_concurrency: 10)
  |> Enum.to_list()
```

**Why this translation:**
- `Task` in Elixir is similar to `Future` but built on lightweight processes
- `Task.async` + `Task.await` for async/await pattern
- `Task.async_stream` for parallel collection processing
- No need for ExecutionContext - BEAM scheduler handles it

---

### Pattern: Akka Actor to GenServer

**Scala:**
```scala
import akka.actor.{Actor, ActorRef, Props}

case class Increment()
case class GetCount()
case class SetCount(value: Int)

class CounterActor extends Actor {
  var count: Int = 0

  def receive: Receive = {
    case Increment() =>
      count += 1
    case GetCount() =>
      sender() ! count
    case SetCount(value) =>
      count = value
  }
}

object CounterActor {
  def props(): Props = Props(new CounterActor)
}

// Usage
val counter = system.actorOf(CounterActor.props(), "counter")
counter ! Increment()
counter ! GetCount()
```

**Elixir:**
```elixir
defmodule CounterServer do
  use GenServer

  # Client API

  def start_link(opts \\ []) do
    GenServer.start_link(__MODULE__, :ok, opts ++ [name: __MODULE__])
  end

  def increment do
    GenServer.cast(__MODULE__, :increment)
  end

  def get_count do
    GenServer.call(__MODULE__, :get_count)
  end

  def set_count(value) do
    GenServer.cast(__MODULE__, {:set_count, value})
  end

  # Server Callbacks

  @impl true
  def init(:ok) do
    {:ok, %{count: 0}}
  end

  @impl true
  def handle_cast(:increment, state) do
    {:noreply, %{state | count: state.count + 1}}
  end

  @impl true
  def handle_cast({:set_count, value}, state) do
    {:noreply, %{state | count: value}}
  end

  @impl true
  def handle_call(:get_count, _from, state) do
    {:reply, state.count, state}
  end
end

# Usage
{:ok, _pid} = CounterServer.start_link()
CounterServer.increment()
count = CounterServer.get_count()
```

**Why this translation:**
- `GenServer` is Elixir's equivalent to Akka's typed actor pattern
- `use GenServer` imports behavior
- `handle_cast` for async messages (fire-and-forget)
- `handle_call` for sync messages (request-reply)
- State is immutable - return new state from handlers

---

### Pattern: Pattern Matching

**Scala:**
```scala
sealed trait HttpResponse
case class Ok(body: String) extends HttpResponse
case class NotFound(path: String) extends HttpResponse
case class ServerError(message: String) extends HttpResponse

def handleResponse(response: HttpResponse): String = response match {
  case Ok(body) => s"Success: $body"
  case NotFound(path) => s"Not found: $path"
  case ServerError(msg) => s"Error: $msg"
}

// List pattern matching
def sum(list: List[Int]): Int = list match {
  case Nil => 0
  case head :: tail => head + sum(tail)
}
```

**Elixir:**
```elixir
@type http_response ::
  {:ok, String.t()}
  | {:not_found, String.t()}
  | {:server_error, String.t()}

@spec handle_response(http_response()) :: String.t()
def handle_response(response) do
  case response do
    {:ok, body} -> "Success: #{body}"
    {:not_found, path} -> "Not found: #{path}"
    {:server_error, msg} -> "Error: #{msg}"
  end
end

# List pattern matching
def sum([]), do: 0
def sum([head | tail]), do: head + sum(tail)
```

**Why this translation:**
- Elixir uses tuples with atom tags for ADTs
- Pattern matching works directly in function heads (multi-clause functions)
- List syntax `[head | tail]` similar to Scala's `head :: tail`
- `case` expressions work identically

---

## Paradigm Translation

### Mental Model Shift: JVM OOP+FP → BEAM Pure FP

| Scala Concept | Elixir Approach | Key Insight |
|---------------|-----------------|-------------|
| Class with state | Struct + GenServer | Separate data from behavior |
| Inheritance | Protocol implementation | Favor protocols over hierarchies |
| Mutable var | Immutable + recursion | All data immutable |
| Thread pool | Process pool (lightweight) | Millions of processes possible |
| ExecutionContext | BEAM scheduler | Scheduler is always available |
| synchronized block | GenServer serialization | Message-based synchronization |
| Exception handling | {:ok, _} / {:error, _} tuples | Errors as values |

### Concurrency Mental Model

| Scala Model | Elixir Model | Conceptual Translation |
|-------------|--------------|------------------------|
| Future[A] | Task | Async computation |
| Akka Actor | GenServer / Process | Stateful concurrent entity |
| ExecutionContext | BEAM Scheduler | Work distribution |
| Ask pattern | GenServer.call | Synchronous request |
| Tell pattern | GenServer.cast | Asynchronous message |
| Supervision | Supervisor | Fault tolerance |
| Thread | Process (lightweight) | Concurrent execution unit |

---

## Error Handling

### Scala Try/Either → Elixir Tuples

**Scala:**
```scala
import scala.util.{Try, Success, Failure}

def parseNumber(s: String): Try[Int] =
  Try(s.toInt)

def divide(a: Int, b: Int): Either[String, Int] =
  if (b == 0) Left("Division by zero")
  else Right(a / b)

// Chaining with for-comprehension
def calculate(x: String, y: String): Either[String, Int] = {
  for {
    a <- parseNumber(x).toEither.left.map(_ => "Invalid x")
    b <- parseNumber(y).toEither.left.map(_ => "Invalid y")
    result <- divide(a, b)
  } yield result
}
```

**Elixir:**
```elixir
@spec parse_number(String.t()) :: {:ok, integer()} | {:error, :invalid}
def parse_number(s) do
  case Integer.parse(s) do
    {num, ""} -> {:ok, num}
    _ -> {:error, :invalid}
  end
end

@spec divide(integer(), integer()) :: {:ok, integer()} | {:error, :division_by_zero}
def divide(_a, 0), do: {:error, :division_by_zero}
def divide(a, b), do: {:ok, div(a, b)}

# Chaining with `with` construct
@spec calculate(String.t(), String.t()) :: {:ok, integer()} | {:error, atom()}
def calculate(x, y) do
  with {:ok, a} <- parse_number(x),
       {:ok, b} <- parse_number(y),
       {:ok, result} <- divide(a, b) do
    {:ok, result}
  else
    {:error, :invalid} -> {:error, :invalid_input}
    {:error, reason} -> {:error, reason}
  end
end
```

**Why this translation:**
- `{:ok, value}` / `{:error, reason}` is idiomatic Elixir
- `with` construct chains operations, short-circuits on first error
- Explicit error handling with pattern matching
- No exceptions thrown for expected errors

### Exception Handling

**Scala:**
```scala
def readFile(path: String): Try[String] = Try {
  scala.io.Source.fromFile(path).mkString
}

// With explicit exception handling
def safeDivide(a: Int, b: Int): Try[Int] = {
  try {
    Success(a / b)
  } catch {
    case _: ArithmeticException => Failure(new Exception("Division by zero"))
    case e: Exception => Failure(e)
  }
}
```

**Elixir:**
```elixir
@spec read_file(String.t()) :: {:ok, String.t()} | {:error, File.posix()}
def read_file(path) do
  File.read(path)
end

# With explicit exception handling (rare - prefer tuples)
def safe_divide(a, b) do
  try do
    {:ok, div(a, b)}
  rescue
    ArithmeticError -> {:error, :division_by_zero}
    e in [File.Error, RuntimeError] -> {:error, e.message}
  end
end

# Better: use guards
def safe_divide(_a, 0), do: {:error, :division_by_zero}
def safe_divide(a, b), do: {:ok, div(a, b)}
```

**Why this translation:**
- Elixir uses `try/rescue` for exceptions, but prefers tuples
- `rescue` is like Scala's `catch`
- Pattern match on error types
- Guards prevent errors at function head

---

## Concurrency Patterns

### Scala Future → Elixir Task

**Scala:**
```scala
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

val users: Future[List[User]] = Future {
  Database.findAllUsers()
}

val enriched: Future[List[EnrichedUser]] = users.flatMap { userList =>
  Future.traverse(userList) { user =>
    Future {
      enrichWithOrders(user)
    }
  }
}

enriched.foreach { result =>
  println(s"Processed ${result.size} users")
}
```

**Elixir:**
```elixir
# Simple async task
task = Task.async(fn ->
  Database.find_all_users()
end)

users = Task.await(task, 5000)  # 5 second timeout

# Parallel processing of list
enriched =
  users
  |> Task.async_stream(&enrich_with_orders/1, max_concurrency: 10, timeout: 5000)
  |> Enum.map(fn {:ok, result} -> result end)

IO.puts("Processed #{length(enriched)} users")

# Or using processes directly
users
|> Enum.each(fn user ->
  Task.start(fn ->
    process_user(user)
  end)
end)
```

**Why this translation:**
- `Task.async` + `Task.await` mirrors Future semantics
- `Task.async_stream` for concurrent collection processing
- Built-in timeout support (no need for Await.result)
- Lightweight processes instead of thread pools

### Akka Stream → Elixir Stream/Flow

**Scala:**
```scala
import akka.stream.scaladsl._

val source = Source(1 to 1000)
  .map(_ * 2)
  .filter(_ % 3 == 0)
  .grouped(10)
  .mapAsync(4)(batch => Future {
    processBatch(batch)
  })
  .runFold(0)(_ + _)
```

**Elixir:**
```elixir
# Using Stream (lazy)
result =
  1..1000
  |> Stream.map(&(&1 * 2))
  |> Stream.filter(&(rem(&1, 3) == 0))
  |> Stream.chunk_every(10)
  |> Task.async_stream(&process_batch/1, max_concurrency: 4)
  |> Enum.reduce(0, fn {:ok, val}, acc -> acc + val end)

# Using Flow (parallel + lazy)
alias Experimental.Flow

result =
  1..1000
  |> Flow.from_enumerable()
  |> Flow.map(&(&1 * 2))
  |> Flow.filter(&(rem(&1, 3) == 0))
  |> Flow.partition()
  |> Flow.reduce(fn -> 0 end, &(&1 + &2))
  |> Enum.sum()
```

**Why this translation:**
- `Stream` provides lazy evaluation like Akka Source
- `Task.async_stream` for concurrent processing
- `Flow` library for advanced parallel stream processing
- No need for explicit materialization

---

## Memory & Ownership

### JVM GC → BEAM GC

| JVM (Scala) | BEAM (Elixir) | Key Difference |
|-------------|---------------|----------------|
| Shared heap | Per-process heap | Each process has isolated memory |
| Stop-the-world GC | Per-process GC | GC pauses isolated to single process |
| Object references | Immutable data copying | Data copied between processes |
| Memory leaks possible | Leaks die with process | Process death frees all memory |

**Key insights:**
- Elixir processes have tiny isolated heaps (measured in KB)
- No stop-the-world pauses - each process GCs independently
- Immutability + copying prevents shared state bugs
- Process crash = automatic memory reclamation

---

## Build and Dependencies

### sbt → Mix

**Scala (build.sbt):**
```scala
name := "my-app"
version := "0.1.0"
scalaVersion := "3.3.1"

libraryDependencies ++= Seq(
  "com.typesafe.akka" %% "akka-actor-typed" % "2.8.5",
  "org.typelevel" %% "cats-core" % "2.10.0",
  "io.circe" %% "circe-core" % "0.14.6"
)
```

**Elixir (mix.exs):**
```elixir
defmodule MyApp.MixProject do
  use Mix.Project

  def project do
    [
      app: :my_app,
      version: "0.1.0",
      elixir: "~> 1.15",
      deps: deps()
    ]
  end

  def application do
    [
      extra_applications: [:logger],
      mod: {MyApp.Application, []}
    ]
  end

  defp deps do
    [
      {:phoenix, "~> 1.7"},
      {:jason, "~> 1.4"},
      {:httpoison, "~> 2.0"}
    ]
  end
end
```

### Common Task Mapping

| sbt | Mix | Purpose |
|-----|-----|---------|
| `sbt compile` | `mix compile` | Compile project |
| `sbt run` | `mix run` | Run application |
| `sbt test` | `mix test` | Run tests |
| `sbt console` | `iex -S mix` | Interactive REPL |
| `sbt clean` | `mix clean` | Clean build |
| `sbt ~compile` | `mix compile --watch` | Watch mode |
| `sbt assembly` | `mix release` | Build deployable artifact |

---

## Testing Strategy

### ScalaTest → ExUnit

**Scala:**
```scala
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers

class UserSpec extends AnyFlatSpec with Matchers {
  "User" should "have valid email" in {
    val user = User("Alice", "alice@example.com")
    user.email should include("@")
  }

  it should "handle optional age" in {
    val user = User("Bob", "bob@example.com", Some(30))
    user.age shouldBe Some(30)
  }
}
```

**Elixir:**
```elixir
defmodule UserTest do
  use ExUnit.Case, async: true

  describe "User" do
    test "has valid email" do
      user = %User{name: "Alice", email: "alice@example.com"}
      assert String.contains?(user.email, "@")
    end

    test "handles optional age" do
      user = %User{name: "Bob", email: "bob@example.com", age: 30}
      assert user.age == 30
    end

    test "handles nil age" do
      user = %User{name: "Charlie", email: "charlie@example.com", age: nil}
      assert is_nil(user.age)
    end
  end
end
```

---

## REPL Workflow

### Scala REPL/Ammonite → IEx

Both Scala and Elixir are REPL-centric languages with strong interactive development workflows.

**Scala (Ammonite):**
```scala
// Start REPL
$ amm

// Load file
@ import $file.MyModule

// Hot reload
@ import $ivy.`com.lihaoyi::requests:0.8.0`

// Inspect types
scala> :type myVariable
```

**Elixir (IEx):**
```elixir
# Start REPL with project
$ iex -S mix

# Recompile after changes
iex> recompile()

# Load module
iex> c "lib/my_module.ex"

# Introspection
iex> h Enum.map
iex> i my_variable

# Hot code reloading in production
iex> :code.purge(MyModule)
iex> :code.load_file(MyModule)
```

**Key differences:**
- IEx supports hot code reloading in production (BEAM feature)
- Scala REPL is faster at compile-edit-test cycle for type checking
- Elixir's observer for live system inspection: `:observer.start()`

---

## Common Pitfalls

### 1. Forgetting Elixir is Dynamically Typed

```elixir
# WRONG - Type errors caught at runtime, not compile time
def add(a, b), do: a + b
add("hello", 5)  # Runtime error

# BETTER - Use guards
def add(a, b) when is_integer(a) and is_integer(b), do: a + b

# BEST - Use Dialyzer typespecs
@spec add(integer(), integer()) :: integer()
def add(a, b), do: a + b
```

### 2. Null vs Nil Confusion

```scala
// Scala
val x: Option[Int] = None
val y: Int = x.getOrElse(0)
```

```elixir
# Elixir - nil is just an atom
x = nil
y = x || 0  # Short-circuit evaluation

# Pattern matching on nil
case x do
  nil -> 0
  value -> value
end
```

### 3. String vs Charlist

```elixir
# WRONG
string = 'hello'  # This is a charlist!
String.upcase(string)  # Error

# CORRECT
string = "hello"  # Binary string
String.upcase(string)  # "HELLO"

# Charlist (for Erlang interop)
charlist = 'hello'
:string.uppercase(charlist)  # 'HELLO'
```

### 4. Immutability in Both Languages

```scala
// Scala - val is immutable
val list = List(1, 2, 3)
list = list :+ 4  // Compile error
```

```elixir
# Elixir - rebinding is allowed
list = [1, 2, 3]
list = list ++ [4]  # OK - creates new list

# Use pin operator to prevent rebinding
^list = [1, 2, 3, 4]  # Match error if list != [1,2,3,4]
```

### 5. Process Isolation

```elixir
# WRONG - Trying to share mutable state
defmodule Counter do
  @count 0  # Module attribute, not mutable

  def increment do
    @count = @count + 1  # Compile error
  end
end

# CORRECT - Use GenServer for stateful processes
defmodule Counter do
  use GenServer

  def init(_), do: {:ok, 0}

  def handle_call(:increment, _from, count) do
    {:reply, count + 1, count + 1}
  end
end
```

### 6. Akka Ask Pattern Timeout

```scala
// Scala Akka
implicit val timeout = Timeout(5.seconds)
val future = actor ? GetCount
```

```elixir
# Elixir GenServer
count = GenServer.call(CounterServer, :get_count, 5000)  # 5 second timeout
```

---

## Tooling

| Scala | Elixir | Purpose |
|-------|--------|---------|
| sbt | Mix | Build tool |
| Ammonite | IEx | Enhanced REPL |
| ScalaTest | ExUnit | Testing framework |
| ScalaCheck | StreamData | Property-based testing |
| Akka | OTP | Concurrency framework |
| Cats/Scalaz | (built-in) | FP abstractions |
| Circe | Jason/Poison | JSON library |
| Scala CLI | Mix scripts | Scripting |
| Metals | ElixirLS | Language server |
| Scalafmt | mix format | Code formatter |

---

## Examples

### Example 1: Simple - Option Handling

**Before (Scala):**
```scala
def findUser(id: Int): Option[User] = {
  if (id > 0) Some(User(id, "Alice"))
  else None
}

val result = findUser(1) match {
  case Some(user) => s"Found: ${user.name}"
  case None => "Not found"
}
```

**After (Elixir):**
```elixir
def find_user(id) when id > 0, do: {:ok, %User{id: id, name: "Alice"}}
def find_user(_id), do: {:error, :not_found}

result = case find_user(1) do
  {:ok, user} -> "Found: #{user.name}"
  {:error, :not_found} -> "Not found"
end
```

---

### Example 2: Medium - Error Handling Chain

**Before (Scala):**
```scala
import scala.util.{Try, Success, Failure}

def validateEmail(email: String): Either[String, String] =
  if (email.contains("@")) Right(email)
  else Left("Invalid email")

def createUser(name: String, email: String): Either[String, User] = {
  for {
    validEmail <- validateEmail(email)
  } yield User(name = name, email = validEmail)
}

def saveUser(user: User): Try[User] = Try {
  // Database save
  user
}

def registerUser(name: String, email: String): Either[String, User] = {
  createUser(name, email) match {
    case Right(user) =>
      saveUser(user) match {
        case Success(saved) => Right(saved)
        case Failure(ex) => Left(s"DB error: ${ex.getMessage}")
      }
    case Left(error) => Left(error)
  }
}
```

**After (Elixir):**
```elixir
def validate_email(email) do
  if String.contains?(email, "@") do
    {:ok, email}
  else
    {:error, :invalid_email}
  end
end

def create_user(name, email) do
  with {:ok, valid_email} <- validate_email(email) do
    {:ok, %User{name: name, email: valid_email}}
  end
end

def save_user(user) do
  # Database save
  {:ok, user}
rescue
  e -> {:error, "DB error: #{inspect(e)}"}
end

def register_user(name, email) do
  with {:ok, user} <- create_user(name, email),
       {:ok, saved} <- save_user(user) do
    {:ok, saved}
  end
end
```

---

### Example 3: Complex - Concurrent Data Processing

**Before (Scala):**
```scala
import scala.concurrent.{Future, ExecutionContext}
import scala.concurrent.duration._
import akka.actor.ActorSystem
import akka.stream.scaladsl._

implicit val system: ActorSystem = ActorSystem("processor")
implicit val ec: ExecutionContext = system.dispatcher

case class Order(id: Int, userId: Int, total: Double)
case class EnrichedOrder(order: Order, userName: String)

def fetchOrders(): Future[List[Order]] = Future {
  // Database call
  (1 to 100).map(i => Order(i, i % 10, i * 10.0)).toList
}

def fetchUserName(userId: Int): Future[String] = Future {
  s"User-$userId"
}

def enrichOrder(order: Order): Future[EnrichedOrder] = {
  fetchUserName(order.userId).map { userName =>
    EnrichedOrder(order, userName)
  }
}

def processOrders(): Future[List[EnrichedOrder]] = {
  for {
    orders <- fetchOrders()
    enriched <- Future.traverse(orders)(enrichOrder)
  } yield enriched
}

// Using Akka Streams for backpressure
def processOrdersStream(): Future[List[EnrichedOrder]] = {
  Source.future(fetchOrders())
    .mapConcat(identity)
    .mapAsync(10)(enrichOrder)
    .runWith(Sink.seq)
    .map(_.toList)
}
```

**After (Elixir):**
```elixir
defmodule OrderProcessor do
  @moduledoc """
  Concurrent order processing with Task.async_stream
  """

  defmodule Order do
    defstruct [:id, :user_id, :total]
  end

  defmodule EnrichedOrder do
    defstruct [:order, :user_name]
  end

  def fetch_orders do
    # Database call
    orders = for i <- 1..100 do
      %Order{id: i, user_id: rem(i, 10), total: i * 10.0}
    end
    {:ok, orders}
  end

  def fetch_user_name(user_id) do
    # Simulate async call
    Process.sleep(10)
    "User-#{user_id}"
  end

  def enrich_order(%Order{} = order) do
    user_name = fetch_user_name(order.user_id)
    %EnrichedOrder{order: order, user_name: user_name}
  end

  # Using Task.async_stream for concurrent processing
  def process_orders do
    with {:ok, orders} <- fetch_orders() do
      enriched =
        orders
        |> Task.async_stream(&enrich_order/1,
             max_concurrency: 10,
             timeout: 5000)
        |> Enum.map(fn {:ok, result} -> result end)

      {:ok, enriched}
    end
  end

  # Using Flow for advanced parallel processing
  def process_orders_flow do
    alias Experimental.Flow

    with {:ok, orders} <- fetch_orders() do
      enriched =
        orders
        |> Flow.from_enumerable(max_demand: 10)
        |> Flow.map(&enrich_order/1)
        |> Enum.to_list()

      {:ok, enriched}
    end
  end

  # Using GenStage for backpressure (like Akka Streams)
  defmodule OrderProducer do
    use GenStage

    def start_link(orders) do
      GenStage.start_link(__MODULE__, orders)
    end

    def init(orders) do
      {:producer, orders}
    end

    def handle_demand(demand, orders) do
      {to_send, remaining} = Enum.split(orders, demand)
      {:noreply, to_send, remaining}
    end
  end

  defmodule OrderProcessor do
    use GenStage

    def start_link() do
      GenStage.start_link(__MODULE__, :ok)
    end

    def init(:ok) do
      {:producer_consumer, :ok}
    end

    def handle_events(orders, _from, state) do
      enriched = Enum.map(orders, &enrich_order/1)
      {:noreply, enriched, state}
    end
  end
end
```

**Key differences:**
- Elixir uses `Task.async_stream` for simple concurrent collection processing
- `Flow` library provides parallel streaming similar to Akka Streams
- `GenStage` for advanced backpressure and producer-consumer patterns
- No need for ExecutionContext - BEAM scheduler handles everything
- Timeout built into Task functions
- Simpler error handling with `{:ok, _}` tuples

---

## See Also

For more examples and patterns, see:
- `meta-convert-dev` - Foundational patterns with cross-language examples
- `convert-erlang-elixir` - Related BEAM conversion (Erlang → Elixir)
- `convert-elixir-scala` - Reverse conversion (Elixir → Scala)
- `lang-scala-dev` - Scala development patterns
- `lang-scala-akka-dev` - Akka-specific patterns
- `lang-elixir-dev` - Elixir development patterns
- `lang-elixir-otp-dev` - Advanced OTP patterns

Cross-cutting pattern skills:
- `patterns-concurrency-dev` - Async, actors, processes across languages
- `patterns-serialization-dev` - JSON, validation across languages

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…