Disclaimer: I use Scala since 2012 and am author of doobie-typesafe and yantl libraries and a contributor to Laminar and Mill build tool. This is also generated by GPT 5 with a prompt tailored from my instructions and reviewed by me.
Short answer: given what you already do (C#, TypeScript, Rust, React, .NET/Express/Axum), I’d seriously pick Scala 3 as your “functional programming language,” not Haskell/Elm/PureScript, and lean on the modern FP ecosystem on top of it.
You get:
- Pure FP when you want it (Cats Effect / ZIO / Kyo).
- A “real” industrial ecosystem for web APIs, CLIs, and UIs.
- One language for JVM backends, Node/browser via Scala.js, and even native.
Below is how that looks in practice.
You want:
- Web APIs
- Web UIs
- CLIs
- DB access
- While learning FP properly.
Scala 3 gives you:
- Single language, three runtimes: JVM, JavaScript (Scala.js), and Native.
- Strong FP ecosystem – Cats, Cats Effect, ZIO, Kyo, FS2, etc.
- Industrial-grade HTTP & DB libs – http4s, ZIO HTTP, Tapir, Doobie, etc.
- An advanced type system with HKTs, typeclass derivation, union/intersection types, implicit parameters (now
given/using), opaque types/newtypes, etc.
Compared to:
- Haskell – fantastic if you want “pure FP lab environment,” but you’ll fight tooling and deployment for common web stacks, plus interop with JS is not first-class.
- Elm – nice for learning and front-end-only SPAs, but it’s deliberately restrictive and not general-purpose. You’d still need another stack for backend.
- PureScript – closer to what you want, but yes: you’ll end up with a lot of JS glue and a smaller ecosystem around HTTP, DBs, and infra than on the JVM.
Scala lets you do “Haskell-ish” FP on stacks that look much more like what you already ship.
You can build APIs with a pure FP core and a thin HTTP/DB layer around it.
Pick one (they’re all good):
- Cats Effect – the “standard” Typelevel effect runtime, with a huge ecosystem and integrations (http4s, FS2, Tapir, Doobie, etc.).
- ZIO – batteries-included effect system with its own ecosystem (ZIO HTTP, ZIO Config, ZIO Schema, etc.).
- Kyo – a newer, multi-effect toolkit that targets JVM, JS, and Native out of the box and aims to unify effects in a more ergonomic way.
If you like Rust’s async + structured concurrency, Cats Effect and ZIO will feel conceptually familiar: pure effect values, fibers, resource safety, etc.
Typical options:
- http4s + Cats Effect – purely functional HTTP, very close to the metal of FP.
- ZIO HTTP – HTTP server/client built directly on ZIO, high performance and nice DSL.
- Cask – small, Flask-style microframework for when you want something dead simple without deep FP machinery.
On top of your HTTP choice, you can define APIs with Tapir:
- With Tapir, you define endpoints as Scala types, then generate:
- servers (http4s, ZIO HTTP, Akka, etc.)
- OpenAPI docs
- clients.
That’s very close to “type-level description of your API,” which is exactly what FP people brag about.
For relational DBs:
- Doobie – a mature, functional JDBC wrapper.
- doobie-typesafe – an extra layer that gives you typesafe table/column references on top of Doobie, so schema changes show up as compile errors.
This answers your “ecosystem for DB connections” question: it’s solid, battle-tested, and fits the FP style nicely.
This is where you lean on the type system hard:
- yantl – “Yet Another Newtype Library” for Scala 3, focused on localized error messages, and available on JVM, JS, and Native.
Use it (or similar libraries) to encode domain-specific types: UserId, OrderId, NonEmptyString, etc. This gets you very close to the “Haskell newtype” feel, but in Scala.
Combine that with derivation (auto-deriving typeclass instances for codecs, configs, etc.), HKTs, and implicits/givens to build a strongly-typed domain model that still composes.
Scala.js compiles Scala 3 to JavaScript and supports browser, Node.js, and serverless.
You have two options:
- Laminar – FRP-style Scala.js UI library, very clean reactive model, great for SPAs.
- Tyrian – Elm-inspired Scala.js framework aimed at SPAs, built with Cats Effect/FS2 under the hood.
Tyrian in particular is very close to Elm’s architecture: a Model, a Msg ADT, and an update function. You get Elm-style reasoning while staying in Scala and reusing your Cats Effect knowledge.
If you’re willing to move away from React on the front-end, Laminar/Tyrian are a clean, fully FP story.
If you want to stick with React specifically, you can use Scala.js bindings like Slinky or scalajs-react (they’re listed in the Scala.js library index next to Laminar/Tyrian).
You still get:
- Strong typing
- FP style (especially if you keep using Cats Effect / ZIO for your effectful bits)
- JS interop when needed
But you keep the React mental model (components, hooks, etc.). It’s not as “pure FP” as Laminar/Tyrian, but it’ll feel familiar as a TS/React dev.
For CLIs, Scala is fine:
- decline – composable, Cats-based CLI parser, inspired by Haskell’s optparse-applicative.
- case-app – case-class-based, type-level CLI parser, widely used (Scala CLI, coursier).
Pair either with Cats Effect / ZIO, and optionally Scala Native if you want self-contained native binaries.
This covers your “occasional CLIs” requirement without introducing another language.
Realistic mapping:
- Express-style API →
http4sorZIO HTTP(or Cask if you want something more minimal). You run on the JVM instead of Node, but conceptually it’s an HTTP server with middleware, routing, and handlers. - React front-end →
- Either Scala.js + React bindings (keep React), or
- Scala.js + Tyrian/Laminar (Elm-style FP front-end).
The big difference from PureScript is: with Scala.js you’re not stuck in a tiny ecosystem. You can:
- Interop with JavaScript directly whenever needed.
- Use Scala.js-native UI libs like Laminar/Tyrian.
- Share domain types and codecs between backend and frontend via a cross-compiled
commonmodule (JVM + JS).
So you’re not just writing FP domain code with a mountain of JS glue; you can keep almost everything in Scala if you want.
These matter in day-to-day web/API work:
- HKTs – the reason Cats/ZIO/Kyo can exist in the first place, and why you can write generic abstractions over
F[_]. - Typeclass derivation – auto-derive JSON codecs, configs, schema descriptions, etc. Very handy for web APIs.
- Union & intersection types, opaque types, newtypes (yantl, etc.) – build rich domain models without runtime overhead, and keep the compiler screaming when you mess up.
- Implicits / givens – the mechanism behind typeclasses and a lot of FP “magic.”
You get most of Haskell’s type-level power, but with pragmatic escape hatches and a much larger “boring Java” ecosystem if you just need a library that works.
If I were you, with your background, I’d do this:
- Write a small CLI tool in Scala 3 + Cats Effect + decline or case-app. Get comfortable with the syntax,
IO, error handling, and basic effect composition. - Build a small HTTP API with
http4sorZIO HTTP, using Doobie + doobie-typesafe + yantl for DB and domain modelling. This will show you how FP applies to your usual API + DB stack. - Pick a front-end story:
- If you want Elm-like FP front-end: Tyrian or Laminar.
- If you want to keep React exactly: Scala.js + React bindings.
- Go full-stack with a shared
commonmodule:- Shared domain types
- Shared JSON codecs
- Shared endpoint definitions via Tapir.
- If you want guided, end-to-end practice: Rock the JVM – “Typelevel Rite of Passage” course builds exactly this: a full-stack Scala 3 jobs platform with Cats, Cats Effect, Doobie, http4s, and Tyrian, from scratch.
That course is basically a concrete answer to your question: “is there a language with ecosystems for web UIs, web APIs, CLIs, DB, in a functional style?” – yes: Scala, and that stack.
If your goal is:
- Learn FP properly, not just dabble.
- Still build real-world backends, frontends, and CLIs with good libraries.
- Avoid getting stuck in a research ecosystem.
Then Scala 3 with the Typelevel / ZIO / Kyo stacks plus Scala.js is a very good fit.
Use Haskell/Elm/PureScript for inspiration and reading; write production code in Scala.