Bidirectional conversion between Elm and Scala. Use when migrating projects between these languages in either direction. Extends meta-convert-dev with Elm↔Scala specific patterns. 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.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add aRustyDev/agents --skill convert-elm-scala --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Convert Elm Scala?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/arustydev-convert-elm-scala-agents)More formats (shields.io, HTML) on the badges page.
---
name: convert-elm-scala
description: Bidirectional conversion between Elm and Scala. Use when migrating projects between these languages in either direction. Extends meta-convert-dev with Elm↔Scala specific patterns. 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.
---
# Elm ↔ Scala Conversion
Bidirectional conversion between Elm and Scala. This skill extends `meta-convert-dev` with Elm↔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`
- 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
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!