Agent Skills: Elixir ↔ Scala Conversion

Bidirectional conversion between Elixir and Scala. Use when migrating projects between these languages in either direction. Extends meta-convert-dev with Elixir↔Scala specific patterns. Use when migrating Elixir/Phoenix applications to Scala, translating BEAM OTP patterns to Akka actors, or refactoring dynamic concurrency to statically-typed JVM patterns. Extends meta-convert-dev with Elixir-to-Scala specific patterns.

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

Skill Files

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

Download Skill

Loading file tree…

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

Skill Metadata

Name
convert-elixir-scala
Description
Bidirectional conversion between Elixir and Scala. Use when migrating projects between these languages in either direction. Extends meta-convert-dev with Elixir↔Scala specific patterns. Use when migrating Elixir/Phoenix applications to Scala, translating BEAM OTP patterns to Akka actors, or refactoring dynamic concurrency to statically-typed JVM patterns. Extends meta-convert-dev with Elixir-to-Scala specific patterns.

Elixir ↔ Scala Conversion

Bidirectional conversion between Elixir and Scala. This skill extends meta-convert-dev with Elixir↔Scala specific type mappings, idiom translations, and tooling for translating between BEAM VM's dynamic actors and JVM's statically-typed Akka actors.

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: Elixir types (dynamic) → Scala types (static)
  • Idiom translations: Elixir patterns → idiomatic Scala with type safety
  • Runtime transition: BEAM VM (lightweight processes) → JVM (Akka actors/Futures)
  • Concurrency models: GenServer/Supervisor → Akka actors with typed protocols
  • OTP patterns: Supervision trees → Akka supervision hierarchies
  • Data structures: Elixir maps/structs → Scala case classes
  • Error handling: {:ok/:error} tuples → Either/Try monads
  • Build tools: Mix → sbt/Mill

This Skill Does NOT Cover

  • General conversion methodology - see meta-convert-dev
  • Elixir language fundamentals - see lang-elixir-dev
  • Scala language fundamentals - see lang-scala-dev
  • Phoenix framework specifics - see lang-scala-play-dev for web frameworks

Quick Reference

| Elixir | Scala | Notes | |--------|-------|-------| | :atom | Symbol / sealed trait | Atoms → sealed traits for exhaustiveness | | {:ok, value} | Right(value) | Result tuples → Either/Try | | {:error, reason} | Left(reason) | Error tuples → Either | | %{key: value} | Map("key" -> value) | Maps require type parameters | | [1, 2, 3] | List(1, 2, 3) | Lists immutable by default | | defmodule Module | object Module | Module → singleton object | | def func(x) | def func(x: Type): ReturnType | Functions require type signatures | | fn x -> x end | (x: Type) => x | Anonymous functions | | \|> (pipe) | .map().flatMap() | Pipe → method chaining | | GenServer | Actor[Protocol] | Typed actors in Akka | | Supervisor | ActorSystem / Behaviors.supervise | Supervision strategies | | Task.async | Future { } | Async execution | | Pattern matching | match/case | Similar but with static types |


8 Pillars Validation

Elixir Coverage

| Pillar | Coverage | Status | |--------|----------|--------| | Module System | ✓ | defmodule, alias, import, use | | Error Handling | ✓ | {:ok/:error} tuples, pattern matching, try/rescue | | Concurrency Model | ✓ | Processes, GenServer, Supervisor, Task | | Metaprogramming | ✓ | Macros, use directive, compile-time generation | | Zero/Default Values | ✓ | nil, pattern matching with defaults | | Serialization | ✓ | Phoenix params, Jason/Poison, Ecto | | Build/Deps | ✓ | Mix, hex.pm, deps management | | Testing Idioms | ✓ | ExUnit, doctests, Mox for mocking |

Score: 8/8 (Green)

Scala Coverage

| Pillar | Coverage | Status | |--------|----------|--------| | Module System | ✓ | Packages, objects, imports, traits | | Error Handling | ✓ | Either, Try, Option, exceptions | | Concurrency Model | ✓ | Futures, Akka actors, Cats Effect IO | | Metaprogramming | ✓ | Macros (Scala 2), inline/metaprogramming (Scala 3) | | Zero/Default Values | ✓ | Option, default parameters, null (avoid) | | Serialization | ✓ | Play JSON, Circe, uPickle, case classes | | Build/Deps | ✓ | sbt, Mill, Maven Central | | Testing Idioms | ✓ | ScalaTest, ScalaCheck, MUnit, specs2 |

Score: 8/8 (Green)

Validation Status: ✅ Both languages have comprehensive coverage. Conversion can proceed with confidence.

Key Challenge: Elixir's dynamic typing to Scala's static typing requires careful type inference and explicit type annotations.


Type System Mapping

Primitive Types

| Elixir | Scala | Notes | |--------|-------|-------| | :atom | Symbol / sealed trait | Use sealed traits for ADTs | | integer | Int / Long / BigInt | Choose based on range needs | | float | Double | 64-bit floating point | | true / false | Boolean | Direct mapping | | nil | None / null (avoid) | Prefer Option[T] | | "string" | String | Immutable strings | | 'charlist' | List[Char] | Less common in Scala |

Collection Types

| Elixir | Scala | Notes | |--------|-------|-------| | [1, 2, 3] | List(1, 2, 3) | Immutable linked lists | | {:ok, value} | Right(value): Either[Error, Value] | Tagged tuples → Either | | {a, b, c} | (a, b, c): (A, B, C) | Tuples map directly | | %{a: 1, b: 2} | Map("a" -> 1, "b" -> 2) | Immutable maps | | MapSet.new([1, 2]) | Set(1, 2) | Immutable sets | | Keyword list | List[(Symbol, A)] | Less common in Scala | | Range 1..10 | Range(1, 11) | Inclusive → exclusive end |

Composite Types

| Elixir | Scala | Notes | |--------|-------|-------| | defstruct [:name, :age] | case class User(name: String, age: Int) | Structs → case classes | | %User{name: "Alice"} | User("Alice", 30) | Constructor syntax | | @type user :: %{name: String.t()} | type User = Map[String, Any] | Type aliases | | @spec func(integer) :: String.t() | def func(x: Int): String | Function signatures required | | Protocol | Trait | Polymorphism via traits |


Idiom Translation

Pattern: Module Definition

Elixir:

defmodule MyApp.Users do
  @moduledoc """
  Handles user-related operations.
  """

  alias MyApp.Repo
  import Ecto.Query

  @type user :: %{id: integer, name: String.t()}

  @spec get_user(integer) :: {:ok, user} | {:error, :not_found}
  def get_user(id) do
    case Repo.get(User, id) do
      nil -> {:error, :not_found}
      user -> {:ok, user}
    end
  end

  defp build_query(filters) do
    from u in User, where: ^filters
  end
end

Scala:

package myapp

/**
 * Handles user-related operations.
 */
object Users {
  import myapp.repo.Repo
  import myapp.models.User

  type UserId = Int
  type UserResult = Either[UserError, User]

  sealed trait UserError
  case object NotFound extends UserError

  def getUser(id: UserId): UserResult = {
    Repo.get[User](id) match {
      case Some(user) => Right(user)
      case None => Left(NotFound)
    }
  }

  private def buildQuery(filters: Map[String, Any]): Query = {
    Query.from[User].where(filters)
  }
}

Why this translation:

  • defmoduleobject (singleton)
  • @moduledoc → Scaladoc /** */
  • @typetype alias with explicit types
  • @spec → method signature with types
  • {:ok/:error} tuples → Either[Error, Value]
  • Pattern matching on nil → Option.match
  • CamelCase modules → package + object structure

Pattern: Pattern Matching to Case Classes

Elixir:

def process_response({:ok, %{"data" => data, "meta" => meta}}) do
  {:success, data, meta}
end

def process_response({:error, reason}) do
  {:failure, reason}
end

def handle_user(%User{name: name, age: age}) when age >= 18 do
  "Adult: #{name}"
end

Scala:

sealed trait Response
case class Success(data: String, meta: Map[String, Any]) extends Response
case class Error(reason: String) extends Response

def processResponse(response: Response): Result = response match {
  case Success(data, meta) =>
    SuccessResult(data, meta)
  case Error(reason) =>
    FailureResult(reason)
}

case class User(name: String, age: Int)

def handleUser(user: User): Option[String] = user match {
  case User(name, age) if age >= 18 =>
    Some(s"Adult: $name")
  case _ =>
    None
}

Why this translation:

  • Multiple function clauses → sealed trait with case classes
  • Guards → pattern guards with if in match
  • Struct pattern matching → case class pattern matching
  • Tag tuples → sealed trait hierarchy for type safety
  • Exhaustiveness checking enforced by compiler

Pattern: Pipelines to Method Chaining

Elixir:

def process_data(input) do
  input
  |> String.trim()
  |> String.downcase()
  |> String.split(",")
  |> Enum.map(&String.trim/1)
  |> Enum.reject(&(&1 == ""))
  |> Enum.join(";")
end

Scala:

def processData(input: String): String = {
  input
    .trim
    .toLowerCase
    .split(",")
    .map(_.trim)
    .filterNot(_.isEmpty)
    .mkString(";")
}

// Or with explicit types for clarity
def processData(input: String): String = {
  val trimmed: String = input.trim
  val lowercased: String = trimmed.toLowerCase
  val parts: Array[String] = lowercased.split(",")
  val cleaned: Array[String] = parts.map(_.trim).filterNot(_.isEmpty)
  cleaned.mkString(";")
}

Why this translation:

  • |> → method chaining (.)
  • Enum functions → collection methods
  • Capture operator &func/arity → anonymous functions _
  • Enum.rejectfilterNot
  • Enum.joinmkString

Pattern: GenServer to Akka Typed Actor

Elixir:

defmodule UserCache do
  use GenServer

  # Client API

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  def get(key) do
    GenServer.call(__MODULE__, {:get, key})
  end

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

  # Server Callbacks

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

  @impl true
  def handle_call({:get, key}, _from, state) do
    {:reply, Map.get(state, key), state}
  end

  @impl true
  def handle_cast({:put, key, value}, state) do
    {:noreply, Map.put(state, key, value)}
  end
end

Scala (Akka Typed):

import akka.actor.typed._
import akka.actor.typed.scaladsl.Behaviors

object UserCache {
  // Protocol
  sealed trait Command
  final case class Get(key: String, replyTo: ActorRef[Option[String]])
    extends Command
  final case class Put(key: String, value: String)
    extends Command

  // Behavior
  def apply(): Behavior[Command] = {
    behavior(Map.empty[String, String])
  }

  private def behavior(cache: Map[String, String]): Behavior[Command] = {
    Behaviors.receive { (context, message) =>
      message match {
        case Get(key, replyTo) =>
          replyTo ! cache.get(key)
          Behaviors.same

        case Put(key, value) =>
          behavior(cache + (key -> value))
      }
    }
  }
}

// Usage
val system: ActorSystem[UserCache.Command] =
  ActorSystem(UserCache(), "user-cache-system")

implicit val timeout: Timeout = 3.seconds
val futureResult: Future[Option[String]] =
  system.ask(ref => UserCache.Get("key", ref))

Why this translation:

  • GenServer → Akka Typed Actor[Protocol]
  • Message tuples → sealed trait protocol
  • handle_call → synchronous message handling with ask pattern
  • handle_cast → asynchronous message handling
  • State recursion → immutable state updates
  • Type safety enforced at compile time

Pattern: Supervisor to Akka Supervision

Elixir:

defmodule MyApp.Application do
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      {UserCache, []},
      {MyWorker, name: MyWorker, restart: :transient},
      {Registry, keys: :unique, name: MyApp.Registry}
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Scala (Akka Typed):

import akka.actor.typed._
import akka.actor.typed.scaladsl._

object MyApp {
  sealed trait RootCommand

  def apply(): Behavior[RootCommand] = {
    Behaviors.setup { context =>
      // Spawn supervised children
      val userCache = context.spawn(
        UserCache(),
        "user-cache"
      )

      val worker = context.spawn(
        Behaviors.supervise(MyWorker())
          .onFailure[Exception](SupervisorStrategy.restart),
        "my-worker"
      )

      // Root behavior
      Behaviors.receiveMessage { message =>
        Behaviors.same
      }
    }
  }
}

// Starting the system
val system = ActorSystem(MyApp(), "my-app-system")

Why this translation:

  • Supervision tree → Behaviors.setup with spawned children
  • :one_for_one → individual supervision per actor
  • :transient restart → SupervisorStrategy.restart.withLimit
  • Named processes → named actors via spawn
  • Declarative children list → imperative spawn calls

Error Handling

Elixir Error Model → Scala Error Model

Elixir uses tagged tuples:

def divide(a, b) when b != 0, do: {:ok, a / b}
def divide(_, 0), do: {:error, :division_by_zero}

# Usage
case divide(10, 2) do
  {:ok, result} -> "Result: #{result}"
  {:error, reason} -> "Error: #{reason}"
end

# Or with pattern matching
{:ok, result} = divide(10, 2)

Scala uses Either/Try monads:

sealed trait MathError
case object DivisionByZero extends MathError

def divide(a: Double, b: Double): Either[MathError, Double] = {
  if (b != 0) Right(a / b)
  else Left(DivisionByZero)
}

// Usage with pattern matching
divide(10, 2) match {
  case Right(result) => s"Result: $result"
  case Left(error) => s"Error: $error"
}

// Or with for-comprehension
val result = for {
  x <- divide(10, 2)
  y <- divide(x, 5)
} yield y

// Or with Try for exceptions
import scala.util.{Try, Success, Failure}

def divideWithTry(a: Double, b: Double): Try[Double] = {
  Try(a / b)
}

divideWithTry(10, 0) match {
  case Success(value) => s"Result: $value"
  case Failure(exception) => s"Error: ${exception.getMessage}"
}

Translation strategy:

  1. {:ok, value}Right(value): Either[Error, Value]
  2. {:error, reason}Left(reason): Either[Error, Value]
  3. Define error ADT with sealed trait
  4. Use for-comprehensions for chaining operations
  5. Consider Try for exception-based code

Concurrency Patterns

Elixir Async → Scala Async

Elixir Task-based async:

def fetch_user_data(user_id) do
  task1 = Task.async(fn -> fetch_profile(user_id) end)
  task2 = Task.async(fn -> fetch_orders(user_id) end)
  task3 = Task.async(fn -> fetch_preferences(user_id) end)

  profile = Task.await(task1)
  orders = Task.await(task2)
  preferences = Task.await(task3)

  %{profile: profile, orders: orders, preferences: preferences}
end

Scala Future-based async:

import scala.concurrent.{Future, Await}
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global

def fetchUserData(userId: Int): Future[UserData] = {
  val profileF = Future { fetchProfile(userId) }
  val ordersF = Future { fetchOrders(userId) }
  val preferencesF = Future { fetchPreferences(userId) }

  for {
    profile <- profileF
    orders <- ordersF
    preferences <- preferencesF
  } yield UserData(profile, orders, preferences)
}

// Or with Applicative for parallel execution
import cats.implicits._

def fetchUserDataParallel(userId: Int): Future[UserData] = {
  (
    Future { fetchProfile(userId) },
    Future { fetchOrders(userId) },
    Future { fetchPreferences(userId) }
  ).mapN(UserData.apply)
}

Why this translation:

  • Task.asyncFuture { }
  • Task.awaitAwait.result (avoid) or for-comprehension
  • Parallel execution → spawn all Futures immediately
  • Sequential composition → for-comprehension
  • Use Cats for applicative parallel composition

Process Message Passing → Actor Tell/Ask

Elixir:

defmodule Counter do
  def start do
    spawn(fn -> loop(0) end)
  end

  defp loop(count) do
    receive do
      {:increment, caller} ->
        send(caller, {:ok, count + 1})
        loop(count + 1)

      {:get, caller} ->
        send(caller, {:ok, count})
        loop(count)

      :stop ->
        :ok
    end
  end
end

# Usage
counter = Counter.start()
send(counter, {:increment, self()})
receive do
  {:ok, new_value} -> IO.puts("New value: #{new_value}")
end

Scala (Akka Typed):

object Counter {
  sealed trait Command
  case class Increment(replyTo: ActorRef[Response]) extends Command
  case class Get(replyTo: ActorRef[Response]) extends Command
  case object Stop extends Command

  sealed trait Response
  case class Value(count: Int) extends Response

  def apply(initialCount: Int): Behavior[Command] = {
    counter(initialCount)
  }

  private def counter(count: Int): Behavior[Command] = {
    Behaviors.receive { (context, message) =>
      message match {
        case Increment(replyTo) =>
          replyTo ! Value(count + 1)
          counter(count + 1)

        case Get(replyTo) =>
          replyTo ! Value(count)
          Behaviors.same

        case Stop =>
          Behaviors.stopped
      }
    }
  }
}

// Usage
val system = ActorSystem(Counter(0), "counter-system")
implicit val timeout: Timeout = 3.seconds

val futureCount: Future[Counter.Value] =
  system.ask(ref => Counter.Get(ref))

Why this translation:

  • spawnActorSystem / context.spawn
  • send → actor ! (tell) operator
  • receiveBehaviors.receive pattern matching
  • Message tuples → sealed trait protocol
  • Tail recursion → return new behavior

Common Pitfalls

1. Dynamic Typing → Static Typing

Problem: Elixir allows dynamic typing; Scala requires explicit types.

# Elixir - dynamic
def process(data) do
  case data do
    x when is_integer(x) -> x * 2
    x when is_binary(x) -> String.length(x)
    x when is_list(x) -> length(x)
  end
end

Solution: Use sealed trait ADT for type safety.

sealed trait Data
case class IntData(value: Int) extends Data
case class StringData(value: String) extends Data
case class ListData[A](values: List[A]) extends Data

def process(data: Data): Int = data match {
  case IntData(x) => x * 2
  case StringData(x) => x.length
  case ListData(xs) => xs.length
}

2. Pattern Match Exhaustiveness

Problem: Elixir warns on non-exhaustive matches; Scala errors.

# Elixir - warning only
def handle({:ok, value}), do: value
# Missing {:error, _} case

Solution: Always handle all cases or use sealed traits.

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

def handle[A](result: Result[A]): A = result match {
  case Success(value) => value
  case Failure(error) => throw new Exception(error)
  // Compiler enforces exhaustiveness
}

3. Nil Safety

Problem: Elixir has nil; Scala should use Option.

# Elixir
user = Repo.get(User, id)  # Can be nil
name = user.name           # Runtime error if nil

Solution: Use Option type.

val user: Option[User] = Repo.get[User](id)
val name: Option[String] = user.map(_.name)

// Or with for-comprehension
val name = for {
  u <- user
} yield u.name

4. Immutability by Convention → Enforced

Problem: Elixir immutability is by convention; Scala enforces it.

# Elixir - creates new map
map = %{a: 1}
new_map = Map.put(map, :b, 2)

Solution: Use val and immutable collections.

val map = Map("a" -> 1)
val newMap = map + ("b" -> 2)  // Creates new map

// Avoid var and mutable collections
var mutableMap = scala.collection.mutable.Map("a" -> 1)  // Avoid

5. Process Supervision → Actor Supervision

Problem: Different restart semantics and error handling.

Elixir:

# Let it crash - supervisor restarts
def handle_call(:dangerous, _from, state) do
  result = do_dangerous_operation()  # Might crash
  {:reply, result, state}
end

Scala:

// Must handle errors explicitly or define supervision strategy
Behaviors.supervise {
  Behaviors.receiveMessage {
    case Dangerous =>
      try {
        val result = doDangerousOperation()
        Behaviors.same
      } catch {
        case e: Exception =>
          throw e  // Supervisor handles via strategy
      }
  }
}.onFailure[Exception](SupervisorStrategy.restart)

Tooling

| Category | Elixir | Scala | Notes | |----------|--------|-------|-------| | Build Tool | Mix | sbt, Mill | Mill is more modern | | Package Manager | Hex | Maven Central | sbt resolves dependencies | | REPL | iex | scala / ammonite | Ammonite more feature-rich | | Formatter | mix format | scalafmt | Both auto-format | | Linter | Credo | scalafix, Wartremover | Scala has multiple | | Testing | ExUnit | ScalaTest, MUnit | Multiple test frameworks | | Property Testing | StreamData | ScalaCheck | Similar concepts | | Actor System | OTP | Akka | Akka Typed recommended | | Web Framework | Phoenix | Play, Http4s, Akka HTTP | Play most similar | | DB | Ecto | Slick, Doobie, Quill | Different approaches | | JSON | Jason, Poison | Circe, Play JSON, uPickle | Macro-based derivation | | HTTP Client | HTTPoison, Tesla | sttp, http4s-client | Functional HTTP clients |


Examples

Example 1: Simple - Data Transformation

Before (Elixir):

defmodule DataProcessor do
  def transform_user(%{name: name, age: age}) do
    %{
      display_name: String.upcase(name),
      is_adult: age >= 18,
      category: categorize_age(age)
    }
  end

  defp categorize_age(age) when age < 13, do: :child
  defp categorize_age(age) when age < 20, do: :teenager
  defp categorize_age(_), do: :adult
end

After (Scala):

object DataProcessor {
  case class User(name: String, age: Int)

  sealed trait AgeCategory
  case object Child extends AgeCategory
  case object Teenager extends AgeCategory
  case object Adult extends AgeCategory

  case class TransformedUser(
    displayName: String,
    isAdult: Boolean,
    category: AgeCategory
  )

  def transformUser(user: User): TransformedUser = {
    TransformedUser(
      displayName = user.name.toUpperCase,
      isAdult = user.age >= 18,
      category = categorizeAge(user.age)
    )
  }

  private def categorizeAge(age: Int): AgeCategory = age match {
    case a if a < 13 => Child
    case a if a < 20 => Teenager
    case _ => Adult
  }
}

Example 2: Medium - Error Handling Pipeline

Before (Elixir):

defmodule UserRegistration do
  def register(params) do
    with {:ok, validated} <- validate_params(params),
         {:ok, user} <- create_user(validated),
         {:ok, _email} <- send_welcome_email(user) do
      {:ok, user}
    else
      {:error, :invalid_email} -> {:error, "Email is invalid"}
      {:error, :user_exists} -> {:error, "User already exists"}
      {:error, reason} -> {:error, "Registration failed: #{reason}"}
    end
  end

  defp validate_params(%{email: email, password: password})
    when byte_size(password) >= 8 do
    if valid_email?(email) do
      {:ok, %{email: email, password: password}}
    else
      {:error, :invalid_email}
    end
  end
  defp validate_params(_), do: {:error, :invalid_params}
end

After (Scala):

object UserRegistration {
  case class UserParams(email: String, password: String)
  case class User(id: Int, email: String)

  sealed trait RegistrationError
  case object InvalidEmail extends RegistrationError
  case object UserExists extends RegistrationError
  case class GeneralError(message: String) extends RegistrationError

  type RegistrationResult = Either[RegistrationError, User]

  def register(params: UserParams): RegistrationResult = {
    for {
      validated <- validateParams(params)
      user <- createUser(validated)
      _ <- sendWelcomeEmail(user)
    } yield user
  }

  private def validateParams(
    params: UserParams
  ): Either[RegistrationError, UserParams] = {
    if (params.password.length < 8) {
      Left(GeneralError("Password too short"))
    } else if (!validEmail(params.email)) {
      Left(InvalidEmail)
    } else {
      Right(params)
    }
  }

  private def createUser(
    params: UserParams
  ): Either[RegistrationError, User] = {
    // Database logic
    Right(User(1, params.email))
  }

  private def sendWelcomeEmail(
    user: User
  ): Either[RegistrationError, Unit] = {
    // Email logic
    Right(())
  }

  private def validEmail(email: String): Boolean = {
    email.contains("@")
  }
}

Example 3: Complex - GenServer to Akka Actor with State

Before (Elixir):

defmodule RateLimiter do
  use GenServer
  require Logger

  defstruct requests: %{}, window_ms: 60_000, max_requests: 100

  # Client API

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  def check_rate(user_id) do
    GenServer.call(__MODULE__, {:check_rate, user_id})
  end

  def reset(user_id) do
    GenServer.cast(__MODULE__, {:reset, user_id})
  end

  # Server Callbacks

  @impl true
  def init(opts) do
    state = %__MODULE__{
      window_ms: Keyword.get(opts, :window_ms, 60_000),
      max_requests: Keyword.get(opts, :max_requests, 100)
    }
    {:ok, state}
  end

  @impl true
  def handle_call({:check_rate, user_id}, _from, state) do
    now = System.system_time(:millisecond)
    requests = Map.get(state.requests, user_id, [])

    # Filter out expired requests
    valid_requests =
      Enum.filter(requests, fn ts ->
        now - ts < state.window_ms
      end)

    count = length(valid_requests)

    if count < state.max_requests do
      new_requests = [now | valid_requests]
      new_state = put_in(state.requests[user_id], new_requests)
      {:reply, {:ok, state.max_requests - count - 1}, new_state}
    else
      {:reply, {:error, :rate_limit_exceeded}, state}
    end
  end

  @impl true
  def handle_cast({:reset, user_id}, state) do
    new_state = put_in(state.requests[user_id], [])
    {:noreply, new_state}
  end

  @impl true
  def handle_info(:cleanup, state) do
    now = System.system_time(:millisecond)

    new_requests =
      state.requests
      |> Enum.map(fn {user_id, requests} ->
        valid = Enum.filter(requests, fn ts ->
          now - ts < state.window_ms
        end)
        {user_id, valid}
      end)
      |> Enum.into(%{})

    Process.send_after(self(), :cleanup, 60_000)
    {:noreply, %{state | requests: new_requests}}
  end
end

After (Scala with Akka Typed):

import akka.actor.typed._
import akka.actor.typed.scaladsl._
import scala.concurrent.duration._

object RateLimiter {
  // Protocol
  sealed trait Command
  final case class CheckRate(
    userId: String,
    replyTo: ActorRef[RateResponse]
  ) extends Command
  final case class Reset(userId: String) extends Command
  private case object Cleanup extends Command

  // Responses
  sealed trait RateResponse
  final case class Allowed(remaining: Int) extends RateResponse
  case object RateLimitExceeded extends RateResponse

  // Configuration
  case class Config(
    windowMs: Long = 60000,
    maxRequests: Int = 100
  )

  // State
  private case class State(
    requests: Map[String, List[Long]],
    config: Config
  )

  def apply(config: Config = Config()): Behavior[Command] = {
    Behaviors.setup { context =>
      Behaviors.withTimers { timers =>
        timers.startTimerWithFixedDelay(Cleanup, 60.seconds)
        active(State(Map.empty, config), context)
      }
    }
  }

  private def active(
    state: State,
    context: ActorContext[Command]
  ): Behavior[Command] = {
    Behaviors.receiveMessage {
      case CheckRate(userId, replyTo) =>
        val now = System.currentTimeMillis()
        val requests = state.requests.getOrElse(userId, List.empty)

        // Filter expired requests
        val validRequests = requests.filter { ts =>
          now - ts < state.config.windowMs
        }

        val count = validRequests.length

        if (count < state.config.maxRequests) {
          val newRequests = now :: validRequests
          val newState = state.copy(
            requests = state.requests + (userId -> newRequests)
          )
          replyTo ! Allowed(state.config.maxRequests - count - 1)
          active(newState, context)
        } else {
          replyTo ! RateLimitExceeded
          Behaviors.same
        }

      case Reset(userId) =>
        val newState = state.copy(
          requests = state.requests - userId
        )
        active(newState, context)

      case Cleanup =>
        val now = System.currentTimeMillis()
        val cleaned = state.requests.map { case (userId, requests) =>
          val valid = requests.filter { ts =>
            now - ts < state.config.windowMs
          }
          userId -> valid
        }.filter(_._2.nonEmpty)

        active(state.copy(requests = cleaned), context)
    }
  }
}

// Usage
val system: ActorSystem[RateLimiter.Command] =
  ActorSystem(RateLimiter(RateLimiter.Config()), "rate-limiter")

implicit val timeout: Timeout = 3.seconds
val futureResponse: Future[RateLimiter.RateResponse] =
  system.ask(ref => RateLimiter.CheckRate("user123", ref))

See Also

For more examples and patterns, see:

  • meta-convert-dev - Foundational patterns with cross-language examples
  • lang-elixir-dev - Elixir development patterns
  • lang-scala-dev - Scala development patterns
  • lang-scala-akka-dev - Advanced Akka patterns

Cross-cutting pattern skills:

  • patterns-concurrency-dev - Async, actors, processes across languages
  • patterns-serialization-dev - JSON, validation across languages
  • patterns-metaprogramming-dev - Macros, code generation across languages