Claude-skill-registry convert-scala-elm

Convert Scala code to idiomatic Elm. Use when migrating Scala backend logic to Elm frontend applications, translating functional Scala patterns to The Elm Architecture, or refactoring from advanced type system features to simpler compile-time guarantees. Extends meta-convert-dev with Scala-to-Elm specific patterns.

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-scala-elm" ~/.claude/skills/majiayu000-claude-skill-registry-convert-scala-elm && rm -rf "$T"
manifest: skills/data/convert-scala-elm/SKILL.md
source content

Convert Scala to Elm

Convert Scala code to idiomatic Elm. This skill extends

meta-convert-dev
with Scala-to-Elm specific type mappings, idiom translations, and tooling for translating from backend/full-stack functional programming to frontend functional programming with simpler, safer types.

This Skill Extends

  • meta-convert-dev
    - Foundational conversion patterns (APTV workflow, testing strategies)

For general concepts like the Analyze → Plan → Transform → Validate workflow, testing strategies, and common pitfalls, see the meta-skill first.

This Skill Adds

  • Type mappings: Scala's sealed traits and case classes → Elm's union types
  • Idiom translations: Functional Scala patterns → The Elm Architecture (TEA)
  • Error handling: Option/Either with rich combinators → Maybe/Result with simpler API
  • Async patterns: Future/IO/Task with effect systems → Cmd/Sub with TEA
  • Type system: Advanced types (higher-kinded, type classes, implicits) → simple types with guarantees

This Skill Does NOT Cover

  • General conversion methodology - see
    meta-convert-dev
  • Scala language fundamentals - see
    lang-scala-dev
  • Elm language fundamentals - see
    lang-elm-dev
  • Reverse conversion (Elm → Scala) - see
    convert-elm-scala
  • ScalaJS specific patterns - see
    lang-scala-js-dev
    for frontend-to-frontend conversions

Quick Reference

ScalaElmNotes
case class User(name: String)
type alias User = { name : String }
Case classes → records
sealed trait Msg; case object Increment extends Msg
type Msg = Increment | Decrement
Sealed traits → union types
Option[A]
Maybe a
Direct mapping with simpler API
Either[Error, Value]
Result error value
Direct mapping, same right-bias
List[A]
or
Vector[A]
List a
Lists (Elm uses linked lists)
IO[Unit]
or
Task[Unit]
Cmd Msg
Effects → commands
x match { case ... => ... }
case x of ...
Pattern matching
x => x + 1
or
_ + 1
\x -> x + 1
Lambda syntax
def update(model: Model, msg: Msg): (Model, IO[Unit])
update : Msg -> Model -> (Model, Cmd Msg)
Functional effects → TEA
(A, B)
(Tuple2)
( a, b )
Tuples with anonymous access

When Converting Code

  1. Analyze source thoroughly before writing target - understand Scala's abstractions and data flow
  2. Map types first - create type equivalence table, simplifying advanced features
  3. Preserve semantics over syntax similarity - embrace Elm's simpler model
  4. Adopt target idioms - don't write "Scala code in Elm syntax"
  5. Handle edge cases - simplify Option/Either to Maybe/Result, manage effect boundaries
  6. Test equivalence - same inputs → same outputs
  7. Simplify type system usage - remove higher-kinded types, implicits, type classes

Type System Mapping

Primitive Types

ScalaElmNotes
String
String
Direct mapping
Int
Int
32-bit integers (same semantics)
Double
Float
Elm uses Float for all floating-point
Boolean
Bool
Direct mapping
Char
Char
Direct mapping
Unit
()
(unit)
Unit type, same semantics

Collection Types

ScalaElmNotes
List[A]
List a
Immutable linked list (same semantics)
Vector[A]
List a
Elm only has linked lists (O(n) indexed access)
Array[A]
Array a
Elm has Array for O(log n) indexed access
(A, B)
( a, b )
Tuples, access via pattern matching or
Tuple.first
/
Tuple.second
(A, B, C)
( a, b, c )
Elm supports tuples up to 3 elements natively
Map[K, V]
Dict k v
Immutable dictionary (requires comparable keys in Elm)
Set[A]
Set a
Immutable set (requires comparable values in Elm)

Composite Types

ScalaElmNotes
case class User(name: String)
type alias User = { name : String }
Type aliases for records
sealed trait Msg; case object A extends Msg; case object B extends Msg
type Msg = A | B
Union types for ADTs
sealed trait Msg; case class SetName(value: String) extends Msg
type Msg = SetName String
ADTs with data
Either[Err, Ok]
Result err ok
Result type (Ok/Err constructors)
Option[A]
Maybe a
Maybe type (Just/Nothing constructors)

Idiom Translation

Pattern: Sealed Traits to Union Types

Scala uses sealed traits with case classes/objects for discriminated unions. Elm uses simpler union types.

Scala:

// Sealed trait ensures exhaustive pattern matching
sealed trait Msg
case object Increment extends Msg
case object Decrement extends Msg
case class SetCount(value: Int) extends Msg

case class Model(count: Int)

def update(model: Model, msg: Msg): Model = msg match {
  case Increment => model.copy(count = model.count + 1)
  case Decrement => model.copy(count = model.count - 1)
  case SetCount(newCount) => model.copy(count = newCount)
}

Elm:

-- Union type with simple syntax
type Msg
    = Increment
    | Decrement
    | SetCount Int

type alias Model =
    { count : Int }

update : Msg -> Model -> Model
update msg model =
    case msg of
        Increment ->
            { model | count = model.count + 1 }

        Decrement ->
            { model | count = model.count - 1 }

        SetCount newCount ->
            { model | count = newCount }

Why this translation:

  • Elm's union types are simpler and don't require separate declarations for each variant
  • Elm's record update syntax
    { model | count = ... }
    is more concise than Scala's
    copy
  • Both provide compile-time exhaustiveness checking
  • Elm enforces purity at the type system level (no side effects in update)

Pattern: Option to Maybe

Scala's Option type with rich combinators translates to Elm's Maybe with simpler API.

Scala:

case class User(name: String, age: Int)

def findUser(id: Int): Option[User] = {
  if (id == 1) Some(User("Alice", 30))
  else None
}

def displayName(maybeUser: Option[User]): String = maybeUser match {
  case Some(user) => user.name
  case None => "Anonymous"
}

// Using Option combinators
val name: String =
  findUser(1)
    .map(_.name)
    .getOrElse("Anonymous")

// Or more idiomatically with fold
val name2: String =
  findUser(1).fold("Anonymous")(_.name)

Elm:

type alias User =
    { name : String
    , age : Int
    }

findUser : Int -> Maybe User
findUser id =
    if id == 1 then
        Just { name = "Alice", age = 30 }
    else
        Nothing

displayName : Maybe User -> String
displayName maybeUser =
    case maybeUser of
        Just user ->
            user.name

        Nothing ->
            "Anonymous"

-- Using Maybe combinators
name : String
name =
    findUser 1
        |> Maybe.map .name
        |> Maybe.withDefault "Anonymous"

Why this translation:

  • Maybe has the same semantics as Option (Just/Nothing vs Some/None)
  • Elm uses
    Maybe.withDefault
    instead of
    getOrElse
  • Elm doesn't have
    fold
    on Maybe, use
    map
    +
    withDefault
    instead
  • Elm's pipe operator
    |>
    is similar to method chaining
  • Property access
    .name
    is more concise than Scala's
    _.name

Pattern: Either to Result

Scala's Either type (right-biased) maps to Elm's Result, which is designed for error handling.

Scala:

def parseAge(str: String): Either[String, Int] = {
  try {
    val age = str.toInt
    if (age >= 0) Right(age)
    else Left("Age must be non-negative")
  } catch {
    case _: NumberFormatException => Left("Not a valid number")
  }
}

// Chain Eithers with flatMap
def validateAge(str: String): Either[String, Int] = {
  parseAge(str).flatMap { age =>
    if (age < 120) Right(age)
    else Left("Age must be less than 120")
  }
}

// Or using for-comprehension (idiomatic)
def validateAge2(str: String): Either[String, Int] = for {
  age <- parseAge(str)
  validAge <- if (age < 120) Right(age)
              else Left("Age must be less than 120")
} yield validAge

Elm:

parseAge : String -> Result String Int
parseAge str =
    case String.toInt str of
        Just age ->
            if age >= 0 then
                Ok age
            else
                Err "Age must be non-negative"

        Nothing ->
            Err "Not a valid number"

-- Chain Results with andThen
validateAge : String -> Result String Int
validateAge str =
    parseAge str
        |> Result.andThen
            (\age ->
                if age < 120 then
                    Ok age
                else
                    Err "Age must be less than 120"
            )

Why this translation:

  • Result has the same semantics as Either (Ok/Err vs Right/Left)
  • Elm uses
    Result.andThen
    instead of
    flatMap
    for chaining
  • Elm doesn't have for-comprehensions; use pipe chains instead
  • Elm doesn't have exceptions, so parsing returns Maybe, not throws
  • Elm Result is explicitly for error handling, not general sum types

Pattern: Functional Effects to The Elm Architecture

Scala's functional effect systems (cats-effect, ZIO) translate to The Elm Architecture's Cmd/Sub.

Scala (with cats-effect):

import cats.effect.IO

// MODEL
case class Model(count: Int)

def init: Model = Model(0)

// UPDATE
sealed trait Msg
case object Increment extends Msg
case object Decrement extends Msg
case object LoadData extends Msg
case class DataLoaded(data: String) extends Msg

def update(model: Model, msg: Msg): (Model, IO[Unit]) = msg match {
  case Increment => (model.copy(count = model.count + 1), IO.unit)
  case Decrement => (model.copy(count = model.count - 1), IO.unit)
  case LoadData => (model, loadDataEffect)
  case DataLoaded(data) => (model, IO.unit) // handle loaded data
}

def loadDataEffect: IO[Unit] =
  IO(println("Loading data..."))

// VIEW
def view(model: Model): String =
  s"Count: ${model.count}"

Elm:

import Http

-- MODEL
type alias Model =
    { count : Int }

init : ( Model, Cmd Msg )
init =
    ( { count = 0 }, Cmd.none )

-- UPDATE
type Msg
    = Increment
    | Decrement
    | LoadData
    | DataLoaded (Result Http.Error String)

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Increment ->
            ( { model | count = model.count + 1 }, Cmd.none )

        Decrement ->
            ( { model | count = model.count - 1 }, Cmd.none )

        LoadData ->
            ( model, loadDataCmd )

        DataLoaded result ->
            case result of
                Ok data ->
                    ( model, Cmd.none )  -- handle loaded data

                Err _ ->
                    ( model, Cmd.none )  -- handle error

loadDataCmd : Cmd Msg
loadDataCmd =
    Http.get
        { url = "https://api.example.com/data"
        , expect = Http.expectString DataLoaded
        }

-- VIEW
view : Model -> Html Msg
view model =
    div []
        [ text ("Count: " ++ String.fromInt model.count) ]

Why this translation:

  • TEA's
    ( Model, Cmd Msg )
    return type is similar to Scala's
    (Model, IO[Unit])
  • Cmd.none
    is equivalent to
    IO.unit
    (no effect)
  • Elm enforces the TEA pattern at the type system level
  • Effects in Elm are always async and message-based (no synchronous IO)
  • Elm runtime manages all effects; you describe what to do, runtime executes

Paradigm Translation

Mental Model Shift: Backend/Full-Stack FP → Frontend FP

Scala ConceptElm ApproachKey Insight
Effect systems (IO, Task, ZIO)Cmd/Sub in TEAEffects are managed by runtime, not type-level
Higher-kinded typesSimple parametric typesElm lacks HKTs; use concrete types
Type classes (implicits/given)No type classesUse explicit function passing or modules
For-comprehensionsPipe chains with
andThen
Similar monadic chaining, different syntax
Rich combinator librariesSmaller standard libraryElm prefers simplicity over completeness

Type System Mental Model

Scala FeatureElm TranslationConceptual Translation
Sealed traits + case classesUnion typesADTs with simpler syntax
Implicits/givenExplicit parametersNo automatic resolution
Type classesModule functionsPolymorphism via modules, not constraints
Higher-kinded typesNot supportedUse concrete types (List, Maybe, etc.)
Variance annotationsNot neededAll types are invariant

Error Handling

Scala Error Model → Elm Error Model

Scala approach:

  • Multiple error types:
    Option
    ,
    Either
    ,
    Try
    , exceptions
  • Rich combinators:
    fold
    ,
    flatMap
    ,
    recover
    ,
    recoverWith
  • For-comprehensions for sequencing
  • Type classes for generic error handling

Elm approach:

  • Two error types:
    Maybe
    (no error info),
    Result
    (with error info)
  • Simpler combinators:
    map
    ,
    andThen
    ,
    withDefault
  • Pipe chains for sequencing
  • Explicit pattern matching for error handling

Translation strategy:

ScalaElmWhen to Use
Option[A]
Maybe a
When error context doesn't matter
Either[E, A]
Result e a
When you need error information
Try[A]
Result String a
Elm has no exceptions; use Result with String error
For-comprehensionPipe chain with
andThen
Sequential error-prone operations

Example:

// Scala
def getUser(id: Int): Option[User] = ???
def getOrders(user: User): Either[String, List[Order]] = ???
def processOrders(orders: List[Order]): Try[Summary] = ???

val result: Either[String, Summary] = for {
  user <- getUser(1).toRight("User not found")
  orders <- getOrders(user)
  summary <- processOrders(orders).toEither.left.map(_.getMessage)
} yield summary
-- Elm
getUser : Int -> Maybe User
getOrders : User -> Result String (List Order)
processOrders : List Order -> Result String Summary

result : Result String Summary
result =
    getUser 1
        |> Result.fromMaybe "User not found"
        |> Result.andThen getOrders
        |> Result.andThen processOrders

Concurrency Patterns

Scala Async → Elm Async

Scala concurrency models:

  • Future
    (eager, callback-based)
  • IO
    /
    Task
    (lazy, referentially transparent)
  • Akka actors (message-passing)
  • Parallel execution with
    parMapN
    ,
    parTraverse

Elm concurrency model:

  • Cmd
    (command to runtime, always async)
  • Sub
    (subscription to events)
  • No parallelism control (runtime manages)
  • Everything goes through TEA update loop

Translation strategy:

Scala PatternElm PatternNotes
Future[A]
Cmd Msg
+ callback
Future result becomes a Msg
IO[A]
Cmd Msg
Elm runtime executes commands
Sequential effects
Cmd.batch [cmd1, cmd2]
Multiple commands issued at once
Parallel effects
Cmd.batch [cmd1, cmd2]
Runtime decides execution strategy
HTTP request
Http.get
Cmd Msg
Built-in HTTP module
WebSocket
Browser.Events.onVisibilityChange
Subscriptions for streaming

Example:

// Scala (cats-effect)
def fetchUser(id: Int): IO[User] = ???
def fetchOrders(userId: Int): IO[List[Order]] = ???

val program: IO[String] = for {
  user <- fetchUser(1)
  orders <- fetchOrders(user.id)
} yield s"${user.name} has ${orders.length} orders"

program.unsafeRunSync()
-- Elm
type Msg
    = UserFetched (Result Http.Error User)
    | OrdersFetched (Result Http.Error (List Order))

fetchUser : Int -> Cmd Msg
fetchUser id =
    Http.get
        { url = "/api/users/" ++ String.fromInt id
        , expect = Http.expectJson UserFetched userDecoder
        }

fetchOrders : Int -> Cmd Msg
fetchOrders userId =
    Http.get
        { url = "/api/orders?userId=" ++ String.fromInt userId
        , expect = Http.expectJson OrdersFetched ordersDecoder
        }

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        UserFetched (Ok user) ->
            ( { model | user = Just user }
            , fetchOrders user.id
            )

        UserFetched (Err _) ->
            ( model, Cmd.none )

        OrdersFetched (Ok orders) ->
            ( { model | orders = orders }, Cmd.none )

        OrdersFetched (Err _) ->
            ( model, Cmd.none )

Why this translation:

  • Elm doesn't allow synchronous effects; everything is async via Cmd
  • Sequential effects become chained messages in the update loop
  • Elm runtime manages all async execution; no manual thread management
  • Type safety is preserved through Result types in message variants

Advanced Feature Translation

Type Classes to Module Functions

Scala's type classes (via implicits/given) don't exist in Elm. Use explicit module functions.

Scala:

// Type class
trait Show[A] {
  def show(a: A): String
}

object Show {
  implicit val intShow: Show[Int] = (a: Int) => a.toString
  implicit val stringShow: Show[String] = (a: String) => s""""$a""""

  def show[A](a: A)(implicit ev: Show[A]): String = ev.show(a)
}

// Usage
Show.show(42)        // "42"
Show.show("hello")   // "\"hello\""

Elm:

-- Explicit functions per type
showInt : Int -> String
showInt n =
    String.fromInt n

showString : String -> String
showString s =
    "\"" ++ s ++ "\""

-- No polymorphic show function; use type-specific functions
result1 = showInt 42           -- "42"
result2 = showString "hello"   -- "\"hello\""

Why this translation:

  • Elm doesn't have type classes or ad-hoc polymorphism
  • Use concrete functions for each type
  • This is more explicit but less generic
  • Consider using custom types to unify different representations

Higher-Kinded Types - Not Supported

Scala's higher-kinded types (type constructors as parameters) don't exist in Elm.

Scala:

trait Functor[F[_]] {
  def map[A, B](fa: F[A])(f: A => B): F[B]
}

def mapTwice[F[_]: Functor, A, B, C](fa: F[A])(f: A => B)(g: B => C): F[C] = {
  val fb = Functor[F].map(fa)(f)
  Functor[F].map(fb)(g)
}

// Works with List, Option, Either, etc.
mapTwice(List(1, 2, 3))(_ + 1)(_ * 2)
mapTwice(Some(5))(_ + 1)(_ * 2)

Elm - Use concrete types:

-- Separate implementations for each type
mapTwiceList : (a -> b) -> (b -> c) -> List a -> List c
mapTwiceList f g list =
    list
        |> List.map f
        |> List.map g

mapTwiceMaybe : (a -> b) -> (b -> c) -> Maybe a -> Maybe c
mapTwiceMaybe f g maybe =
    maybe
        |> Maybe.map f
        |> Maybe.map g

-- Usage
mapTwiceList (\x -> x + 1) (\x -> x * 2) [1, 2, 3]
mapTwiceMaybe (\x -> x + 1) (\x -> x * 2) (Just 5)

Why this translation:

  • Elm prioritizes simplicity over abstraction
  • Code is more explicit and easier to understand
  • No accidental complexity from type-level programming
  • Duplicate code is acceptable if it improves clarity

Common Pitfalls

  1. Trying to use type classes: Elm has no type classes. Use explicit function passing or modules for polymorphism.

  2. Expecting exceptions: Elm has no runtime exceptions. All errors must be represented as Maybe or Result.

  3. Using mutable state: Elm is purely functional with immutable data. Use record updates and model state instead.

  4. Synchronous side effects: All effects in Elm are async via Cmd/Sub. You can't perform IO directly in functions.

  5. Advanced type features: Elm doesn't have higher-kinded types, variance, existential types, or path-dependent types. Keep it simple.

  6. For-comprehensions: Elm doesn't have for-comprehensions. Use pipe chains with

    andThen
    for monadic sequencing.

  7. Lazy evaluation: Elm is strict (eager) by default. Use lazy values explicitly with

    Lazy.lazy
    .

  8. Tuple size limits: Elm tuples are limited to 3 elements. Use records for larger structures.

  9. Dict/Set key constraints: In Elm, Dict keys and Set values must be

    comparable
    (Int, Float, String, etc.). Can't use custom types as keys without workarounds.

  10. Missing standard library functions: Elm's standard library is intentionally small. You may need to implement common utilities yourself or use packages.


Tooling

ToolPurposeNotes
elm
Compiler and CLIFast compiler with excellent error messages
elm-format
Code formatterEnforces consistent style, integrates with editors
elm-test
Testing frameworkUnit and fuzz testing
elm-review
LinterCustomizable code review tool
elm-json
Package manager helperManage dependencies more easily
elm-spa
SPA frameworkHigher-level abstractions for single-page apps
ScalaJS → ElmManual rewriteNo automatic transpiler exists

Migration strategy:

  1. Extract pure business logic from Scala
  2. Identify data types and ADTs
  3. Map types to Elm equivalents
  4. Rewrite logic using Elm idioms
  5. Structure as TEA (Model-Update-View)
  6. Use elm-test to verify behavior equivalence

Examples

Examples should progress in complexity:

Example 1: Simple - ADT Translation

Before (Scala):

sealed trait Status
case object Pending extends Status
case object Active extends Status
case object Completed extends Status

def statusToString(status: Status): String = status match {
  case Pending => "Pending"
  case Active => "Active"
  case Completed => "Completed"
}

val current: Status = Active
println(statusToString(current))  // "Active"

After (Elm):

type Status
    = Pending
    | Active
    | Completed

statusToString : Status -> String
statusToString status =
    case status of
        Pending ->
            "Pending"

        Active ->
            "Active"

        Completed ->
            "Completed"

current : Status
current =
    Active

-- Usage: statusToString current  => "Active"

Example 2: Medium - Option/Maybe and Error Handling

Before (Scala):

case class User(name: String, age: Int)

def findUser(id: Int): Option[User] =
  if (id == 1) Some(User("Alice", 30))
  else None

def validateAge(age: Int): Either[String, Int] =
  if (age >= 18) Right(age)
  else Left("Must be 18 or older")

def getUserAge(id: Int): Either[String, Int] =
  findUser(id)
    .toRight("User not found")
    .flatMap(user => validateAge(user.age))

getUserAge(1)  // Right(30)
getUserAge(2)  // Left("User not found")

After (Elm):

type alias User =
    { name : String
    , age : Int
    }

findUser : Int -> Maybe User
findUser id =
    if id == 1 then
        Just { name = "Alice", age = 30 }
    else
        Nothing

validateAge : Int -> Result String Int
validateAge age =
    if age >= 18 then
        Ok age
    else
        Err "Must be 18 or older"

getUserAge : Int -> Result String Int
getUserAge id =
    findUser id
        |> Result.fromMaybe "User not found"
        |> Result.andThen (\user -> validateAge user.age)

-- getUserAge 1  => Ok 30
-- getUserAge 2  => Err "User not found"

Example 3: Complex - TEA Application with HTTP

Before (Scala with cats-effect):

import cats.effect.IO
import io.circe.generic.auto._
import org.http4s._
import org.http4s.circe._
import org.http4s.client._

case class Post(id: Int, title: String, body: String)

case class Model(
  posts: List[Post],
  loading: Boolean,
  error: Option[String]
)

sealed trait Msg
case object FetchPosts extends Msg
case class PostsFetched(posts: List[Post]) extends Msg
case class FetchFailed(error: String) extends Msg

def update(model: Model, msg: Msg): (Model, IO[Msg]) = msg match {
  case FetchPosts =>
    (model.copy(loading = true), fetchPosts)

  case PostsFetched(posts) =>
    (model.copy(posts = posts, loading = false, error = None), IO.pure(FetchPosts))

  case FetchFailed(error) =>
    (model.copy(loading = false, error = Some(error)), IO.pure(FetchPosts))
}

def fetchPosts(implicit client: Client[IO]): IO[Msg] = {
  val uri = Uri.unsafeFromString("https://jsonplaceholder.typicode.com/posts")
  client.expect[List[Post]](uri)
    .map(PostsFetched)
    .handleErrorWith(err => IO.pure(FetchFailed(err.getMessage)))
}

def view(model: Model): String = {
  if (model.loading) "Loading..."
  else if (model.error.isDefined) s"Error: ${model.error.get}"
  else s"Posts: ${model.posts.map(_.title).mkString(", ")}"
}

After (Elm):

import Browser
import Html exposing (Html, div, text, button, ul, li)
import Html.Events exposing (onClick)
import Http
import Json.Decode as Decode exposing (Decoder, field, int, string)

-- MODEL
type alias Post =
    { id : Int
    , title : String
    , body : String
    }

type alias Model =
    { posts : List Post
    , loading : Bool
    , error : Maybe String
    }

init : ( Model, Cmd Msg )
init =
    ( { posts = []
      , loading = False
      , error = Nothing
      }
    , Cmd.none
    )

-- UPDATE
type Msg
    = FetchPosts
    | PostsFetched (Result Http.Error (List Post))

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        FetchPosts ->
            ( { model | loading = True, error = Nothing }
            , fetchPosts
            )

        PostsFetched (Ok posts) ->
            ( { model | posts = posts, loading = False }
            , Cmd.none
            )

        PostsFetched (Err error) ->
            ( { model | loading = False, error = Just (errorToString error) }
            , Cmd.none
            )

errorToString : Http.Error -> String
errorToString error =
    case error of
        Http.BadUrl url ->
            "Bad URL: " ++ url

        Http.Timeout ->
            "Request timeout"

        Http.NetworkError ->
            "Network error"

        Http.BadStatus status ->
            "Bad status: " ++ String.fromInt status

        Http.BadBody body ->
            "Bad body: " ++ body

-- HTTP
fetchPosts : Cmd Msg
fetchPosts =
    Http.get
        { url = "https://jsonplaceholder.typicode.com/posts"
        , expect = Http.expectJson PostsFetched postsDecoder
        }

postsDecoder : Decoder (List Post)
postsDecoder =
    Decode.list postDecoder

postDecoder : Decoder Post
postDecoder =
    Decode.map3 Post
        (field "id" int)
        (field "title" string)
        (field "body" string)

-- VIEW
view : Model -> Html Msg
view model =
    div []
        [ button [ onClick FetchPosts ] [ text "Fetch Posts" ]
        , viewContent model
        ]

viewContent : Model -> Html Msg
viewContent model =
    if model.loading then
        text "Loading..."

    else
        case model.error of
            Just error ->
                text ("Error: " ++ error)

            Nothing ->
                ul []
                    (List.map viewPost model.posts)

viewPost : Post -> Html Msg
viewPost post =
    li [] [ text post.title ]

-- MAIN
main : Program () Model Msg
main =
    Browser.element
        { init = \_ -> init
        , update = update
        , view = view
        , subscriptions = \_ -> Sub.none
        }

Why this translation:

  • Scala's IO effects become Elm's Cmd in TEA
  • HTTP client becomes
    Http.get
    with JSON decoder
  • Error handling via Result instead of IO error handling
  • View is pure HTML (no side effects)
  • Elm runtime manages all effects and rendering
  • Type safety maintained throughout with no runtime exceptions

See Also

For more examples and patterns, see:

  • meta-convert-dev
    - Foundational patterns with cross-language examples
  • convert-elm-scala
    - Reverse conversion (Elm → Scala)
  • lang-scala-dev
    - Scala development patterns
  • lang-elm-dev
    - Elm development patterns

Cross-cutting pattern skills (for areas not fully covered by lang-*-dev):

  • patterns-concurrency-dev
    - Async, channels, threads across languages
  • patterns-serialization-dev
    - JSON, validation, struct tags across languages