| name | convert-scala-elm |
| description | 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. |
Convert 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
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'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
lang-scala-js-devfor frontend-to-frontend conversions
Quick Reference
| 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 |
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 |
|---|---|---|
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 |
Collection Types
| 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) |
Composite Types
| 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) |
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
{ model | count = ... }is more concise than Scala'scopy - 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
Maybe.withDefaultinstead ofgetOrElse - Elm doesn't have
foldon Maybe, usemap+withDefaultinstead - Elm's pipe operator
|>is similar to method chaining - Property access
.nameis more concise than Scala's_.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
Result.andTheninstead offlatMapfor chaining - 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
( Model, Cmd Msg )return type is similar to Scala's(Model, IO[Unit]) Cmd.noneis equivalent toIO.unit(no effect)- 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 andThen |
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,Try, exceptions - Rich combinators:
fold,flatMap,recover,recoverWith - For-comprehensions for sequencing
- Type classes for generic error handling
Elm approach:
- Two error types:
Maybe(no error info),Result(with error info) - Simpler combinators:
map,andThen,withDefault - Pipe chains for sequencing
- Explicit pattern matching for error handling
Translation 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
Concurrency Patterns
Scala Async → Elm Async
Scala concurrency models:
Future(eager, callback-based)IO/Task(lazy, referentially transparent)- Akka actors (message-passing)
- Parallel execution with
parMapN,parTraverse
Elm concurrency model:
Cmd(command to runtime, always async)Sub(subscription to events)- No parallelism control (runtime manages)
- Everything goes through TEA update loop
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:
- 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
andThenfor 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.
Tooling
| 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:
- 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
Http.getwith JSON decoder - 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:
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 patterns
Cross-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 languages