Claude-skill-registry convert-scala-elm
Convert Scala code to idiomatic Elm. Use when migrating Scala backend logic to Elm frontend applications, translating functional Scala patterns to The Elm Architecture, or refactoring from advanced type system features to simpler compile-time guarantees. Extends meta-convert-dev with Scala-to-Elm specific patterns.
git clone https://github.com/majiayu000/claude-skill-registry
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-elm" ~/.claude/skills/majiayu000-claude-skill-registry-convert-scala-elm && rm -rf "$T"
skills/data/convert-scala-elm/SKILL.mdConvert Scala to Elm
Convert Scala code to idiomatic Elm. This skill extends
meta-convert-dev with Scala-to-Elm specific type mappings, idiom translations, and tooling for translating from backend/full-stack functional programming to frontend functional programming with simpler, safer 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: Scala's sealed traits and case classes → Elm's union types
- Idiom translations: Functional Scala patterns → The Elm Architecture (TEA)
- Error handling: Option/Either with rich combinators → Maybe/Result with simpler API
- Async patterns: Future/IO/Task with effect systems → Cmd/Sub with TEA
- Type system: Advanced types (higher-kinded, type classes, implicits) → simple types with guarantees
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - Scala language fundamentals - see
lang-scala-dev - Elm language fundamentals - see
lang-elm-dev - Reverse conversion (Elm → Scala) - see
convert-elm-scala - ScalaJS specific patterns - see
for frontend-to-frontend conversionslang-scala-js-dev
Quick Reference
| Scala | Elm | Notes |
|---|---|---|
| | Case classes → records |
| | Sealed traits → union types |
| | Direct mapping with simpler API |
| | Direct mapping, same right-bias |
or | | Lists (Elm uses linked lists) |
or | | Effects → commands |
| | Pattern matching |
or | | Lambda syntax |
| | Functional effects → TEA |
(Tuple2) | | Tuples with anonymous access |
When Converting Code
- Analyze source thoroughly before writing target - understand Scala's abstractions and data flow
- Map types first - create type equivalence table, simplifying advanced features
- Preserve semantics over syntax similarity - embrace Elm's simpler model
- Adopt target idioms - don't write "Scala code in Elm syntax"
- Handle edge cases - simplify Option/Either to Maybe/Result, manage effect boundaries
- Test equivalence - same inputs → same outputs
- Simplify type system usage - remove higher-kinded types, implicits, type classes
Type System Mapping
Primitive Types
| Scala | Elm | Notes |
|---|---|---|
| | Direct mapping |
| | 32-bit integers (same semantics) |
| | Elm uses Float for all floating-point |
| | Direct mapping |
| | Direct mapping |
| (unit) | Unit type, same semantics |
Collection Types
| Scala | Elm | Notes |
|---|---|---|
| | Immutable linked list (same semantics) |
| | Elm only has linked lists (O(n) indexed access) |
| | Elm has Array for O(log n) indexed access |
| | Tuples, access via pattern matching or / |
| | Elm supports tuples up to 3 elements natively |
| | Immutable dictionary (requires comparable keys in Elm) |
| | Immutable set (requires comparable values in Elm) |
Composite Types
| Scala | Elm | Notes |
|---|---|---|
| | Type aliases for records |
| | Union types for ADTs |
| | ADTs with data |
| | Result type (Ok/Err constructors) |
| | Maybe type (Just/Nothing constructors) |
Idiom Translation
Pattern: Sealed Traits to Union Types
Scala uses sealed traits with case classes/objects for discriminated unions. Elm uses simpler union types.
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) }
Elm:
-- Union type with simple syntax type Msg = Increment | Decrement | SetCount Int type alias Model = { count : 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 }
Why this translation:
- Elm's union types are simpler and don't require separate declarations for each variant
- Elm's record update syntax
is more concise than Scala's{ model | count = ... }copy - Both provide compile-time exhaustiveness checking
- Elm enforces purity at the type system level (no side effects in update)
Pattern: Option to Maybe
Scala's Option type with rich combinators translates to Elm's Maybe with simpler API.
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)
Elm:
type alias User = { name : String , age : Int } 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 combinators name : String name = findUser 1 |> Maybe.map .name |> Maybe.withDefault "Anonymous"
Why this translation:
- Maybe has the same semantics as Option (Just/Nothing vs Some/None)
- Elm uses
instead ofMaybe.withDefaultgetOrElse - Elm doesn't have
on Maybe, usefold
+map
insteadwithDefault - Elm's pipe operator
is similar to method chaining|> - Property access
is more concise than Scala's.name_.name
Pattern: Either to Result
Scala's Either type (right-biased) maps to Elm's Result, which is designed for error handling.
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
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 with andThen 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" )
Why this translation:
- Result has the same semantics as Either (Ok/Err vs Right/Left)
- Elm uses
instead ofResult.andThen
for chainingflatMap - Elm doesn't have for-comprehensions; use pipe chains instead
- Elm doesn't have exceptions, so parsing returns Maybe, not throws
- Elm Result is explicitly for error handling, not general sum types
Pattern: Functional Effects to The Elm Architecture
Scala's functional effect systems (cats-effect, ZIO) translate to The Elm Architecture's Cmd/Sub.
Scala (with cats-effect):
import cats.effect.IO // 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 case object LoadData extends Msg case class DataLoaded(data: String) 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) case LoadData => (model, loadDataEffect) case DataLoaded(data) => (model, IO.unit) // handle loaded data } def loadDataEffect: IO[Unit] = IO(println("Loading data...")) // VIEW def view(model: Model): String = s"Count: ${model.count}"
Elm:
import Http -- MODEL type alias Model = { count : Int } init : ( Model, Cmd Msg ) init = ( { count = 0 }, Cmd.none ) -- UPDATE type Msg = Increment | Decrement | LoadData | DataLoaded (Result Http.Error String) 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 ) LoadData -> ( model, loadDataCmd ) DataLoaded result -> case result of Ok data -> ( model, Cmd.none ) -- handle loaded data Err _ -> ( model, Cmd.none ) -- handle error loadDataCmd : Cmd Msg loadDataCmd = Http.get { url = "https://api.example.com/data" , expect = Http.expectString DataLoaded } -- VIEW view : Model -> Html Msg view model = div [] [ text ("Count: " ++ String.fromInt model.count) ]
Why this translation:
- TEA's
return type is similar to Scala's( Model, Cmd Msg )(Model, IO[Unit])
is equivalent toCmd.none
(no effect)IO.unit- Elm enforces the TEA pattern at the type system level
- Effects in Elm are always async and message-based (no synchronous IO)
- Elm runtime manages all effects; you describe what to do, runtime executes
Paradigm Translation
Mental Model Shift: Backend/Full-Stack FP → Frontend FP
| Scala Concept | Elm Approach | Key Insight |
|---|---|---|
| Effect systems (IO, Task, ZIO) | Cmd/Sub in TEA | Effects are managed by runtime, not type-level |
| Higher-kinded types | Simple parametric types | Elm lacks HKTs; use concrete types |
| Type classes (implicits/given) | No type classes | Use explicit function passing or modules |
| For-comprehensions | Pipe chains with | Similar monadic chaining, different syntax |
| Rich combinator libraries | Smaller standard library | Elm prefers simplicity over completeness |
Type System Mental Model
| Scala Feature | Elm Translation | Conceptual Translation |
|---|---|---|
| Sealed traits + case classes | Union types | ADTs with simpler syntax |
| Implicits/given | Explicit parameters | No automatic resolution |
| Type classes | Module functions | Polymorphism via modules, not constraints |
| Higher-kinded types | Not supported | Use concrete types (List, Maybe, etc.) |
| Variance annotations | Not needed | All types are invariant |
Error Handling
Scala Error Model → Elm Error Model
Scala approach:
- Multiple error types:
,Option
,Either
, exceptionsTry - Rich combinators:
,fold
,flatMap
,recoverrecoverWith - For-comprehensions for sequencing
- Type classes for generic error handling
Elm approach:
- Two error types:
(no error info),Maybe
(with error info)Result - Simpler combinators:
,map
,andThenwithDefault - Pipe chains for sequencing
- Explicit pattern matching for error handling
Translation strategy:
| Scala | Elm | When to Use |
|---|---|---|
| | When error context doesn't matter |
| | When you need error information |
| | Elm has no exceptions; use Result with String error |
| For-comprehension | Pipe chain with | Sequential error-prone operations |
Example:
// Scala def getUser(id: Int): Option[User] = ??? def getOrders(user: User): Either[String, List[Order]] = ??? def processOrders(orders: List[Order]): Try[Summary] = ??? val result: Either[String, Summary] = for { user <- getUser(1).toRight("User not found") orders <- getOrders(user) summary <- processOrders(orders).toEither.left.map(_.getMessage) } yield summary
-- Elm getUser : Int -> Maybe User getOrders : User -> Result String (List Order) processOrders : List Order -> Result String Summary result : Result String Summary result = getUser 1 |> Result.fromMaybe "User not found" |> Result.andThen getOrders |> Result.andThen processOrders
Concurrency Patterns
Scala Async → Elm Async
Scala concurrency models:
(eager, callback-based)Future
/IO
(lazy, referentially transparent)Task- Akka actors (message-passing)
- Parallel execution with
,parMapNparTraverse
Elm concurrency model:
(command to runtime, always async)Cmd
(subscription to events)Sub- No parallelism control (runtime manages)
- Everything goes through TEA update loop
Translation strategy:
| Scala Pattern | Elm Pattern | Notes |
|---|---|---|
| + callback | Future result becomes a Msg |
| | Elm runtime executes commands |
| Sequential effects | | Multiple commands issued at once |
| Parallel effects | | Runtime decides execution strategy |
| HTTP request | → | Built-in HTTP module |
| WebSocket | | Subscriptions for streaming |
Example:
// Scala (cats-effect) def fetchUser(id: Int): IO[User] = ??? def fetchOrders(userId: Int): IO[List[Order]] = ??? val program: IO[String] = for { user <- fetchUser(1) orders <- fetchOrders(user.id) } yield s"${user.name} has ${orders.length} orders" program.unsafeRunSync()
-- Elm type Msg = UserFetched (Result Http.Error User) | OrdersFetched (Result Http.Error (List Order)) fetchUser : Int -> Cmd Msg fetchUser id = Http.get { url = "/api/users/" ++ String.fromInt id , expect = Http.expectJson UserFetched userDecoder } fetchOrders : Int -> Cmd Msg fetchOrders userId = Http.get { url = "/api/orders?userId=" ++ String.fromInt userId , expect = Http.expectJson OrdersFetched ordersDecoder } update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of UserFetched (Ok user) -> ( { model | user = Just user } , fetchOrders user.id ) UserFetched (Err _) -> ( model, Cmd.none ) OrdersFetched (Ok orders) -> ( { model | orders = orders }, Cmd.none ) OrdersFetched (Err _) -> ( model, Cmd.none )
Why this translation:
- Elm doesn't allow synchronous effects; everything is async via Cmd
- Sequential effects become chained messages in the update loop
- Elm runtime manages all async execution; no manual thread management
- Type safety is preserved through Result types in message variants
Advanced Feature Translation
Type Classes to Module Functions
Scala's type classes (via implicits/given) don't exist in Elm. Use explicit module functions.
Scala:
// Type class 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 show[A](a: A)(implicit ev: Show[A]): String = ev.show(a) } // Usage Show.show(42) // "42" Show.show("hello") // "\"hello\""
Elm:
-- Explicit functions per type showInt : Int -> String showInt n = String.fromInt n showString : String -> String showString s = "\"" ++ s ++ "\"" -- No polymorphic show function; use type-specific functions result1 = showInt 42 -- "42" result2 = showString "hello" -- "\"hello\""
Why this translation:
- Elm doesn't have type classes or ad-hoc polymorphism
- Use concrete functions for each type
- This is more explicit but less generic
- Consider using custom types to unify different representations
Higher-Kinded Types - Not Supported
Scala's higher-kinded types (type constructors as parameters) don't exist in Elm.
Scala:
trait Functor[F[_]] { def map[A, B](fa: F[A])(f: A => B): F[B] } def mapTwice[F[_]: Functor, A, B, C](fa: F[A])(f: A => B)(g: B => C): F[C] = { val fb = Functor[F].map(fa)(f) Functor[F].map(fb)(g) } // Works with List, Option, Either, etc. mapTwice(List(1, 2, 3))(_ + 1)(_ * 2) mapTwice(Some(5))(_ + 1)(_ * 2)
Elm - Use concrete types:
-- Separate implementations for each type mapTwiceList : (a -> b) -> (b -> c) -> List a -> List c mapTwiceList f g list = list |> List.map f |> List.map g mapTwiceMaybe : (a -> b) -> (b -> c) -> Maybe a -> Maybe c mapTwiceMaybe f g maybe = maybe |> Maybe.map f |> Maybe.map g -- Usage mapTwiceList (\x -> x + 1) (\x -> x * 2) [1, 2, 3] mapTwiceMaybe (\x -> x + 1) (\x -> x * 2) (Just 5)
Why this translation:
- Elm prioritizes simplicity over abstraction
- Code is more explicit and easier to understand
- No accidental complexity from type-level programming
- Duplicate code is acceptable if it improves clarity
Common Pitfalls
-
Trying to use type classes: Elm has no type classes. Use explicit function passing or modules for polymorphism.
-
Expecting exceptions: Elm has no runtime exceptions. All errors must be represented as Maybe or Result.
-
Using mutable state: Elm is purely functional with immutable data. Use record updates and model state instead.
-
Synchronous side effects: All effects in Elm are async via Cmd/Sub. You can't perform IO directly in functions.
-
Advanced type features: Elm doesn't have higher-kinded types, variance, existential types, or path-dependent types. Keep it simple.
-
For-comprehensions: Elm doesn't have for-comprehensions. Use pipe chains with
for monadic sequencing.andThen -
Lazy evaluation: Elm is strict (eager) by default. Use lazy values explicitly with
.Lazy.lazy -
Tuple size limits: Elm tuples are limited to 3 elements. Use records for larger structures.
-
Dict/Set key constraints: In Elm, Dict keys and Set values must be
(Int, Float, String, etc.). Can't use custom types as keys without workarounds.comparable -
Missing standard library functions: Elm's standard library is intentionally small. You may need to implement common utilities yourself or use packages.
Tooling
| Tool | Purpose | Notes |
|---|---|---|
| Compiler and CLI | Fast compiler with excellent error messages |
| Code formatter | Enforces consistent style, integrates with editors |
| Testing framework | Unit and fuzz testing |
| Linter | Customizable code review tool |
| Package manager helper | Manage dependencies more easily |
| SPA framework | Higher-level abstractions for single-page apps |
| ScalaJS → Elm | Manual rewrite | No automatic transpiler exists |
Migration strategy:
- Extract pure business logic from Scala
- Identify data types and ADTs
- Map types to Elm equivalents
- Rewrite logic using Elm idioms
- Structure as TEA (Model-Update-View)
- Use elm-test to verify behavior equivalence
Examples
Examples should progress in complexity:
Example 1: Simple - ADT Translation
Before (Scala):
sealed trait Status case object Pending extends Status case object Active extends Status case object Completed extends Status def statusToString(status: Status): String = status match { case Pending => "Pending" case Active => "Active" case Completed => "Completed" } val current: Status = Active println(statusToString(current)) // "Active"
After (Elm):
type Status = Pending | Active | Completed statusToString : Status -> String statusToString status = case status of Pending -> "Pending" Active -> "Active" Completed -> "Completed" current : Status current = Active -- Usage: statusToString current => "Active"
Example 2: Medium - Option/Maybe and Error Handling
Before (Scala):
case class User(name: String, age: Int) def findUser(id: Int): Option[User] = if (id == 1) Some(User("Alice", 30)) else None def validateAge(age: Int): Either[String, Int] = if (age >= 18) Right(age) else Left("Must be 18 or older") def getUserAge(id: Int): Either[String, Int] = findUser(id) .toRight("User not found") .flatMap(user => validateAge(user.age)) getUserAge(1) // Right(30) getUserAge(2) // Left("User not found")
After (Elm):
type alias User = { name : String , age : Int } findUser : Int -> Maybe User findUser id = if id == 1 then Just { name = "Alice", age = 30 } else Nothing validateAge : Int -> Result String Int validateAge age = if age >= 18 then Ok age else Err "Must be 18 or older" getUserAge : Int -> Result String Int getUserAge id = findUser id |> Result.fromMaybe "User not found" |> Result.andThen (\user -> validateAge user.age) -- getUserAge 1 => Ok 30 -- getUserAge 2 => Err "User not found"
Example 3: Complex - TEA Application with HTTP
Before (Scala with cats-effect):
import cats.effect.IO import io.circe.generic.auto._ import org.http4s._ import org.http4s.circe._ import org.http4s.client._ case class Post(id: Int, title: String, body: String) case class Model( posts: List[Post], loading: Boolean, error: Option[String] ) sealed trait Msg case object FetchPosts extends Msg case class PostsFetched(posts: List[Post]) extends Msg case class FetchFailed(error: String) extends Msg def update(model: Model, msg: Msg): (Model, IO[Msg]) = msg match { case FetchPosts => (model.copy(loading = true), fetchPosts) case PostsFetched(posts) => (model.copy(posts = posts, loading = false, error = None), IO.pure(FetchPosts)) case FetchFailed(error) => (model.copy(loading = false, error = Some(error)), IO.pure(FetchPosts)) } def fetchPosts(implicit client: Client[IO]): IO[Msg] = { val uri = Uri.unsafeFromString("https://jsonplaceholder.typicode.com/posts") client.expect[List[Post]](uri) .map(PostsFetched) .handleErrorWith(err => IO.pure(FetchFailed(err.getMessage))) } def view(model: Model): String = { if (model.loading) "Loading..." else if (model.error.isDefined) s"Error: ${model.error.get}" else s"Posts: ${model.posts.map(_.title).mkString(", ")}" }
After (Elm):
import Browser import Html exposing (Html, div, text, button, ul, li) import Html.Events exposing (onClick) import Http import Json.Decode as Decode exposing (Decoder, field, int, string) -- MODEL type alias Post = { id : Int , title : String , body : String } type alias Model = { posts : List Post , loading : Bool , error : Maybe String } init : ( Model, Cmd Msg ) init = ( { posts = [] , loading = False , error = Nothing } , Cmd.none ) -- UPDATE type Msg = FetchPosts | PostsFetched (Result Http.Error (List Post)) update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of FetchPosts -> ( { model | loading = True, error = Nothing } , fetchPosts ) PostsFetched (Ok posts) -> ( { model | posts = posts, loading = False } , Cmd.none ) PostsFetched (Err error) -> ( { model | loading = False, error = Just (errorToString error) } , Cmd.none ) 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 -- HTTP fetchPosts : Cmd Msg fetchPosts = Http.get { url = "https://jsonplaceholder.typicode.com/posts" , expect = Http.expectJson PostsFetched postsDecoder } postsDecoder : Decoder (List Post) postsDecoder = Decode.list postDecoder postDecoder : Decoder Post postDecoder = Decode.map3 Post (field "id" int) (field "title" string) (field "body" string) -- VIEW view : Model -> Html Msg view model = div [] [ button [ onClick FetchPosts ] [ text "Fetch Posts" ] , viewContent model ] viewContent : Model -> Html Msg viewContent model = if model.loading then text "Loading..." else case model.error of Just error -> text ("Error: " ++ error) Nothing -> ul [] (List.map viewPost model.posts) viewPost : Post -> Html Msg viewPost post = li [] [ text post.title ] -- MAIN main : Program () Model Msg main = Browser.element { init = \_ -> init , update = update , view = view , subscriptions = \_ -> Sub.none }
Why this translation:
- Scala's IO effects become Elm's Cmd in TEA
- HTTP client becomes
with JSON decoderHttp.get - Error handling via Result instead of IO error handling
- View is pure HTML (no side effects)
- Elm runtime manages all effects and rendering
- Type safety maintained throughout with no runtime exceptions
See Also
For more examples and patterns, see:
- Foundational patterns with cross-language examplesmeta-convert-dev
- Reverse conversion (Elm → Scala)convert-elm-scala
- Scala development patternslang-scala-dev
- Elm development patternslang-elm-dev
Cross-cutting pattern skills (for areas not fully covered by lang-*-dev):
- Async, channels, threads across languagespatterns-concurrency-dev
- JSON, validation, struct tags across languagespatterns-serialization-dev