Agents convert-elm-scala
Bidirectional conversion between Elm and Scala. Use when migrating projects between these languages in either direction. Extends meta-convert-dev with Elm↔Scala specific patterns. Use when migrating Elm frontend applications to Scala backends or full-stack Scala, translating The Elm Architecture to functional Scala patterns, or refactoring type-safe functional code from compile-time guarantees to more powerful type system features. Extends meta-convert-dev with Elm-to-Scala specific patterns.
git clone https://github.com/aRustyDev/agents
T=$(mktemp -d) && git clone --depth=1 https://github.com/aRustyDev/agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/content/skills/convert-elm-scala" ~/.claude/skills/arustydev-agents-convert-elm-scala && rm -rf "$T"
content/skills/convert-elm-scala/SKILL.mdElm ↔ Scala Conversion
Bidirectional conversion between Elm and Scala. This skill extends
meta-convert-dev with Elm↔Scala specific type mappings, idiom translations, and tooling for translating from frontend functional programming to backend/full-stack functional programming with more expressive types.
This Skill Extends
- Foundational conversion patterns (APTV workflow, testing strategies)meta-convert-dev
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: Elm's union types → Scala's sealed traits and case classes
- Idiom translations: The Elm Architecture → functional Scala patterns (cats-effect, ZIO)
- Error handling: Maybe/Result → Option/Either with rich combinators
- Async patterns: Cmd/Sub → Future/IO/Task with effect systems
- Type system: Simple types → advanced types (higher-kinded, type classes, implicits)
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - Elm language fundamentals - see
lang-elm-dev - Scala language fundamentals - see
lang-scala-dev - ScalaJS specific patterns - see
for frontend-to-frontend conversionslang-scala-js-dev
Quick Reference
| Elm | Scala | Notes |
|---|---|---|
| | Records → case classes |
| | Union types → sealed traits |
| | Direct mapping with richer combinators |
| | Direct mapping, right-biased |
| or | Lists or vectors |
| or | Effects with cats-effect/ZIO |
| | Pattern matching |
| or | Lambda syntax |
| | TEA → functional effects |
| (Tuple2) | Tuples with named accessors |
When Converting Code
- Analyze source thoroughly before writing target - understand TEA flow and data dependencies
- Map types first - create type equivalence table for domain models
- Preserve semantics over syntax similarity - leverage Scala's richer type system
- Adopt target idioms - don't write "Elm code in Scala syntax"
- Handle edge cases - Option chaining, Either composition, effect management
- Test equivalence - same inputs → same outputs
- Leverage type classes - use implicits for compile-time guarantees Elm lacks
Type System Mapping
Primitive Types
| Elm | Scala | Notes |
|---|---|---|
| | Direct mapping |
| | 32-bit integers |
| | Scala uses Double by default |
| | Direct mapping |
| | Direct mapping |
(unit) | | Unit type, same semantics |
Collection Types
| Elm | Scala | Notes |
|---|---|---|
| | Immutable linked list (similar semantics) |
| | Better for indexed access (O(log n) vs O(n)) |
| or | Vector preferred for immutability |
| | Tuples, access via , |
| | Scala supports tuples up to Tuple22 |
| | Immutable map |
| | Immutable set |
Composite Types
| Elm | Scala | Notes |
|---|---|---|
| | Case classes are idiomatic |
| | Sealed trait ADTs |
| | ADTs with data |
| | Either is built-in, right-biased |
| | Option is built-in with Some/None |
Idiom Translation
Pattern: Union Types to Sealed Traits
Elm uses union types for discriminated unions. Scala uses sealed traits with case classes/objects.
Elm:
type Msg = Increment | Decrement | SetCount Int update : Msg -> Model -> Model update msg model = case msg of Increment -> { model | count = model.count + 1 } Decrement -> { model | count = model.count - 1 } SetCount newCount -> { model | count = newCount }
Scala:
// Sealed trait ensures exhaustive pattern matching sealed trait Msg case object Increment extends Msg case object Decrement extends Msg case class SetCount(value: Int) extends Msg case class Model(count: Int) def update(model: Model, msg: Msg): Model = msg match { case Increment => model.copy(count = model.count + 1) case Decrement => model.copy(count = model.count - 1) case SetCount(newCount) => model.copy(count = newCount) }
Why this translation:
- Sealed traits provide compile-time exhaustiveness checking like Elm
- Case objects for singleton variants are lightweight
- Case classes for variants with data provide automatic pattern matching
- The
method on case classes is similar to Elm's record update syntaxcopy
Pattern: Maybe to Option
Elm's Maybe type translates directly to Scala's Option with richer combinators.
Elm:
findUser : Int -> Maybe User findUser id = if id == 1 then Just { name = "Alice", age = 30 } else Nothing displayName : Maybe User -> String displayName maybeUser = case maybeUser of Just user -> user.name Nothing -> "Anonymous" -- Using Maybe.withDefault name : String name = findUser 1 |> Maybe.map .name |> Maybe.withDefault "Anonymous"
Scala:
case class User(name: String, age: Int) def findUser(id: Int): Option[User] = { if (id == 1) Some(User("Alice", 30)) else None } def displayName(maybeUser: Option[User]): String = maybeUser match { case Some(user) => user.name case None => "Anonymous" } // Using Option combinators val name: String = findUser(1) .map(_.name) .getOrElse("Anonymous") // Or more idiomatically with fold val name2: String = findUser(1).fold("Anonymous")(_.name)
Why this translation:
- Option has the same semantics as Maybe
- Scala's Option provides richer combinators (fold, orElse, collect, etc.)
- Pattern matching syntax is similar but uses
instead of=>-> - getOrElse is equivalent to withDefault
Pattern: Result Type to Either
Elm's Result type maps to Scala's Either, which is right-biased for easy chaining.
Elm:
parseAge : String -> Result String Int parseAge str = case String.toInt str of Just age -> if age >= 0 then Ok age else Err "Age must be non-negative" Nothing -> Err "Not a valid number" -- Chain Results validateAge : String -> Result String Int validateAge str = parseAge str |> Result.andThen (\age -> if age < 120 then Ok age else Err "Age must be less than 120" )
Scala:
def parseAge(str: String): Either[String, Int] = { try { val age = str.toInt if (age >= 0) Right(age) else Left("Age must be non-negative") } catch { case _: NumberFormatException => Left("Not a valid number") } } // Chain Eithers with flatMap def validateAge(str: String): Either[String, Int] = { parseAge(str).flatMap { age => if (age < 120) Right(age) else Left("Age must be less than 120") } } // Or using for-comprehension (idiomatic) def validateAge2(str: String): Either[String, Int] = for { age <- parseAge(str) validAge <- if (age < 120) Right(age) else Left("Age must be less than 120") } yield validAge
Why this translation:
- Either is right-biased, so flatMap/map operate on Right values
- For-comprehensions make chaining more readable
- Exception handling with try/catch is more idiomatic in Scala than creating helper parsers
- Either provides the same type safety as Result
Pattern: The Elm Architecture to Functional Effects
TEA's Model-Update-View pattern translates to functional effect systems in Scala.
Elm:
-- MODEL type alias Model = { count : Int } init : Model init = { count = 0 } -- UPDATE type Msg = Increment | Decrement update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of Increment -> ( { model | count = model.count + 1 }, Cmd.none ) Decrement -> ( { model | count = model.count - 1 }, Cmd.none ) -- VIEW view : Model -> Html Msg view model = div [] [ button [ onClick Decrement ] [ text "-" ] , div [] [ text (String.fromInt model.count) ] , button [ onClick Increment ] [ text "+" ] ]
Scala (with cats-effect):
import cats.effect.IO import cats.effect.concurrent.Ref // MODEL case class Model(count: Int) def init: Model = Model(0) // UPDATE sealed trait Msg case object Increment extends Msg case object Decrement extends Msg def update(model: Model, msg: Msg): (Model, IO[Unit]) = msg match { case Increment => (model.copy(count = model.count + 1), IO.unit) case Decrement => (model.copy(count = model.count - 1), IO.unit) } // Stateful version using Ref def runApp: IO[Unit] = for { modelRef <- Ref.of[IO, Model](init) _ <- modelRef.update { model => val (newModel, effect) = update(model, Increment) newModel } finalModel <- modelRef.get _ <- IO(println(s"Count: ${finalModel.count}")) } yield ()
Scala (with ZIO):
import zio._ // MODEL case class Model(count: Int) // UPDATE sealed trait Msg case object Increment extends Msg case object Decrement extends Msg def update(model: Model, msg: Msg): (Model, Task[Unit]) = msg match { case Increment => (model.copy(count = model.count + 1), ZIO.unit) case Decrement => (model.copy(count = model.count - 1), ZIO.unit) } // Stateful version using Ref def runApp: Task[Unit] = for { modelRef <- Ref.make(Model(0)) _ <- modelRef.update { model => val (newModel, effect) = update(model, Increment) newModel } finalModel <- modelRef.get _ <- Console.printLine(s"Count: ${finalModel.count}") } yield ()
Why this translation:
- IO/Task types represent side effects like Cmd in Elm
- Ref provides mutable reference in pure FP (like Elm's managed state)
- For-comprehensions sequence effects like Elm's Cmd.batch
- Pattern separates pure logic (update) from effects
Pattern: List Operations
Elm and Scala share similar list APIs due to functional roots.
Elm:
-- Transform List.map (\x -> x * 2) [1, 2, 3] List.filter (\x -> x > 2) [1, 2, 3, 4] List.concatMap (\x -> [x, x * 10]) [1, 2] -- Reduce List.foldl (+) 0 [1, 2, 3, 4] List.foldr (::) [] [1, 2, 3] -- Utilities List.length [1, 2, 3] List.head [1, 2, 3] -- Maybe Int List.tail [1, 2, 3] -- Maybe (List Int)
Scala:
// Transform List(1, 2, 3).map(_ * 2) List(1, 2, 3, 4).filter(_ > 2) List(1, 2).flatMap(x => List(x, x * 10)) // Reduce List(1, 2, 3, 4).foldLeft(0)(_ + _) List(1, 2, 3).foldRight(List.empty[Int])(_ :: _) // Utilities List(1, 2, 3).length List(1, 2, 3).headOption // Option[Int] List(1, 2, 3).tail // List[Int] (throws on empty!) List(1, 2, 3).drop(1) // Safe version of tail
Why this translation:
- APIs are nearly identical due to shared FP heritage
- Scala's flatMap is equivalent to Elm's concatMap
- Use headOption instead of head for safety (returns Option)
- tail throws exception on empty list - prefer drop(1) or tailOption (via extension)
Error Handling
Elm Error Model → Scala Error Model
Elm uses:
for nullable values (explicit, no null)Maybe a
for operations that can fail with contextResult error value- No exceptions (compiler enforces handling)
Scala uses:
for nullable values (explicit, but null still exists in Java interop)Option[A]
for operations that can fail with contextEither[E, A]
for exception handlingTry[A]- Exceptions are available (but discouraged in FP)
Translation strategy:
| Elm Pattern | Scala Pattern | Notes |
|---|---|---|
| | Direct mapping |
| | Extract with default |
| | Transform value |
| | Chain operations |
| | Direct mapping |
| | Transform right value |
| | Chain operations |
| | Transform left (error) |
Advanced pattern: Accumulating errors
// Elm doesn't have built-in error accumulation // Scala can use Validated from cats for this import cats.data.Validated import cats.implicits._ case class ValidationError(message: String) def validateAge(age: Int): Validated[ValidationError, Int] = { if (age >= 0 && age < 120) age.valid else ValidationError("Invalid age").invalid } def validateName(name: String): Validated[ValidationError, String] = { if (name.nonEmpty) name.valid else ValidationError("Name is empty").invalid } // Accumulate errors (can't do this easily in Elm) val result = (validateAge(-1), validateName("")).mapN { (age, name) => User(name, age) } // Result: Invalid(ValidationError("Invalid age") + ValidationError("Name is empty"))
Concurrency Patterns
Elm Async → Scala Async
Elm uses:
for side effectsCmd Msg
for subscriptionsSub Msg
for composable async operationsTask- No direct control over concurrency (runtime manages it)
Scala uses:
- eager, implicit ExecutionContextFuture[A]
(cats-effect) - lazy, explicit runtimeIO[A]
(ZIO) - lazy, fiber-basedTask[A]
(fs2) - streaming effectsStream[F, A]
Translation strategies:
Simple HTTP Request
Elm:
type Msg = GotUser (Result Http.Error User) getUser : Int -> Cmd Msg getUser id = Http.get { url = "https://api.example.com/users/" ++ String.fromInt id , expect = Http.expectJson GotUser userDecoder }
Scala (with http4s + cats-effect):
import cats.effect.IO import org.http4s.client.Client import org.http4s.circe.CirceEntityDecoder._ import io.circe.generic.auto._ case class User(name: String, age: Int) def getUser(id: Int)(implicit client: Client[IO]): IO[Either[Throwable, User]] = { client.expect[User](s"https://api.example.com/users/$id") .attempt }
Concurrent Operations
Elm:
-- Elm doesn't expose concurrency primitives -- Multiple Cmds are handled by the runtime Cmd.batch [ fetchUser 1 , fetchUser 2 , fetchUser 3 ]
Scala (cats-effect parallel):
import cats.effect.IO import cats.syntax.parallel._ // Run requests in parallel val users: IO[List[User]] = List(1, 2, 3) .parTraverse(id => getUser(id))
Scala (ZIO parallel):
import zio._ val users: Task[List[User]] = ZIO.collectAllPar( List(1, 2, 3).map(id => getUser(id)) )
Memory & Ownership
Both Elm and Scala run on garbage-collected runtimes:
- Elm: Compiles to JavaScript, uses JS GC
- Scala: Runs on JVM, uses JVM GC
Translation considerations:
- No ownership concerns like Rust
- Both use immutable data structures by default
- Scala allows mutable collections but discouraged
- Scala has more control over performance (lazy collections, views, iterators)
Performance patterns:
// Elm: Lists are always strict List.map f (List.map g list) -- Creates intermediate list // Scala: Can optimize with views/iterators list.view.map(f).map(g).toList // No intermediate collection (Scala 2.13+) // Or use LazyList for lazy evaluation LazyList(1, 2, 3).map(f).map(g) // Only computes on demand
Common Pitfalls
-
Null values from Java interop: Elm has no null, but Scala inherits null from Java. Always wrap nullable Java values in Option.
// BAD: Assumes non-null val name: String = javaObject.getName() // Can be null! // GOOD: Wrap in Option val name: Option[String] = Option(javaObject.getName()) -
Non-exhaustive pattern matching: Elm enforces exhaustiveness at compile-time. Scala only warns by default.
// Enable fatal warnings in build.sbt scalacOptions += "-Xfatal-warnings" scalacOptions += "-Xlint:_" // Use sealed traits for exhaustive checking sealed trait Msg // Compiler knows all subtypes -
Mutability creeping in: Elm is purely immutable. Scala allows var and mutable collections.
// BAD: Mutable state var count = 0 // GOOD: Immutable updates val count = 0 val newCount = count + 1 -
Exceptions instead of Either: Elm forces explicit error handling. Scala allows exceptions.
// BAD: Throwing exceptions def divide(a: Int, b: Int): Int = { if (b == 0) throw new Exception("Division by zero") else a / b } // GOOD: Return Either def divide(a: Int, b: Int): Either[String, Int] = { if (b == 0) Left("Division by zero") else Right(a / b) } -
Future vs IO confusion: Future is eager and executes immediately. IO is lazy and needs explicit run.
// EAGER: Executes on creation val future = Future { println("Running"); 42 } // LAZY: Only executes when explicitly run val io = IO { println("Running"); 42 } io.unsafeRunSync() // Only now does it print -
Type inference differences: Elm infers everything. Scala sometimes needs help with higher-kinded types.
// May need explicit type annotations def sequence[F[_]: Applicative, A](list: List[F[A]]): F[List[A]] = ... -
Pattern matching on List.tail: Scala's tail throws on empty list, unlike Elm.
// BAD: Can throw exception val rest = list.tail // GOOD: Use pattern matching list match { case head :: tail => // Safe case Nil => // Handle empty }
Tooling
| Tool | Purpose | Notes |
|---|---|---|
| sbt | Build tool | Most common Scala build tool |
| Scala CLI | Scripting | Quick scripts and REPLs |
| scalac | Compiler | Scala compiler (usually via sbt) |
| scalafmt | Code formatter | Like elm-format, auto-formats code |
| scalafix | Linting/refactoring | Like elm-review, code quality |
| Metals | LSP server | IDE support (VS Code, Vim, Emacs) |
| IntelliJ IDEA | IDE | Full-featured Scala IDE |
| ScalaTest | Testing | Most popular test framework |
| ScalaCheck | Property testing | QuickCheck-style property tests |
| cats | FP library | Type classes, data types |
| cats-effect | Effect system | IO, concurrency primitives |
| ZIO | Effect system | Alternative to cats-effect |
| http4s | HTTP | Functional HTTP library |
| circe | JSON | Pure FP JSON library |
Examples
Examples progress in complexity from simple type mappings to realistic applications.
Example 1: Simple - Type Alias to Case Class
Before (Elm):
type alias User = { name : String , email : String , age : Int } createUser : String -> String -> Int -> User createUser name email age = { name = name , email = email , age = age } updateAge : User -> Int -> User updateAge user newAge = { user | age = newAge }
After (Scala):
case class User(name: String, email: String, age: Int) def createUser(name: String, email: String, age: Int): User = User(name, email, age) def updateAge(user: User, newAge: Int): User = user.copy(age = newAge)
Example 2: Medium - Union Types and Pattern Matching
Before (Elm):
type Route = Home | Users | User Int | NotFound type Msg = NavigateTo Route | FetchUsers | GotUsers (Result Http.Error (List User)) update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of NavigateTo route -> ( { model | currentRoute = route } , case route of Users -> fetchUsers User id -> fetchUser id _ -> Cmd.none ) FetchUsers -> ( model, fetchUsers ) GotUsers (Ok users) -> ( { model | users = users }, Cmd.none ) GotUsers (Err error) -> ( { model | error = Just (errorToString error) }, Cmd.none )
After (Scala):
import cats.effect.IO sealed trait Route case object Home extends Route case object Users extends Route case class User(id: Int) extends Route case object NotFound extends Route sealed trait Msg case class NavigateTo(route: Route) extends Msg case object FetchUsers extends Msg case class GotUsers(result: Either[Throwable, List[UserData]]) extends Msg case class UserData(name: String, email: String) case class Model( currentRoute: Route, users: List[UserData], error: Option[String] ) def update(model: Model, msg: Msg): (Model, IO[Unit]) = msg match { case NavigateTo(route) => val effect = route match { case Users => fetchUsers case User(id) => fetchUser(id) case _ => IO.unit } (model.copy(currentRoute = route), effect) case FetchUsers => (model, fetchUsers) case GotUsers(Right(users)) => (model.copy(users = users), IO.unit) case GotUsers(Left(error)) => (model.copy(error = Some(error.getMessage)), IO.unit) } // Placeholder effects def fetchUsers: IO[Unit] = IO.unit def fetchUser(id: Int): IO[Unit] = IO.unit
Example 3: Complex - Complete TEA Application
Before (Elm):
module Main exposing (main) import Browser import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (..) import Http import Json.Decode as Decode -- MODEL type alias Model = { query : String , results : List SearchResult , status : Status } type Status = Loading | Success | Failure String type alias SearchResult = { title : String , url : String , snippet : String } init : () -> ( Model, Cmd Msg ) init _ = ( { query = "" , results = [] , status = Success } , Cmd.none ) -- UPDATE type Msg = UpdateQuery String | Search | GotResults (Result Http.Error (List SearchResult)) update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of UpdateQuery newQuery -> ( { model | query = newQuery }, Cmd.none ) Search -> ( { model | status = Loading } , searchApi model.query ) GotResults (Ok results) -> ( { model | results = results, status = Success } , Cmd.none ) GotResults (Err error) -> ( { model | status = Failure (errorToString error) } , Cmd.none ) -- HTTP searchApi : String -> Cmd Msg searchApi query = Http.get { url = "https://api.example.com/search?q=" ++ query , expect = Http.expectJson GotResults resultsDecoder } resultsDecoder : Decode.Decoder (List SearchResult) resultsDecoder = Decode.list <| Decode.map3 SearchResult (Decode.field "title" Decode.string) (Decode.field "url" Decode.string) (Decode.field "snippet" Decode.string) errorToString : Http.Error -> String errorToString error = case error of Http.BadUrl url -> "Bad URL: " ++ url Http.Timeout -> "Request timeout" Http.NetworkError -> "Network error" Http.BadStatus status -> "Bad status: " ++ String.fromInt status Http.BadBody body -> "Bad body: " ++ body -- VIEW view : Model -> Html Msg view model = div [ class "container" ] [ h1 [] [ text "Search Engine" ] , div [ class "search-box" ] [ input [ type_ "text" , placeholder "Enter search query" , value model.query , onInput UpdateQuery ] [] , button [ onClick Search ] [ text "Search" ] ] , viewStatus model.status , div [ class "results" ] (List.map viewResult model.results) ] viewStatus : Status -> Html Msg viewStatus status = case status of Loading -> div [ class "loading" ] [ text "Loading..." ] Success -> text "" Failure error -> div [ class "error" ] [ text error ] viewResult : SearchResult -> Html Msg viewResult result = div [ class "result" ] [ h3 [] [ a [ href result.url ] [ text result.title ] ] , p [] [ text result.snippet ] ] -- MAIN main : Program () Model Msg main = Browser.element { init = init , update = update , view = view , subscriptions = \_ -> Sub.none }
After (Scala with cats-effect and http4s):
import cats.effect._ import cats.effect.concurrent.Ref import io.circe.generic.auto._ import org.http4s._ import org.http4s.circe.CirceEntityDecoder._ import org.http4s.client.Client // MODEL case class Model( query: String, results: List[SearchResult], status: Status ) sealed trait Status case object Loading extends Status case object Success extends Status case class Failure(error: String) extends Status case class SearchResult( title: String, url: String, snippet: String ) def init: Model = Model( query = "", results = List.empty, status = Success ) // UPDATE sealed trait Msg case class UpdateQuery(newQuery: String) extends Msg case object Search extends Msg case class GotResults(result: Either[Throwable, List[SearchResult]]) extends Msg def update(model: Model, msg: Msg)(implicit client: Client[IO]): (Model, IO[Unit]) = msg match { case UpdateQuery(newQuery) => (model.copy(query = newQuery), IO.unit) case Search => val effect = searchApi(model.query).flatMap { result => processMsg(GotResults(result)) } (model.copy(status = Loading), effect) case GotResults(Right(results)) => (model.copy(results = results, status = Success), IO.unit) case GotResults(Left(error)) => (model.copy(status = Failure(error.getMessage)), IO.unit) } // HTTP def searchApi(query: String)(implicit client: Client[IO]): IO[Either[Throwable, List[SearchResult]]] = { val uri = Uri.unsafeFromString(s"https://api.example.com/search?q=$query") client.expect[List[SearchResult]](uri).attempt } // APPLICATION RUNTIME def runApp(implicit client: Client[IO]): IO[Unit] = for { // Create mutable reference for model modelRef <- Ref.of[IO, Model](init) // Example: Simulate user actions _ <- processMsg(UpdateQuery("functional programming")).flatMap { msg => modelRef.update { model => val (newModel, effect) = update(model, msg) // Run effect in background effect.unsafeRunAsync(_ => ()) newModel } } _ <- processMsg(Search).flatMap { msg => modelRef.update { model => val (newModel, effect) = update(model, msg) effect.unsafeRunAsync(_ => ()) newModel } } // Get final model finalModel <- modelRef.get _ <- IO(println(s"Final model: $finalModel")) } yield () // Helper to process messages def processMsg(msg: Msg): IO[Msg] = IO.pure(msg) // In a real application, you would integrate with a web framework // like http4s for server-side rendering, or ScalaJS + Laminar for frontend
Notes on the complex example:
- Scala version separates pure logic (update function) from effects
- IO type represents side effects, making them explicit like Cmd in Elm
- Ref provides mutable reference in pure FP context
- In production, you'd use a web framework (http4s, ZIO HTTP) or frontend library (ScalaJS + Laminar, Outwatch)
- The pattern preserves TEA's separation of concerns: Model, Update, Effects
See Also
For more examples and patterns, see:
- Foundational patterns with cross-language examplesmeta-convert-dev
- Related conversion (Elm → dynamic FP)convert-elm-clojure
- Elm development patternslang-elm-dev
- Scala development patternslang-scala-dev
- Cats library for advanced FPlang-scala-cats-dev
- ZIO effect systemlang-scala-zio-dev
- ScalaJS for frontend (if staying in browser)lang-scala-js-dev
Cross-cutting pattern skills:
- Async, channels, threads across languagespatterns-concurrency-dev
- JSON, validation across languagespatterns-serialization-dev
- Macros, implicits, type-level programmingpatterns-metaprogramming-dev