home / skills / arustydev / ai / convert-elm-scala

convert-elm-scala skill

/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-scala

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
29.1 KB
---
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

Overview

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.

How this skill works

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.

When to use it

  • Migrating an Elm frontend into a Scala backend or full-stack Scala project.
  • Translating Elm The Elm Architecture (TEA) into functional Scala effect patterns.
  • Refactoring simple Elm types into richer Scala type-level solutions (type classes, Validated).
  • Converting Elm code that relies on Maybe/Result into Scala Option/Either workflows.
  • Preparing Elm domain models for server-side processing with Scala effect systems.

Best practices

  • Map domain types first: create a type equivalence table and implement case classes/sealed traits early.
  • Prefer Option/Either/Validated and effect types (IO/Task) over exceptions; adopt cats-effect or ZIO consistently.
  • Translate semantics, not syntax: avoid writing Elm code in Scala—use idiomatic Scala constructs (copy, for-comprehensions).
  • Use sealed traits and exhaustive pattern matching to retain compile-time guarantees similar to Elm.
  • Test behavioral equivalence: same inputs should yield same outputs; unit tests and property tests help validate translation.

Example use cases

  • Convert an Elm TEA counter app into a Scala service using Ref and cats-effect IO for state and effects.
  • Translate Elm union-based message handling into sealed trait ADTs with case classes/objects and pattern matching.
  • Migrate Elm form validation using Result to Scala Either or Validated for error accumulation.
  • Port list/collection transformations to Scala, choosing List vs Vector for performance needs.
  • Refactor Elm domain models into Scala case classes and introduce type classes for serialization and validation.

FAQ

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.