xiaohongshu
xiaohongshu
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...
Full skill instructions
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.
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.
meta-convert-devlang-scala-devlang-elm-devconvert-elm-scalalang-scala-js-dev for frontend-to-frontend conversions| Scala | Elm | Notes |
|---|---|---|
case class User(name: String) | type alias User = { name : String } | Case classes → records |
sealed trait Msg; case object Increment extends Msg | type Msg = Increment | Decrement | Sealed traits → union types |
Option[A] | Maybe a | Direct mapping with simpler API |
Either[Error, Value] | Result error value | Direct mapping, same right-bias |
List[A] or Vector[A] | List a | Lists (Elm uses linked lists) |
IO[Unit] or Task[Unit] | Cmd Msg | Effects → commands |
x match { case ... => ... } | case x of ... | Pattern matching |
x => x + 1 or _ + 1 | \x -> x + 1 | Lambda syntax |
def update(model: Model, msg: Msg): (Model, IO[Unit]) | update : Msg -> Model -> (Model, Cmd Msg) | Functional effects → TEA |
(A, B) (Tuple2) | ( a, b ) | Tuples with anonymous access |
| Scala | Elm | Notes |
|---|---|---|
String | String | Direct mapping |
Int | Int | 32-bit integers (same semantics) |
Double | Float | Elm uses Float for all floating-point |
Boolean | Bool | Direct mapping |
Char | Char | Direct mapping |
Unit | () (unit) | Unit type, same semantics |
| Scala | Elm | Notes |
|---|---|---|
List[A] | List a | Immutable linked list (same semantics) |
Vector[A] | List a | Elm only has linked lists (O(n) indexed access) |
Array[A] | Array a | Elm has Array for O(log n) indexed access |
(A, B) | ( a, b ) | Tuples, access via pattern matching or Tuple.first/Tuple.second |
(A, B, C) | ( a, b, c ) | Elm supports tuples up to 3 elements natively |
Map[K, V] | Dict k v | Immutable dictionary (requires comparable keys in Elm) |
Set[A] | Set a | Immutable set (requires comparable values in Elm) |
| Scala | Elm | Notes |
|---|---|---|
case class User(name: String) | type alias User = { name : String } | Type aliases for records |
sealed trait Msg; case object A extends Msg; case object B extends Msg | type Msg = A | B | Union types for ADTs |
sealed trait Msg; case class SetName(value: String) extends Msg | type Msg = SetName String | ADTs with data |
Either[Err, Ok] | Result err ok | Result type (Ok/Err constructors) |
Option[A] | Maybe a | Maybe type (Just/Nothing constructors) |
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:
{ model | count = ... } is more concise than Scala's copyScala'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.withDefault instead of getOrElsefold on Maybe, use map + withDefault instead|> is similar to method chaining.name is more concise than Scala's _.nameScala'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.andThen instead of flatMap for chainingScala'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:
( Model, Cmd Msg ) return type is similar to Scala's (Model, IO[Unit])Cmd.none is equivalent to IO.unit (no effect)| 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 andThen | Similar monadic chaining, different syntax |
| Rich combinator libraries | Smaller standard library | Elm prefers simplicity over completeness |
| 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 |
Scala approach:
Option, Either, Try, exceptionsfold, flatMap, recover, recoverWithElm approach:
Maybe (no error info), Result (with error info)map, andThen, withDefaultTranslation strategy:
| Scala | Elm | When to Use |
|---|---|---|
Option[A] | Maybe a | When error context doesn't matter |
Either[E, A] | Result e a | When you need error information |
Try[A] | Result String a | Elm has no exceptions; use Result with String error |
| For-comprehension | Pipe chain with andThen | 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
Scala concurrency models:
Future (eager, callback-based)IO / Task (lazy, referentially transparent)parMapN, parTraverseElm concurrency model:
Cmd (command to runtime, always async)Sub (subscription to events)Translation strategy:
| Scala Pattern | Elm Pattern | Notes |
|---|---|---|
Future[A] | Cmd Msg + callback | Future result becomes a Msg |
IO[A] | Cmd Msg | Elm runtime executes commands |
| Sequential effects | Cmd.batch [cmd1, cmd2] | Multiple commands issued at once |
| Parallel effects | Cmd.batch [cmd1, cmd2] | Runtime decides execution strategy |
| HTTP request | Http.get → Cmd Msg | Built-in HTTP module |
| WebSocket | Browser.Events.onVisibilityChange | 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:
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:
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:
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 andThen for monadic sequencing.
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 comparable (Int, Float, String, etc.). Can't use custom types as keys without workarounds.
Missing standard library functions: Elm's standard library is intentionally small. You may need to implement common utilities yourself or use packages.
| Tool | Purpose | Notes |
|---|---|---|
elm | Compiler and CLI | Fast compiler with excellent error messages |
elm-format | Code formatter | Enforces consistent style, integrates with editors |
elm-test | Testing framework | Unit and fuzz testing |
elm-review | Linter | Customizable code review tool |
elm-json | Package manager helper | Manage dependencies more easily |
elm-spa | SPA framework | Higher-level abstractions for single-page apps |
| ScalaJS → Elm | Manual rewrite | No automatic transpiler exists |
Migration strategy:
Examples should progress in complexity:
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"
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"
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:
Http.get with JSON decoderFor more examples and patterns, see:
meta-convert-dev - Foundational patterns with cross-language examplesconvert-elm-scala - Reverse conversion (Elm → Scala)lang-scala-dev - Scala development patternslang-elm-dev - Elm development patternsCross-cutting pattern skills (for areas not fully covered by lang-*-dev):
patterns-concurrency-dev - Async, channels, threads across languagespatterns-serialization-dev - JSON, validation, struct tags across languagesxiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience