AgentSkillsCN

convert-elixir-scala

将 Elixir 代码转换为符合 Scala 风格的代码。当需要将 Elixir/Phoenix 应用程序迁移到 Scala、将 BEAM OTP 的编程模式移植到 Akka Actor,或将动态并发编程重构为静态类型化的 JVM 编程模式时,可使用此技能。该技能在 meta-convert-dev 的基础上,新增了专属于 Elixir 到 Scala 的转换模式。

SKILL.md
--- frontmatter
name: convert-elixir-scala
description: Convert Elixir code to idiomatic Scala. 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.

Convert Elixir to Scala

Convert Elixir code to idiomatic Scala. This skill extends meta-convert-dev with Elixir-to-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
  • Reverse conversion (Scala → Elixir) - see convert-scala-elixir
  • Phoenix framework specifics - see lang-scala-play-dev for web frameworks

Quick Reference

ElixirScalaNotes
:atomSymbol / sealed traitAtoms → 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 Moduleobject ModuleModule → singleton object
def func(x)def func(x: Type): ReturnTypeFunctions require type signatures
fn x -> x end(x: Type) => xAnonymous functions
|> (pipe).map().flatMap()Pipe → method chaining
GenServerActor[Protocol]Typed actors in Akka
SupervisorActorSystem / Behaviors.superviseSupervision strategies
Task.asyncFuture { }Async execution
Pattern matchingmatch/caseSimilar but with static types

8 Pillars Validation

Elixir Coverage

PillarCoverageStatus
Module Systemdefmodule, alias, import, use
Error Handling{:ok/:error} tuples, pattern matching, try/rescue
Concurrency ModelProcesses, GenServer, Supervisor, Task
MetaprogrammingMacros, use directive, compile-time generation
Zero/Default Valuesnil, pattern matching with defaults
SerializationPhoenix params, Jason/Poison, Ecto
Build/DepsMix, hex.pm, deps management
Testing IdiomsExUnit, doctests, Mox for mocking

Score: 8/8 (Green)

Scala Coverage

PillarCoverageStatus
Module SystemPackages, objects, imports, traits
Error HandlingEither, Try, Option, exceptions
Concurrency ModelFutures, Akka actors, Cats Effect IO
MetaprogrammingMacros (Scala 2), inline/metaprogramming (Scala 3)
Zero/Default ValuesOption, default parameters, null (avoid)
SerializationPlay JSON, Circe, uPickle, case classes
Build/Depssbt, Mill, Maven Central
Testing IdiomsScalaTest, 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

ElixirScalaNotes
:atomSymbol / sealed traitUse sealed traits for ADTs
integerInt / Long / BigIntChoose based on range needs
floatDouble64-bit floating point
true / falseBooleanDirect mapping
nilNone / null (avoid)Prefer Option[T]
"string"StringImmutable strings
'charlist'List[Char]Less common in Scala

Collection Types

ElixirScalaNotes
[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 listList[(Symbol, A)]Less common in Scala
Range 1..10Range(1, 11)Inclusive → exclusive end

Composite Types

ElixirScalaNotes
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): StringFunction signatures required
ProtocolTraitPolymorphism via traits

Idiom Translation

Pattern: Module Definition

Elixir:

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:

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:

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:

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:

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:

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:

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):

scala
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:

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):

scala
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:

elixir
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:

scala
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:

elixir
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:

scala
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:

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):

scala
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
# 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.

scala
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
# Elixir - warning only
def handle({:ok, value}), do: value
# Missing {:error, _} case

Solution: Always handle all cases or use sealed traits.

scala
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
# Elixir
user = Repo.get(User, id)  # Can be nil
name = user.name           # Runtime error if nil

Solution: Use Option type.

scala
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
# Elixir - creates new map
map = %{a: 1}
new_map = Map.put(map, :b, 2)

Solution: Use val and immutable collections.

scala
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:

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

Scala:

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

CategoryElixirScalaNotes
Build ToolMixsbt, MillMill is more modern
Package ManagerHexMaven Centralsbt resolves dependencies
REPLiexscala / ammoniteAmmonite more feature-rich
Formattermix formatscalafmtBoth auto-format
LinterCredoscalafix, WartremoverScala has multiple
TestingExUnitScalaTest, MUnitMultiple test frameworks
Property TestingStreamDataScalaCheckSimilar concepts
Actor SystemOTPAkkaAkka Typed recommended
Web FrameworkPhoenixPlay, Http4s, Akka HTTPPlay most similar
DBEctoSlick, Doobie, QuillDifferent approaches
JSONJason, PoisonCirce, Play JSON, uPickleMacro-based derivation
HTTP ClientHTTPoison, Teslasttp, http4s-clientFunctional HTTP clients

Examples

Example 1: Simple - Data Transformation

Before (Elixir):

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):

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):

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):

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):

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):

scala
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
  • convert-scala-elixir - Reverse conversion (Scala → Elixir)
  • 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