home / skills / arustydev / ai / convert-elm-scala
/components/skills/convert-elm-scala
This skill converts Elm code to idiomatic Scala, mapping types, patterns, and effects for safe, idiomatic migration.
npx playbooks add skill arustydev/ai --skill convert-elm-scalaReview the files below or copy the command above to add this skill to your agents.
---
name: convert-elm-scala
description: Convert Elm code to idiomatic Scala. 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.
---
# Convert Elm to Scala
Convert Elm code to idiomatic Scala. This skill extends `meta-convert-dev` with Elm-to-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
- `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**: 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`
- Reverse conversion (Scala → Elm) - see `convert-scala-elm`
- ScalaJS specific patterns - see `lang-scala-js-dev` for frontend-to-frontend conversions
---
## Quick Reference
| Elm | Scala | Notes |
|-----|-------|-------|
| `type alias User = { name : String }` | `case class User(name: String)` | Records → case classes |
| `type Msg = Increment \| Decrement` | `sealed trait Msg; case object Increment extends Msg` | Union types → sealed traits |
| `Maybe a` | `Option[A]` | Direct mapping with richer combinators |
| `Result error value` | `Either[Error, Value]` | Direct mapping, right-biased |
| `List a` | `List[A]` or `Vector[A]` | Lists or vectors |
| `Cmd Msg` | `IO[Unit]` or `Task[Unit]` | Effects with cats-effect/ZIO |
| `case x of ...` | `x match { case ... => ... }` | Pattern matching |
| `\x -> x + 1` | `x => x + 1` or `_ + 1` | Lambda syntax |
| `update : Msg -> Model -> (Model, Cmd Msg)` | `def update(model: Model, msg: Msg): (Model, IO[Unit])` | TEA → functional effects |
| `( a, b )` | `(A, B)` (Tuple2) | Tuples with named accessors |
---
## When Converting Code
1. **Analyze source thoroughly** before writing target - understand TEA flow and data dependencies
2. **Map types first** - create type equivalence table for domain models
3. **Preserve semantics** over syntax similarity - leverage Scala's richer type system
4. **Adopt target idioms** - don't write "Elm code in Scala syntax"
5. **Handle edge cases** - Option chaining, Either composition, effect management
6. **Test equivalence** - same inputs → same outputs
7. **Leverage type classes** - use implicits for compile-time guarantees Elm lacks
---
## Type System Mapping
### Primitive Types
| Elm | Scala | Notes |
|-----|-------|-------|
| `String` | `String` | Direct mapping |
| `Int` | `Int` | 32-bit integers |
| `Float` | `Double` | Scala uses Double by default |
| `Bool` | `Boolean` | Direct mapping |
| `Char` | `Char` | Direct mapping |
| `()` (unit) | `Unit` | Unit type, same semantics |
### Collection Types
| Elm | Scala | Notes |
|-----|-------|-------|
| `List a` | `List[A]` | Immutable linked list (similar semantics) |
| `List a` | `Vector[A]` | Better for indexed access (O(log n) vs O(n)) |
| `Array a` | `Vector[A]` or `Array[A]` | Vector preferred for immutability |
| `( a, b )` | `(A, B)` | Tuples, access via `._1`, `._2` |
| `( a, b, c )` | `(A, B, C)` | Scala supports tuples up to Tuple22 |
| `Dict k v` | `Map[K, V]` | Immutable map |
| `Set a` | `Set[A]` | Immutable set |
### Composite Types
| Elm | Scala | Notes |
|-----|-------|-------|
| `type alias User = { name : String }` | `case class User(name: String)` | Case classes are idiomatic |
| `type Msg = A \| B` | `sealed trait Msg; case object A extends Msg; case object B extends Msg` | Sealed trait ADTs |
| `type Msg = SetName String` | `sealed trait Msg; case class SetName(value: String) extends Msg` | ADTs with data |
| `type Result err ok = Ok ok \| Err err` | `Either[Err, Ok]` | Either is built-in, right-biased |
| `Maybe a` | `Option[A]` | 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:**
```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:**
```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 `copy` method on case classes is similar to Elm's record update syntax
---
### Pattern: Maybe to Option
Elm's Maybe type translates directly to Scala's Option with richer combinators.
**Elm:**
```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:**
```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:**
```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:**
```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:**
```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):**
```scala
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):**
```scala
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:**
```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:**
```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:**
- `Maybe a` for nullable values (explicit, no null)
- `Result error value` for operations that can fail with context
- No exceptions (compiler enforces handling)
**Scala uses:**
- `Option[A]` for nullable values (explicit, but null still exists in Java interop)
- `Either[E, A]` for operations that can fail with context
- `Try[A]` for exception handling
- Exceptions are available (but discouraged in FP)
**Translation strategy:**
| Elm Pattern | Scala Pattern | Notes |
|-------------|---------------|-------|
| `Maybe a` | `Option[A]` | Direct mapping |
| `Maybe.withDefault d m` | `m.getOrElse(d)` | Extract with default |
| `Maybe.map f m` | `m.map(f)` | Transform value |
| `Maybe.andThen f m` | `m.flatMap(f)` | Chain operations |
| `Result err val` | `Either[Err, Val]` | Direct mapping |
| `Result.map f r` | `r.map(f)` | Transform right value |
| `Result.andThen f r` | `r.flatMap(f)` | Chain operations |
| `Result.mapError f r` | `r.left.map(f)` | Transform left (error) |
**Advanced pattern: Accumulating errors**
```scala
// 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:**
- `Cmd Msg` for side effects
- `Sub Msg` for subscriptions
- `Task` for composable async operations
- No direct control over concurrency (runtime manages it)
**Scala uses:**
- `Future[A]` - eager, implicit ExecutionContext
- `IO[A]` (cats-effect) - lazy, explicit runtime
- `Task[A]` (ZIO) - lazy, fiber-based
- `Stream[F, A]` (fs2) - streaming effects
**Translation strategies:**
#### Simple HTTP Request
**Elm:**
```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):**
```scala
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
-- 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):**
```scala
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):**
```scala
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:**
```scala
// 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
1. **Null values from Java interop**: Elm has no null, but Scala inherits null from Java. Always wrap nullable Java values in Option.
```scala
// BAD: Assumes non-null
val name: String = javaObject.getName() // Can be null!
// GOOD: Wrap in Option
val name: Option[String] = Option(javaObject.getName())
```
2. **Non-exhaustive pattern matching**: Elm enforces exhaustiveness at compile-time. Scala only warns by default.
```scala
// Enable fatal warnings in build.sbt
scalacOptions += "-Xfatal-warnings"
scalacOptions += "-Xlint:_"
// Use sealed traits for exhaustive checking
sealed trait Msg // Compiler knows all subtypes
```
3. **Mutability creeping in**: Elm is purely immutable. Scala allows var and mutable collections.
```scala
// BAD: Mutable state
var count = 0
// GOOD: Immutable updates
val count = 0
val newCount = count + 1
```
4. **Exceptions instead of Either**: Elm forces explicit error handling. Scala allows exceptions.
```scala
// 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)
}
```
5. **Future vs IO confusion**: Future is eager and executes immediately. IO is lazy and needs explicit run.
```scala
// 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
```
6. **Type inference differences**: Elm infers everything. Scala sometimes needs help with higher-kinded types.
```scala
// May need explicit type annotations
def sequence[F[_]: Applicative, A](list: List[F[A]]): F[List[A]] = ...
```
7. **Pattern matching on List.tail**: Scala's tail throws on empty list, unlike Elm.
```scala
// 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):**
```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):**
```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):**
```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):**
```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):**
```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):**
```scala
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:
- `meta-convert-dev` - Foundational patterns with cross-language examples
- `convert-elm-clojure` - Related conversion (Elm → dynamic FP)
- `lang-elm-dev` - Elm development patterns
- `lang-scala-dev` - Scala development patterns
- `lang-scala-cats-dev` - Cats library for advanced FP
- `lang-scala-zio-dev` - ZIO effect system
- `lang-scala-js-dev` - ScalaJS for frontend (if staying in browser)
Cross-cutting pattern skills:
- `patterns-concurrency-dev` - Async, channels, threads across languages
- `patterns-serialization-dev` - JSON, validation across languages
- `patterns-metaprogramming-dev` - Macros, implicits, type-level programming
This skill converts Elm code into idiomatic, type-safe Scala. It focuses on translating Elm ADTs, TEA patterns, and effectful Cmd/Sub into Scala sealed traits, case classes, and effect systems like cats-effect or ZIO. Use it when migrating Elm frontends to Scala backends or full-stack Scala architectures while preserving semantics and leveraging Scala's richer type system.
The skill inspects Elm source structures (type aliases, union types, Maybe/Result, List/Dict usage, and update/view patterns) and maps them to Scala equivalents (case classes, sealed traits, Option/Either, List/Vector, and IO/Task). It applies idiom translations: TEA → pure update functions plus effectful IO, union types → sealed traits with case classes/objects, and Cmd/Sub → effect values. The output emphasizes idiomatic Scala patterns, effect libraries, and advanced type techniques (type classes, implicits, higher-kinded types) rather than literal syntax translation.
Does this produce runnable Scala code or just patterns?
It produces idiomatic Scala translations and patterns that are compiler-ready examples; minor adjustments may be required for project-specific dependencies and imports.
Which effect library should I use?
Either cats-effect (IO) or ZIO are recommended; choose the one aligned with your codebase and convert Cmd/Sub into the corresponding effect type.