| name | convert-scala-erlang |
| description | Convert Scala code to idiomatic Erlang. Use when migrating Scala projects to Erlang/OTP, translating JVM functional patterns to BEAM/OTP patterns, or refactoring Scala codebases to leverage Erlang's fault-tolerance and distribution. Extends meta-convert-dev with Scala-to-Erlang specific patterns. |
Convert Scala to Erlang
Convert Scala code to idiomatic Erlang. This skill extends meta-convert-dev with Scala-to-Erlang specific type mappings, idiom translations, and tooling for migrating functional JVM code to the BEAM VM and OTP framework.
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 → Erlang types and records
- Idiom translations: Scala patterns → idiomatic Erlang/OTP
- Error handling: Scala Either/Try/Option → Erlang tuples and let-it-crash
- Concurrency patterns: Scala Future/Akka → Erlang processes and OTP behaviors
- Platform migration: JVM → BEAM VM and OTP
- Memory model shift: JVM shared heap → BEAM per-process heaps
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - Scala language fundamentals - see
lang-scala-dev - Erlang language fundamentals - see
lang-erlang-dev - Reverse conversion (Erlang → Scala) - see
convert-erlang-scala
Quick Reference
| Scala | Erlang | Notes |
|---|---|---|
String |
binary() / list() |
UTF-8 binary or char list |
Int |
integer() |
Arbitrary precision in Erlang |
Long |
integer() |
Same as Int in Erlang |
Double |
float() |
IEEE 754 double |
Boolean |
true / false |
Atoms |
Option[T] |
{ok, Value} | error |
Tagged tuple |
Some(x) |
{ok, X} |
Success tuple |
None |
error / undefined |
Absence |
Either[L,R] |
{ok, Value} | {error, Reason} |
Tagged tuple |
Right(x) |
{ok, X} |
Success |
Left(e) |
{error, Reason} |
Error |
Try[T] |
{ok, Value} | {error, Reason} |
Exception handling |
List[T] |
list() |
Linked list |
Vector[T] |
list() / array() |
List or array module |
Array[T] |
tuple() / array() |
Fixed-size tuple or array |
Map[K,V] |
#{K => V} / maps:map() |
Map literal or maps module |
Set[T] |
sets:set() / ordsets |
Sets module |
case class |
-record(name, {...}) |
Record definition |
sealed trait |
Tagged tuples | Discriminated union |
Future[T] |
pid() / gen_server |
Lightweight process |
object |
-module(name). |
Singleton as module |
When Converting Code
- Analyze source thoroughly before writing target
- Map types first - create type equivalence table
- Preserve semantics over syntax similarity
- Adopt Erlang/OTP idioms - don't write "Scala code in Erlang syntax"
- Embrace let-it-crash - replace defensive programming with supervision
- Handle edge cases - null safety, error paths, process lifecycle
- Test equivalence - same inputs → same outputs
Type System Mapping
Primitive Types
| Scala | Erlang | Notes |
|---|---|---|
String |
binary() |
UTF-8 binary (most common): <<"Hello">> |
String |
string() |
Character list (for compatibility): "Hello" |
Int |
integer() |
32-bit in Scala, arbitrary precision in Erlang |
Long |
integer() |
64-bit in Scala, arbitrary precision in Erlang |
Short |
integer() |
16-bit in Scala, arbitrary precision in Erlang |
Byte |
integer() |
8-bit in Scala, 0-255 in Erlang |
Double |
float() |
IEEE 754 double precision |
Float |
float() |
IEEE 754 single in Scala, double in Erlang |
Boolean |
true / false |
Atoms (lowercase) |
Char |
integer() |
Unicode codepoint |
Unit |
ok |
Atom representing success |
Any |
any() / term() |
Any Erlang term |
Nothing |
N/A | Bottom type, no Erlang equivalent |
Option and Either Types
| Scala | Erlang | Notes |
|---|---|---|
None |
undefined / error |
Atom for absence |
Some(x) |
{ok, X} |
Tagged tuple for presence |
Option[T] |
{ok, Value} | error | undefined |
Common pattern |
Right(x) |
{ok, X} |
Success tuple |
Left(e) |
{error, Reason} |
Error tuple with reason |
Either[L,R] |
{ok, Value} | {error, Reason} |
Standard error pattern |
Success(x) |
{ok, X} |
Try success |
Failure(e) |
{error, Reason} |
Try failure |
Try[T] |
{ok, Value} | {error, Reason} |
Exception handling |
Collection Types
| Scala | Erlang | Notes |
|---|---|---|
List[T] |
list() |
Linked list: [1, 2, 3] |
Vector[T] |
list() |
No direct equivalent, use list |
Array[T] |
tuple() |
Fixed-size: {1, 2, 3} |
Array[T] |
array:array() |
Mutable array module |
Seq[T] |
list() |
Generic sequence → list |
LazyList[T] |
Process-based stream | Stream via gen_server |
Map[K,V] |
#{K => V} |
Map literal (Erlang 17+) |
Map[K,V] |
dict:dict() |
Legacy dict module |
Set[T] |
sets:set() |
Unordered set |
Set[T] |
ordsets:ordset() |
Ordered set (list-based) |
(A, B) (tuple) |
{A, B} |
Tuple literal |
(A, B, C) |
{A, B, C} |
N-tuple |
Range |
lists:seq(Start, End) |
Sequence generation |
Case Classes and Sealed Traits
| Scala | Erlang | Notes |
|---|---|---|
case class Person(name: String, age: Int) |
-record(person, {name :: binary(), age :: integer()}). |
Record definition |
Person("Alice", 30) |
#person{name = <<"Alice">>, age = 30} |
Record creation |
person.name |
Person#person.name |
Field access |
person.copy(age = 31) |
Person#person{age = 31} |
Record update |
sealed trait Shape |
Tagged tuples | Discriminated union |
case class Circle(r: Double) extends Shape |
{circle, Radius} |
Tagged tuple variant |
case class Rectangle(w: Double, h: Double) extends Shape |
{rectangle, Width, Height} |
Tagged tuple variant |
case object Empty extends Shape |
empty |
Atom for singleton |
Function Types
| Scala | Erlang | Notes |
|---|---|---|
A => B |
fun((A) -> B) |
Anonymous function |
(A, B) => C |
fun((A, B) -> C) |
Multi-param function |
A => B => C |
fun((A) -> fun((B) -> C) end end) |
Curried (uncommon in Erlang) |
Function1[A,B] |
fun((A) -> B) |
Function type |
() => A |
fun(() -> A) |
Nullary function |
A => Unit |
fun((A) -> ok) |
Side-effect function |
Generic Types
| Scala | Erlang | Notes |
|---|---|---|
[T] |
term() |
Any type (runtime polymorphism) |
List[T] |
list(T) |
Parameterized type spec |
Option[T] |
{ok, T} | error |
Type spec pattern |
Either[L,R] |
{ok, R} | {error, L} |
Type spec pattern |
Type bound T <: Upper |
Guard clause | when is_record(X, name) |
Type bound T >: Lower |
N/A | No lower bounds in Erlang |
Idiom Translation
Pattern 1: Option Handling
Scala:
def findUser(id: String): Option[User] =
users.find(_.id == id)
val name = findUser("123")
.map(_.name)
.getOrElse("Unknown")
Erlang:
-spec find_user(binary()) -> {ok, user()} | error.
find_user(Id) ->
case lists:search(fun(U) -> maps:get(id, U) =:= Id end, users()) of
{value, User} -> {ok, User};
false -> error
end.
get_name(UserId) ->
case find_user(UserId) of
{ok, User} -> maps:get(name, User);
error -> <<"Unknown">>
end.
Why this translation:
- Scala's
Option.mapbecomes pattern matching in Erlang getOrElsebecomes the error clause in case expression- Erlang uses
{ok, Value}|errortuples instead ofSome/None - Type specs replace Scala type annotations
Pattern 2: Either-Based Error Handling
Scala:
sealed trait Error
case object DivisionByZero extends Error
case class InvalidInput(msg: String) extends Error
def divide(x: Double, y: Double): Either[Error, Double] =
if (y == 0.0) Left(DivisionByZero)
else Right(x / y)
val result = for {
a <- divide(10.0, 2.0)
b <- divide(20.0, 4.0)
c <- divide(a, b)
} yield c
Erlang:
-type error_reason() :: division_by_zero | {invalid_input, binary()}.
-spec divide(float(), float()) -> {ok, float()} | {error, error_reason()}.
divide(_X, 0.0) ->
{error, division_by_zero};
divide(X, Y) ->
{ok, X / Y}.
-spec calculate() -> {ok, float()} | {error, error_reason()}.
calculate() ->
case divide(10.0, 2.0) of
{ok, A} ->
case divide(20.0, 4.0) of
{ok, B} ->
divide(A, B);
{error, Reason} -> {error, Reason}
end;
{error, Reason} -> {error, Reason}
end.
Why this translation:
- Scala for-comprehensions become nested case statements
Either[L,R]maps to{ok, Value} | {error, Reason}tuples- Sealed traits become atoms or tagged tuples
- Pattern matching on error tuples replaces monadic bind
Pattern 3: List Processing
Scala:
val result = items
.filter(_.active)
.map(_.value)
.sum
Erlang:
calculate_result(Items) ->
lists:foldl(
fun(X, Acc) -> Acc + X end,
0,
[maps:get(value, X) || X <- Items, maps:get(active, X)]
).
% Alternative: using lists module functions
calculate_result_alt(Items) ->
Active = lists:filter(fun(X) -> maps:get(active, X) end, Items),
Values = lists:map(fun(X) -> maps:get(value, X) end, Active),
lists:sum(Values).
Why this translation:
- Scala method chaining becomes list comprehension or nested function calls
- List comprehension is more idiomatic for filter+map in Erlang
lists:sum/1directly replaces.sum- Both approaches are valid; comprehension is more concise
Pattern 4: Case Class Pattern Matching
Scala:
case class Person(firstName: String, lastName: String, age: Int)
def getFullName(person: Person): String = person match {
case Person(first, last, _) => s"$first $last"
}
def isAdult(person: Person): Boolean = person match {
case Person(_, _, age) if age >= 18 => true
case _ => false
}
Erlang:
-record(person, {
first_name :: binary(),
last_name :: binary(),
age :: integer()
}).
get_full_name(#person{first_name = First, last_name = Last}) ->
<<First/binary, " ", Last/binary>>.
is_adult(#person{age = Age}) when Age >= 18 ->
true;
is_adult(_) ->
false.
Why this translation:
- Scala case class patterns map to Erlang record patterns
- Guards (
when) work similarly in both languages - Scala string interpolation becomes binary concatenation
- Function clauses with pattern matching replace match expressions
Pattern 5: Sealed Trait / ADT
Scala:
sealed trait Result[+T]
case class Success[T](value: T) extends Result[T]
case class Failure(error: String) extends Result[Nothing]
case object Pending extends Result[Nothing]
def process[T](result: Result[T]): String = result match {
case Success(value) => s"Got: $value"
case Failure(error) => s"Error: $error"
case Pending => "Still waiting..."
}
Erlang:
-type result(T) :: {success, T} | {failure, binary()} | pending.
-spec process(result(term())) -> binary().
process({success, Value}) ->
iolist_to_binary(io_lib:format("Got: ~p", [Value]));
process({failure, Error}) ->
<<"Error: ", Error/binary>>;
process(pending) ->
<<"Still waiting...">>.
Why this translation:
- Sealed traits become tagged tuples or atoms
- Case objects become atoms
- Pattern matching translates directly
- Type parameters in Scala become type variables in specs
Pattern 6: Singleton Object
Scala:
object MathUtils {
val PI = 3.14159
def square(x: Int): Int = x * x
def cube(x: Int): Int = x * x * x
}
// Usage
val result = MathUtils.square(5)
Erlang:
-module(math_utils).
-export([square/1, cube/1]).
-define(PI, 3.14159).
square(X) -> X * X.
cube(X) -> X * X * X.
% Usage
Result = math_utils:square(5).
Why this translation:
- Scala objects become Erlang modules
- Public methods become exported functions
- Constants become macros or module attributes
- Fully qualified calls use module:function syntax
Paradigm Translation
Mental Model Shift: JVM → BEAM
| Scala Concept | Erlang Approach | Key Insight |
|---|---|---|
| Shared mutable state | Isolated process state | Each process has private memory |
| Class with methods | Record + module functions | Data and behavior separated |
| Inheritance | Protocol implementation | Favor behaviors over hierarchies |
| Thread pool | Lightweight processes | Millions of processes, not threads |
| Synchronized blocks | Message passing | No shared memory, communicate via messages |
| Exception handling | Let-it-crash + supervision | Failures are isolated and handled by supervisors |
| Static typing | Dynamic with dialyzer | Runtime flexibility with static analysis |
Concurrency Mental Model
| Scala Pattern | Erlang Pattern | Conceptual Translation |
|---|---|---|
Future[T] |
spawn/1 + message passing |
Async computation → lightweight process |
Promise[T] |
Process mailbox | Future completion → message receipt |
Await.result |
receive ... end |
Blocking wait → selective receive |
| Akka Actor | gen_server behavior |
Stateful actor → OTP behavior |
Akka receive |
handle_call/handle_cast |
Message handling → callback functions |
| Akka Supervisor | OTP supervisor |
Fault tolerance → supervision tree |
ExecutionContext |
Scheduler | Thread pool → BEAM scheduler |
| Thread-safe collections | Process dictionary / ETS | Shared state → process-local or ETS |
Error Handling
Scala Try/Either → Erlang Tuples and Let-it-Crash
Scala defensive style:
def parseAndProcess(input: String): Try[Int] = Try {
val num = input.toInt
if (num < 0) throw new IllegalArgumentException("Negative")
num * 2
}
val result = parseAndProcess("42") match {
case Success(value) => println(s"Result: $value")
case Failure(ex) => println(s"Error: ${ex.getMessage}")
}
Erlang let-it-crash style:
-spec parse_and_process(binary()) -> integer().
parse_and_process(Input) ->
Num = binary_to_integer(Input),
true = Num >= 0, % Crashes if false
Num * 2.
% Caller can catch or let supervisor handle
-spec safe_parse_and_process(binary()) -> {ok, integer()} | {error, term()}.
safe_parse_and_process(Input) ->
try
{ok, parse_and_process(Input)}
catch
_:Reason -> {error, Reason}
end.
Why this translation:
- Erlang prefers crashing over defensive checks
- Supervisors restart failed processes
- Use
{ok, Value} | {error, Reason}for expected errors - Let unexpected errors crash for debugging clarity
Error Propagation Patterns
| Scala Pattern | Erlang Pattern | Notes |
|---|---|---|
Try { ... } |
try ... catch |
Exception handling |
.recover { case ... } |
catch clauses |
Error recovery |
Either.flatMap |
Nested case |
Error propagation |
| Throwing exceptions | throw/exit/error |
Rare in idiomatic Erlang |
| Custom exception classes | Atoms or tuples | Error reasons as atoms |
| Stack trace capture | erlang:get_stacktrace() |
For debugging |
Concurrency Patterns
Scala Future → Erlang Process
Scala:
import scala.concurrent.{Future, ExecutionContext}
import ExecutionContext.Implicits.global
def fetchData(id: String): Future[Data] = Future {
// Simulate async operation
Thread.sleep(1000)
Data(id, "result")
}
val result = for {
data1 <- fetchData("1")
data2 <- fetchData("2")
} yield combine(data1, data2)
result.onComplete {
case Success(combined) => println(combined)
case Failure(ex) => println(s"Error: $ex")
}
Erlang:
-spec fetch_data(binary()) -> data().
fetch_data(Id) ->
timer:sleep(1000),
#{id => Id, result => <<"result">>}.
-spec async_fetch() -> pid().
async_fetch() ->
Parent = self(),
spawn(fun() ->
Data1 = fetch_data(<<"1">>),
Data2 = fetch_data(<<"2">>),
Combined = combine(Data1, Data2),
Parent ! {result, Combined}
end).
% Usage
Pid = async_fetch(),
receive
{result, Combined} -> io:format("~p~n", [Combined])
after 5000 ->
io:format("Timeout~n")
end.
Why this translation:
- Futures become spawned processes
onCompletebecomesreceivepattern matching- Sequential async (for-comprehension) becomes sequential process code
- Timeout handling is explicit in Erlang
Akka Actor → gen_server
Scala (Akka):
import akka.actor.{Actor, ActorRef, Props}
case class Get(key: String)
case class Put(key: String, value: String)
class KeyValueStore extends Actor {
private var store = Map.empty[String, String]
def receive: Receive = {
case Get(key) =>
sender() ! store.get(key)
case Put(key, value) =>
store = store + (key -> value)
sender() ! "OK"
}
}
// Usage
val store = system.actorOf(Props[KeyValueStore])
store ! Put("name", "Alice")
store ! Get("name")
Erlang (gen_server):
-module(kv_store).
-behaviour(gen_server).
-export([start_link/0, get/1, put/2]).
-export([init/1, handle_call/3, handle_cast/2, terminate/2, code_change/3]).
-record(state, {store = #{} :: map()}).
%% API
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
get(Key) ->
gen_server:call(?MODULE, {get, Key}).
put(Key, Value) ->
gen_server:call(?MODULE, {put, Key, Value}).
%% Callbacks
init([]) ->
{ok, #state{}}.
handle_call({get, Key}, _From, State = #state{store = Store}) ->
Result = maps:get(Key, Store, undefined),
{reply, Result, State};
handle_call({put, Key, Value}, _From, State = #state{store = Store}) ->
NewStore = Store#{Key => Value},
{reply, ok, State#state{store = NewStore}}.
handle_cast(_Msg, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
Why this translation:
- Akka actors map to gen_server behaviors
receiveblock becomeshandle_call/handle_castcallbackssender()is implicit in gen_server'sFromparameter- State management is explicit in callback return tuples
- OTP provides more structure than raw Akka actors
Module and Package Structure
Scala Package → Erlang Module
Scala:
package com.example.myapp
object UserService {
def createUser(name: String): User = ???
def deleteUser(id: Int): Unit = ???
}
Erlang:
-module(user_service).
-export([create_user/1, delete_user/1]).
create_user(Name) ->
% implementation
ok.
delete_user(Id) ->
% implementation
ok.
Translation notes:
- Scala packages become application structure (apps/src directories)
- Scala objects become modules
- Package imports become module calls
- No nested modules in Erlang (flat namespace)
Common Pitfalls
Overusing exceptions: Erlang prefers let-it-crash for unexpected errors and
{ok, Value} | {error, Reason}for expected errors. Don't translate everyTrytotry...catch.Shared mutable state: Scala's
varand mutable collections don't translate directly. Use process state or ETS tables instead.Type erasure issues: Scala's generic types are erased at runtime. Erlang is dynamically typed, but Dialyzer type specs help catch errors.
Numeric precision: Scala's
Intis 32-bit, but Erlang'sinteger()is arbitrary precision. Document overflow assumptions.String encoding: Scala strings are UTF-16; Erlang binaries are UTF-8. Use
<<"binary">>syntax for UTF-8 strings.Null handling: Scala has
null(avoid it),None, andOption. Erlang usesundefined,error, or{ok, Value}. Be consistent.Currying: Scala's curried functions are uncommon in Erlang. Flatten to multi-argument functions.
Implicits: Scala's implicit parameters and conversions have no Erlang equivalent. Make dependencies explicit.
Case object equality: Scala case objects use reference equality. Erlang atoms use value equality.
Pattern match exhaustiveness: Scala's compiler checks exhaustiveness; Erlang/Dialyzer warns but doesn't enforce. Add catch-all clauses.
Limitations
Coverage Gaps
| Pillar | Scala Skill | Erlang Skill | Mitigation |
|---|---|---|---|
| Module | ~ | ✓ | Scala packages explained in context |
| Error | ✓ | ✓ | Full coverage |
| Concurrency | ✓ | ✓ | Full coverage |
| Metaprogramming | ~ | ✓ | Scala macros mentioned where relevant |
| Zero/Default | ~ | ✓ | Option/None covered in idioms |
| Serialization | ~ | ✓ | Refer to patterns-serialization-dev |
| Build | ✓ | ✓ | Full coverage |
| Testing | ✓ | ✓ | Full coverage |
Combined Score: 13.5/16 (Good - proceed with pattern skill references)
Known Limitations
Serialization: This skill has limited guidance on JSON/serialization libraries because lang-scala-dev lacks dedicated serialization coverage. Refer to
patterns-serialization-devfor JSON handling patterns.Metaprogramming: Scala macros and compile-time metaprogramming have limited coverage. Erlang macros and parse transforms are covered in lang-erlang-dev.
Module systems: Scala's package objects and imports are not fully covered. Module structure patterns are provided in context.
External Resources Used
| Resource | What It Provided | Reliability |
|---|---|---|
| Scala official docs | Type system, collections API | High |
| Erlang official docs | OTP behaviors, gen_server patterns | High |
| lang-scala-dev skill | Scala fundamentals, concurrency | High |
| lang-erlang-dev skill | Erlang patterns, OTP | High |
| convert-erlang-scala | Reverse conversion insights | High |
Tooling
| Tool | Purpose | Notes |
|---|---|---|
| rebar3 | Erlang build tool | Successor to rebar, standard for modern Erlang |
| Dialyzer | Static analysis | Catches type errors despite dynamic typing |
| EUnit | Unit testing | Built-in test framework |
| Common Test | Integration testing | OTP's comprehensive test framework |
| Observer | Live system inspection | GUI for monitoring processes, ETS, applications |
| Recon | Production debugging | Runtime inspection and debugging |
| PropEr | Property-based testing | QuickCheck-like testing for Erlang |
| Elvis | Style checker | Erlang linter and style enforcer |
| Relx | Release building | Creates production releases |
| Hex | Package manager | Erlang/Elixir package registry |
No direct transpilers exist from Scala to Erlang. Manual conversion is required.
Examples
Example 1: Simple - Type and Function Translation
Before (Scala):
case class Point(x: Double, y: Double)
def distance(p1: Point, p2: Point): Double = {
val dx = p2.x - p1.x
val dy = p2.y - p1.y
Math.sqrt(dx * dx + dy * dy)
}
val origin = Point(0.0, 0.0)
val point = Point(3.0, 4.0)
val dist = distance(origin, point) // 5.0
After (Erlang):
-module(geometry).
-export([distance/2]).
-record(point, {x :: float(), y :: float()}).
distance(P1, P2) ->
Dx = P2#point.x - P1#point.x,
Dy = P2#point.y - P1#point.y,
math:sqrt(Dx * Dx + Dy * Dy).
% Usage
Origin = #point{x = 0.0, y = 0.0},
Point = #point{x = 3.0, y = 4.0},
Dist = distance(Origin, Point). % 5.0
Example 2: Medium - Option and Error Handling
Before (Scala):
sealed trait ValidationError
case class InvalidEmail(email: String) extends ValidationError
case class UserNotFound(id: Int) extends ValidationError
case class User(id: Int, name: String, email: String)
def validateEmail(email: String): Either[ValidationError, String] =
if (email.contains("@")) Right(email)
else Left(InvalidEmail(email))
def findUser(id: Int): Option[User] =
// Simulated database lookup
if (id == 1) Some(User(1, "Alice", "alice@example.com"))
else None
def getUserEmail(id: Int): Either[ValidationError, String] = {
for {
user <- findUser(id).toRight(UserNotFound(id))
validEmail <- validateEmail(user.email)
} yield validEmail
}
// Usage
getUserEmail(1) match {
case Right(email) => println(s"Email: $email")
case Left(InvalidEmail(e)) => println(s"Invalid email: $e")
case Left(UserNotFound(id)) => println(s"User $id not found")
}
After (Erlang):
-module(user_validator).
-export([get_user_email/1]).
-record(user, {id :: integer(), name :: binary(), email :: binary()}).
-type validation_error() ::
{invalid_email, binary()} |
{user_not_found, integer()}.
-spec validate_email(binary()) -> {ok, binary()} | {error, validation_error()}.
validate_email(Email) ->
case binary:match(Email, <<"@">>) of
{_, _} -> {ok, Email};
nomatch -> {error, {invalid_email, Email}}
end.
-spec find_user(integer()) -> {ok, user()} | error.
find_user(1) ->
{ok, #user{id = 1, name = <<"Alice">>, email = <<"alice@example.com">>}};
find_user(_) ->
error.
-spec get_user_email(integer()) -> {ok, binary()} | {error, validation_error()}.
get_user_email(Id) ->
case find_user(Id) of
{ok, User} ->
validate_email(User#user.email);
error ->
{error, {user_not_found, Id}}
end.
% Usage
case get_user_email(1) of
{ok, Email} ->
io:format("Email: ~s~n", [Email]);
{error, {invalid_email, E}} ->
io:format("Invalid email: ~s~n", [E]);
{error, {user_not_found, Id}} ->
io:format("User ~p not found~n", [Id])
end.
Example 3: Complex - Actor/Process with State Management
Before (Scala with Akka):
import akka.actor.{Actor, ActorRef, Props}
import scala.collection.mutable
case class Subscribe(topic: String, subscriber: ActorRef)
case class Unsubscribe(topic: String, subscriber: ActorRef)
case class Publish(topic: String, message: String)
case class GetSubscribers(topic: String)
class PubSubBroker extends Actor {
private val subscriptions = mutable.Map.empty[String, Set[ActorRef]]
def receive: Receive = {
case Subscribe(topic, subscriber) =>
val current = subscriptions.getOrElse(topic, Set.empty)
subscriptions(topic) = current + subscriber
sender() ! "Subscribed"
case Unsubscribe(topic, subscriber) =>
subscriptions.get(topic).foreach { subs =>
subscriptions(topic) = subs - subscriber
}
sender() ! "Unsubscribed"
case Publish(topic, message) =>
subscriptions.get(topic).foreach { subscribers =>
subscribers.foreach(_ ! message)
}
sender() ! "Published"
case GetSubscribers(topic) =>
val subs = subscriptions.getOrElse(topic, Set.empty)
sender() ! subs.size
}
}
// Usage
val broker = system.actorOf(Props[PubSubBroker], "broker")
broker ! Subscribe("news", subscriberActor)
broker ! Publish("news", "Breaking news!")
After (Erlang with gen_server):
-module(pubsub_broker).
-behaviour(gen_server).
-export([start_link/0, subscribe/2, unsubscribe/2, publish/2, get_subscribers/1]).
-export([init/1, handle_call/3, handle_cast/2, terminate/2, code_change/3]).
-record(state, {
subscriptions = #{} :: #{binary() => [pid()]}
}).
%% API
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
subscribe(Topic, Subscriber) ->
gen_server:call(?MODULE, {subscribe, Topic, Subscriber}).
unsubscribe(Topic, Subscriber) ->
gen_server:call(?MODULE, {unsubscribe, Topic, Subscriber}).
publish(Topic, Message) ->
gen_server:cast(?MODULE, {publish, Topic, Message}).
get_subscribers(Topic) ->
gen_server:call(?MODULE, {get_subscribers, Topic}).
%% Callbacks
init([]) ->
{ok, #state{}}.
handle_call({subscribe, Topic, Subscriber}, _From, State = #state{subscriptions = Subs}) ->
Current = maps:get(Topic, Subs, []),
NewSubs = case lists:member(Subscriber, Current) of
true -> Subs;
false -> Subs#{Topic => [Subscriber | Current]}
end,
{reply, subscribed, State#state{subscriptions = NewSubs}};
handle_call({unsubscribe, Topic, Subscriber}, _From, State = #state{subscriptions = Subs}) ->
NewSubs = case maps:get(Topic, Subs, []) of
[] -> Subs;
List ->
Subs#{Topic => lists:delete(Subscriber, List)}
end,
{reply, unsubscribed, State#state{subscriptions = NewSubs}};
handle_call({get_subscribers, Topic}, _From, State = #state{subscriptions = Subs}) ->
Count = length(maps:get(Topic, Subs, [])),
{reply, Count, State}.
handle_cast({publish, Topic, Message}, State = #state{subscriptions = Subs}) ->
Subscribers = maps:get(Topic, Subs, []),
lists:foreach(fun(Sub) -> Sub ! {message, Topic, Message} end, Subscribers),
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
% Usage
{ok, _Pid} = pubsub_broker:start_link(),
pubsub_broker:subscribe(<<"news">>, self()),
pubsub_broker:publish(<<"news">>, <<"Breaking news!">>),
receive
{message, <<"news">>, Msg} ->
io:format("Received: ~s~n", [Msg])
after 5000 ->
io:format("Timeout~n")
end.
Key differences in this example:
- Akka's mutable Map becomes immutable Map in gen_server state
sender()is handled implicitly by gen_server'sFromparameter- Fire-and-forget messages (
!) map tohandle_cast - Request-reply messages map to
handle_call - Supervision and lifecycle are managed by OTP
See Also
For more examples and patterns, see:
meta-convert-dev- Foundational patterns with cross-language examplesconvert-erlang-scala- Reverse conversion (Erlang → Scala)convert-fsharp-erlang- Similar functional JVM → BEAM conversionlang-scala-dev- Scala development patternslang-erlang-dev- Erlang development patterns
Cross-cutting pattern skills (for areas not fully covered by lang-*-dev):
patterns-concurrency-dev- Async, channels, actors across languagespatterns-serialization-dev- JSON, validation across languagespatterns-metaprogramming-dev- Macros, compile-time code generation