Claude-skill-registry convert-scala-roc

Convert Scala code to idiomatic Roc. Use when migrating Scala projects to Roc, translating JVM/FP patterns to pure functional patterns, or refactoring Scala codebases. Extends meta-convert-dev with Scala-to-Roc specific patterns.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/convert-scala-roc" ~/.claude/skills/majiayu000-claude-skill-registry-convert-scala-roc && rm -rf "$T"
manifest: skills/data/convert-scala-roc/SKILL.md
source content

Convert Scala to Roc

Convert Scala code to idiomatic Roc. This skill extends

meta-convert-dev
with Scala-to-Roc specific type mappings, idiom translations, and architectural patterns for moving from JVM-based functional programming to platform-based pure functional programming.

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 JVM types → Roc static types
  • Paradigm translation: Object-functional hybrid → Pure functional with platform separation
  • Idiom translations: Scala patterns → Roc functional patterns
  • Error handling: Exceptions + Try/Either → Result types
  • Concurrency: Futures/Actors → Platform Tasks
  • Module system: Scala packages/objects → Roc platform/application architecture
  • Type classes: Scala implicits/given → Roc abilities

This Skill Does NOT Cover

  • General conversion methodology - see
    meta-convert-dev
  • Scala language fundamentals - see
    lang-scala-dev
  • Roc language fundamentals - see
    lang-roc-dev
  • Reverse conversion (Roc → Scala) - see
    convert-roc-scala

Quick Reference

ScalaRocNotes
Int
I64
/
I32
Specify bit width
Long
I64
64-bit signed
Double
F64
64-bit float
Boolean
Bool
Direct mapping
String
Str
UTF-8 strings
Option[A]
[Some A, None]
Tag union
Either[L, R]
Result R L
Note: order swapped
Try[A]
Result A [Err Str]
Exception → error tag
List[A]
List A
Immutable list
Vector[A]
List A
Roc List is efficient
Set[A]
Set A
Unique values
Map[K, V]
Dict K V
Key-value map
case class
Record
{ }
Structural records
sealed trait
Tag union
[]
Sum types
trait
(interface)
AbilityType class pattern
Future[A]
Task A err
Platform-provided
Unit
{}
Empty record

When Converting Code

  1. Analyze JVM semantics before writing Roc
  2. Identify effect boundaries - separate pure logic from I/O
  3. Map object hierarchies to data - classes become records, inheritance becomes composition
  4. Redesign for immutability - Scala's var becomes Roc's pure transformation
  5. Extract pure functions - separate computation from effects
  6. Test equivalence - verify behavior matches despite architectural differences

Paradigm Translation

Mental Model Shift: Object-Functional → Pure Functional + Platform

Scala ConceptRoc ApproachKey Insight
Class with stateRecord + functions operating on recordData and behavior separated, no hidden state
InheritanceComposition with recordsFavor records and tag unions over class hierarchies
var (mutation)New value creationExplicit transformation, not mutation
Companion objectModule with functionsNamespace for related functions
Implicit parameterAbility constraintType class pattern via abilities
FuturePlatform TaskEffects are platform capability
ActorPlatform concernConcurrency handled by host
Singleton objectModule-level constantsGlobal state avoided, use module scope
Trait mixingRecord compositionCombine records, not behaviors

Functional Paradigm Alignment

Scala PatternRoc PatternConceptual Translation
for
comprehension
Pipeline
|>
or nested
when
Monadic composition becomes explicit
Pattern matching
when
expression
Similar syntax, structural matching
Case classRecord typeStructural types, automatic equality
Sealed trait ADTTag unionSum types with exhaustiveness
Implicit conversionNo equivalentExplicit conversions preferred
Higher-order functionFunction typesDirect support, same concept

Type System Mapping

Primitive Types

ScalaRocNotes
Byte
I8
8-bit signed
Short
I16
16-bit signed
Int
I32
32-bit signed (common)
Long
I64
64-bit signed
Float
F32
32-bit float
Double
F64
64-bit float
Boolean
Bool
Direct mapping
Char
U32
Unicode scalar value
String
Str
UTF-8 strings
Unit
{}
Empty record
Nothing
-No direct equivalent
Any
-Avoid; use tag unions
AnyVal
-Not needed in Roc
AnyRef
-No reference types

Collection Types

ScalaRocNotes
List[A]
List A
Immutable, efficient
Vector[A]
List A
Roc List performs well
Array[A]
List A
No mutable arrays
Set[A]
Set A
Unique values
Map[K, V]
Dict K V
Hash + Eq required for K
Seq[A]
List A
General sequence → List
IndexedSeq[A]
List A
Use List for indexing
LazyList[A]
Generator patternLazy evaluation via functions
Option[A]
[Some A, None]
Optional values
Either[L, R]
Result R L
Note: order reversed
Try[A]
Result A [Err Str]
Exception handling

Composite Types

ScalaRocNotes
case class User(...)
User : { name : Str, ... }
Records are structural
sealed trait Color
Color : [Red, Green, Blue]
Sum types (ADTs)
trait Service
Ability or moduleDepends on use case
object Utils
interface Utils
Module with functions
(A, B)
(A, B)
Tuples map directly
(A, B, C)
(A, B, C)
Multi-element tuples
Generics
[A]
Type parameters
a
Similar concept
Variance
[+A]
-Roc doesn't need variance

Function Types

ScalaRocNotes
() => R
{} -> R
Zero-arg function
A => R
A -> R
Single arg
(A, B) => R
A, B -> R
Multiple args
Function1[A, B]
A -> B
Function type
A => B => C
A -> (B -> C)
Curried functions
By-name
=> A
{} -> A
Lazy evaluation

Idiom Translation

Pattern 1: Simple Function and Case Class

Scala:

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

object User {
  def create(name: String, age: Int, email: String): User = {
    User(name, age, email)
  }

  def greet(user: User): String = {
    s"Hello, ${user.name}! You are ${user.age} years old."
  }
}

Roc:

interface User
    exposes [User, create, greet]
    imports []

User : {
    name : Str,
    age : U32,
    email : Str,
}

create : Str, U32, Str -> User
create = \name, age, email ->
    { name, age, email }

greet : User -> Str
greet = \{ name, age } ->
    "Hello, \(name)! You are \(Num.toStr(age)) years old."

Why this translation:

  • Scala case class → Roc record type
  • Companion object → Roc interface (module)
  • String interpolation syntax differs
  • Type inference works in both

Pattern 2: Sealed Trait ADT with Pattern Matching

Scala:

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

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

Roc:

Result a : [Success a, Failure Str, Pending]

handle : Result a -> Str where a implements Inspect
handle = \result ->
    when result is
        Success(value) -> "Got: \(Inspect.toStr(value))"
        Failure(error) -> "Error: \(error)"
        Pending -> "Waiting..."

Why this translation:

  • Sealed trait → Tag union
  • Case classes → Tags with payloads
  • Case object → Tag without payload
  • Pattern matching syntax very similar
  • Roc enforces exhaustiveness at compile time

Pattern 3: Option Handling

Scala:

def findUser(id: Int, users: List[User]): Option[User] = {
  users.find(_.id == id)
}

def getEmail(maybeUser: Option[User]): String = {
  maybeUser.map(_.email).getOrElse("no email")
}

// For-comprehension
def combineUsers(id1: Int, id2: Int): Option[(User, User)] = {
  for {
    user1 <- findUser(id1, users)
    user2 <- findUser(id2, users)
  } yield (user1, user2)
}

Roc:

findUser : U64, List User -> [Some User, None]
findUser = \id, users ->
    users
    |> List.findFirst(\user -> user.id == id)
    |> Result.map(Some)
    |> Result.withDefault(None)

getEmail : [Some User, None] -> Str
getEmail = \maybeUser ->
    when maybeUser is
        Some({ email }) -> email
        None -> "no email"

# Nested when for comprehension-like flow
combineUsers : U64, U64, List User -> [Some (User, User), None]
combineUsers = \id1, id2, users ->
    when findUser(id1, users) is
        Some(user1) ->
            when findUser(id2, users) is
                Some(user2) -> Some((user1, user2))
                None -> None
        None -> None

Why this translation:

  • Scala Option → Roc tag union
    [Some a, None]
  • map
    /
    getOrElse
    → pattern matching or Result helpers
  • For-comprehension → nested
    when
    expressions
  • More verbose but explicit

Pattern 4: List Processing

Scala:

val numbers = List(1, 2, 3, 4, 5)

val doubled = numbers.map(_ * 2)
val evens = numbers.filter(_ % 2 == 0)
val sum = numbers.foldLeft(0)(_ + _)

// List comprehension
val squares = for {
  x <- numbers
  if x % 2 == 0
} yield x * x

Roc:

numbers = [1, 2, 3, 4, 5]

doubled = List.map(numbers, \n -> n * 2)
evens = List.keepIf(numbers, \n -> n % 2 == 0)
sum = List.walk(numbers, 0, Num.add)

# List comprehension becomes pipeline
squares = numbers
    |> List.keepIf(\x -> x % 2 == 0)
    |> List.map(\x -> x * x)

Why this translation:

  • Similar higher-order functions
  • foldLeft
    List.walk
  • filter
    List.keepIf
  • For-comprehension → pipeline with map/filter
  • Roc uses explicit function composition

Pattern 5: Error Handling with Either/Try

Scala:

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

def calculate(a: Int, b: Int, c: Int): Either[String, Int] = {
  for {
    x <- divide(a, b)
    y <- divide(x, c)
  } yield y
}

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

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

def safeParse(s: String): Option[Int] = parseInt(s).toOption

Roc:

divide : I64, I64 -> Result I64 [DivByZero]
divide = \a, b ->
    if b == 0 then
        Err(DivByZero)
    else
        Ok(a // b)

calculate : I64, I64, I64 -> Result I64 [DivByZero]
calculate = \a, b, c ->
    x = divide!(a, b)
    y = divide!(x, c)
    Ok(y)

# Try equivalent
parseInt : Str -> Result I64 [ParseError]
parseInt = \s ->
    when Str.toI64(s) is
        Ok(n) -> Ok(n)
        Err(_) -> Err(ParseError)

safeParse : Str -> [Some I64, None]
safeParse = \s ->
    when parseInt(s) is
        Ok(n) -> Some(n)
        Err(_) -> None

Why this translation:

  • Either[L, R]
    Result ok err
    (note: order reversed)
  • For-comprehension → try operator
    !
    for early returns
  • Try
    Result
    with explicit error types
  • toOption
    → pattern matching to convert Result → Option-like tag union

Pattern 6: Trait and Implicits to Abilities

Scala:

trait Show[A] {
  def show(a: A): String
}

object Show {
  implicit val intShow: Show[Int] = (a: Int) => a.toString
  implicit val stringShow: Show[String] = (a: String) => s"'$a'"
}

def print[A](a: A)(implicit s: Show[A]): Unit = {
  println(s.show(a))
}

print(42)      // Uses intShow
print("hello") // Uses stringShow

Roc:

# Roc abilities are automatic for basic types
# For custom behavior, use functions with ability constraints

toString : a -> Str where a implements Inspect
toString = \value ->
    Inspect.toStr(value)

# Usage - Inspect is automatically implemented
expect toString(42) == "42"
expect toString("hello") == "\"hello\""

# For custom types, abilities are derived automatically
User : { name : Str, age : U32 }
user = { name: "Alice", age: 30 }

expect Inspect.toStr(user) == "{ name: \"Alice\", age: 30 }"

Why this translation:

  • Scala trait → Roc ability
  • Implicit instances → automatic derivation for records/tags
  • Type class pattern → ability constraint
    where a implements Ability
  • Roc has fewer built-in abilities but they're more automatic

Pattern 7: Higher-Order Functions and Currying

Scala:

def applyTwice[A](f: A => A, x: A): A = f(f(x))

def add(a: Int)(b: Int): Int = a + b
val add5 = add(5) _

def compose[A, B, C](f: B => C, g: A => B): A => C = {
  a => f(g(a))
}

Roc:

applyTwice : (a -> a), a -> a
applyTwice = \f, x ->
    f(f(x))

# Currying in Roc requires explicit function return
add : I64 -> (I64 -> I64)
add = \a ->
    \b -> a + b

add5 = add(5)

compose : (b -> c), (a -> b) -> (a -> c)
compose = \f, g ->
    \a -> f(g(a))

Why this translation:

  • Higher-order functions work similarly
  • Currying must be explicit in Roc (return a function)
  • Function composition same concept
  • Type signatures use arrows consistently

Pattern 8: Records with Update

Scala:

case class Config(
  host: String,
  port: Int,
  timeout: Int = 5000,
  retries: Int = 3
)

val config = Config("localhost", 8080)
val updated = config.copy(port = 9090, retries = 5)

Roc:

Config : {
    host : Str,
    port : U16,
    timeout : U32,
    retries : U32,
}

defaultConfig : Str, U16 -> Config
defaultConfig = \host, port ->
    {
        host,
        port,
        timeout: 5000,
        retries: 3,
    }

config = defaultConfig("localhost", 8080)
updated = { config &
    port: 9090,
    retries: 5,
}

Why this translation:

  • Case class
    copy
    → Roc record update
    { record & field: value }
  • Default parameters → constructor function with defaults
  • Immutable updates work similarly
  • Roc update syntax is explicit

Concurrency Patterns

Scala Future vs Roc Task

Scala uses Futures for async computation on the JVM. Roc delegates all concurrency to the platform.

Scala:

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

def fetchUser(id: Int): Future[User] = Future {
  // Async operation
  database.query(s"SELECT * FROM users WHERE id = $id")
}

def fetchPosts(userId: Int): Future[List[Post]] = Future {
  database.query(s"SELECT * FROM posts WHERE author = $userId")
}

// Composition
val result: Future[(User, List[Post])] = for {
  user <- fetchUser(123)
  posts <- fetchPosts(user.id)
} yield (user, posts)

// Parallel execution
val users: Future[List[User]] = Future.sequence(
  List(1, 2, 3).map(fetchUser)
)

Roc:

import pf.Task exposing [Task]
import pf.Database

# Platform provides Task type
fetchUser : U64 -> Task User [DbErr]
fetchUser = \id ->
    Database.query!("SELECT * FROM users WHERE id = \(Num.toStr(id))")

fetchPosts : U64 -> Task (List Post) [DbErr]
fetchPosts = \userId ->
    Database.query!("SELECT * FROM posts WHERE author = \(Num.toStr(userId))")

# Sequential composition using !
result : Task (User, List Post) [DbErr]
result =
    user = fetchUser!(123)
    posts = fetchPosts!(user.id)
    Task.ok((user, posts))

# Platform provides parallel primitives
users : Task (List User) [DbErr]
users =
    Task.sequence([
        fetchUser(1),
        fetchUser(2),
        fetchUser(3),
    ])

Why this translation:

  • Scala Future → Roc platform Task
  • For-comprehension → try operator
    !
    with Task
  • ExecutionContext → handled by platform
  • Parallel execution → platform-provided primitives
  • Roc apps stay pure, platform handles concurrency

Scala Actors (Akka) vs Roc Platform

Scala (Akka Typed):

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

sealed trait CounterMsg
case object Increment extends CounterMsg
case class GetCount(replyTo: ActorRef[Int]) extends CounterMsg

def counter(count: Int): Behavior[CounterMsg] =
  Behaviors.receive { (context, message) =>
    message match {
      case Increment =>
        counter(count + 1)
      case GetCount(replyTo) =>
        replyTo ! count
        Behaviors.same
    }
  }

Roc:

# Roc has no built-in actors
# Design as pure state machine

State : I64

init : State
init = 0

increment : State -> State
increment = \count ->
    count + 1

getCount : State -> I64
getCount = \count ->
    count

# Platform would provide state management if needed
# Application code remains pure

Why this translation:

  • Actors → pure state functions
  • Message passing → function parameters
  • State mutation → new state returned
  • Platform handles concurrency, not application
  • Simpler mental model: data transformation, not processes

Module System Translation

Scala Package/Object → Roc Interface

Scala:

package com.example.users

case class User(id: Int, name: String, email: String)

object UserService {
  def create(name: String, email: String): User = {
    val id = generateId()
    User(id, name, email)
  }

  def validate(user: User): Either[String, User] = {
    if (user.email.contains("@")) Right(user)
    else Left("Invalid email")
  }
}

Roc:

interface UserService
    exposes [User, create, validate]
    imports []

User : {
    id : U64,
    name : Str,
    email : Str,
}

create : Str, Str -> User
create = \name, email ->
    id = generateId({})
    { id, name, email }

validate : User -> Result User [InvalidEmail]
validate = \user ->
    if Str.contains(user.email, "@") then
        Ok(user)
    else
        Err(InvalidEmail)

# Private helper (not in exposes)
generateId : {} -> U64
generateId = \{} ->
    # Implementation
    123

Why this translation:

  • Package → Roc module structure (file organization)
  • Companion object → Interface exposing functions
  • Private members → not in
    exposes
    list
  • Public API → explicitly listed in
    exposes

Common Pitfalls

1. Trying to Use Mutable State

Scala (Anti-pattern in Roc):

var counter = 0
def increment(): Unit = { counter += 1 }

Roc Approach:

# No mutable state - return new value
increment : I64 -> I64
increment = \counter ->
    counter + 1

# Usage
counter = 0
newCounter = increment(counter)

Why: Roc has no mutable variables. Always return new values.

2. Expecting JVM Collections Performance Characteristics

Pitfall: Assuming Scala Vector performance in Roc.

Solution: Roc List is the primary collection. It's efficient for most use cases. Don't over-optimize based on JVM knowledge.

3. Trying to Use Null

Scala:

var maybeUser: User = null  // Avoid!
val user: Option[User] = Option(nullableValue)

Roc:

# No null! Use tag unions
maybeUser : [Some User, None]
maybeUser = None

# When converting from nullable source
userFromNullable : [Some User, None]
userFromNullable = Some({ name: "Alice", age: 30 })

Why: Roc has no null. Always use tag unions for optional values.

4. Confusing Either Order

Pitfall: Scala

Either[L, R]
vs Roc
Result ok err

Scala:

val result: Either[String, Int] = Right(42)  // Right is success

Roc:

result : Result I64 Str  # First param is success, second is error
result = Ok(42)

Why: Roc Result has opposite parameter order compared to Scala Either.

5. Expecting Implicit Conversions

Pitfall: Scala's implicit conversions don't exist in Roc.

Solution: All conversions must be explicit:

# Explicit conversion required
intToStr : I64 -> Str
intToStr = Num.toStr

str = intToStr(42)

6. Forgetting Platform Separation

Pitfall: Trying to do I/O directly in application code.

Solution: Use platform-provided Tasks:

# Wrong - no direct I/O
# readFile("path")  # This doesn't exist!

# Correct - platform Task
import pf.File
import pf.Task exposing [Task]

readFile : Str -> Task Str [FileErr]
readFile = \path ->
    File.readUtf8(path)

Why: Roc applications are pure. All effects go through the platform.


Tooling

PurposeScalaRocNotes
Build toolsbt, Mill, Maven
roc
CLI
Roc has built-in build
Package managersbt, MavenPlatform dependenciesPlatforms are URLs
TestingScalaTest, MUnit
roc test
Inline
expect
statements
REPL
scala
REPL
roc repl
Interactive evaluation
FormatterScalafmt
roc format
Built-in formatter
Type checkingscalac
roc check
Fast type checking
DocumentationScaladocComments in codeMarkdown in interfaces

Examples

Example 1: Simple HTTP Client

Scala (Akka HTTP):

import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model._
import scala.concurrent.Future

implicit val system = ActorSystem()
import system.dispatcher

def fetchUrl(url: String): Future[String] = {
  Http().singleRequest(HttpRequest(uri = url)).flatMap { response =>
    response.entity.toStrict(5.seconds).map(_.data.utf8String)
  }
}

val content: Future[String] = fetchUrl("https://example.com")

Roc (basic-cli platform):

app [main] {
    pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br"
}

import pf.Http
import pf.Task exposing [Task]
import pf.Stdout

fetchUrl : Str -> Task Str [HttpErr]
fetchUrl = \url ->
    response = Http.get!(url)
    Task.ok(response.body)

main : Task {} []
main =
    content = fetchUrl!("https://example.com")
    Stdout.line!(content)

Example 2: Data Processing Pipeline

Scala:

case class User(id: Int, name: String, age: Int, active: Boolean)

val users = List(
  User(1, "Alice", 30, true),
  User(2, "Bob", 25, false),
  User(3, "Charlie", 35, true)
)

val activeUserNames = users
  .filter(_.active)
  .filter(_.age >= 30)
  .map(_.name)
  .sorted

// Result: List("Alice", "Charlie")

Roc:

User : { id : U64, name : Str, age : U32, active : Bool }

users = [
    { id: 1, name: "Alice", age: 30, active: Bool.true },
    { id: 2, name: "Bob", age: 25, active: Bool.false },
    { id: 3, name: "Charlie", age: 35, active: Bool.true },
]

activeUserNames = users
    |> List.keepIf(\user -> user.active)
    |> List.keepIf(\user -> user.age >= 30)
    |> List.map(\user -> user.name)
    |> List.sortAsc

# Result: ["Alice", "Charlie"]

Example 3: Error Handling Pipeline

Scala:

def parseAndDivide(aStr: String, bStr: String): Either[String, Int] = {
  for {
    a <- aStr.toIntOption.toRight(s"Invalid a: $aStr")
    b <- bStr.toIntOption.toRight(s"Invalid b: $bStr")
    result <- if (b != 0) Right(a / b) else Left("Division by zero")
  } yield result
}

parseAndDivide("10", "2")  // Right(5)
parseAndDivide("10", "0")  // Left("Division by zero")
parseAndDivide("abc", "2") // Left("Invalid a: abc")

Roc:

parseAndDivide : Str, Str -> Result I64 [InvalidA, InvalidB, DivByZero]
parseAndDivide = \aStr, bStr ->
    a =
        when Str.toI64(aStr) is
            Ok(n) -> Ok(n)
            Err(_) -> Err(InvalidA)

    b =
        when Str.toI64(bStr) is
            Ok(n) -> Ok(n)
            Err(_) -> Err(InvalidB)

    # Using try operator for early returns
    aVal = a!
    bVal = b!

    if bVal == 0 then
        Err(DivByZero)
    else
        Ok(aVal // bVal)

expect parseAndDivide("10", "2") == Ok(5)
expect parseAndDivide("10", "0") == Err(DivByZero)
expect parseAndDivide("abc", "2") == Err(InvalidA)

Performance Considerations

Scala vs Roc Performance Differences

AspectScalaRocImpact
RuntimeJVM (GC, JIT)Native compilationRoc generally faster startup, lower memory
CollectionsOptimized for JVMNative data structuresDifferent performance characteristics
ConcurrencyThread pool, asyncPlatform-managedDepends on platform implementation
MemoryHeap-based, GCPlatform-managedLower overhead in Roc
StartupJVM warmup timeInstantRoc has no warmup period

Optimization Tips

  1. Don't over-optimize based on JVM knowledge - Roc's performance profile is different
  2. Trust List performance - It's the primary collection and is well-optimized
  3. Leverage platform capabilities - Let platform handle concurrency and I/O
  4. Profile before optimizing - Different bottlenecks than JVM code
  5. Avoid premature abstraction - Roc encourages simple, direct code

See Also

For more examples and patterns, see:

  • meta-convert-dev
    - Foundational patterns with cross-language examples
  • lang-scala-dev
    - Scala development patterns
  • lang-roc-dev
    - Roc development patterns
  • convert-erlang-roc
    - Similar functional language conversion (BEAM → Roc)

Cross-cutting pattern skills:

  • patterns-concurrency-dev
    - Futures/Actors vs Tasks across languages
  • patterns-serialization-dev
    - JSON, validation across languages
  • patterns-metaprogramming-dev
    - Implicits vs abilities vs other approaches