Claude-skill-registry convert-elm-scala

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.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/convert-elm-scala" ~/.claude/skills/majiayu000-claude-skill-registry-convert-elm-scala && rm -rf "$T"
manifest: skills/data/convert-elm-scala/SKILL.md
source content

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

ElmScalaNotes
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

ElmScalaNotes
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

ElmScalaNotes
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

ElmScalaNotes
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:

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:

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

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:

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:

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:

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:

-- 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):

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):

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:

-- 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:

// 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 PatternScala PatternNotes
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

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

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):

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 doesn't expose concurrency primitives
-- Multiple Cmds are handled by the runtime
Cmd.batch
    [ fetchUser 1
    , fetchUser 2
    , fetchUser 3
    ]

Scala (cats-effect parallel):

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):

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:

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

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

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

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

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

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

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

    // BAD: Can throw exception
    val rest = list.tail
    
    // GOOD: Use pattern matching
    list match {
      case head :: tail => // Safe
      case Nil => // Handle empty
    }
    

Tooling

ToolPurposeNotes
sbtBuild toolMost common Scala build tool
Scala CLIScriptingQuick scripts and REPLs
scalacCompilerScala compiler (usually via sbt)
scalafmtCode formatterLike elm-format, auto-formats code
scalafixLinting/refactoringLike elm-review, code quality
MetalsLSP serverIDE support (VS Code, Vim, Emacs)
IntelliJ IDEAIDEFull-featured Scala IDE
ScalaTestTestingMost popular test framework
ScalaCheckProperty testingQuickCheck-style property tests
catsFP libraryType classes, data types
cats-effectEffect systemIO, concurrency primitives
ZIOEffect systemAlternative to cats-effect
http4sHTTPFunctional HTTP library
circeJSONPure 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):

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):

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):

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):

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):

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):

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