Agents convert-fsharp-scala
Bidirectional conversion between Fsharp and Scala. Use when migrating projects between these languages in either direction. Extends meta-convert-dev with Fsharp↔Scala specific patterns.
git clone https://github.com/aRustyDev/agents
T=$(mktemp -d) && git clone --depth=1 https://github.com/aRustyDev/agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/content/skills/convert-fsharp-scala" ~/.claude/skills/arustydev-agents-convert-fsharp-scala && rm -rf "$T"
content/skills/convert-fsharp-scala/SKILL.mdConvert F# to Scala
Convert F# code to idiomatic Scala. This skill extends
meta-convert-dev with F#-to-Scala specific type mappings, idiom translations, and tooling for translating functional-first .NET code to JVM functional programming.
This Skill Extends
- Foundational conversion patterns (APTV workflow, testing strategies)meta-convert-dev
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: F# types → Scala types (discriminated unions, records, options)
- Idiom translations: F# patterns → idiomatic Scala (computation expressions, pattern matching, type providers)
- Error handling: F# Result/Option → Scala Option/Either/Try
- Async patterns: F# async workflows → Scala Future/Cats Effect/ZIO
- Paradigm translation: .NET functional-first → JVM functional/OOP hybrid
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - F# language fundamentals - see
lang-fsharp-dev - Scala language fundamentals - see
lang-scala-dev - Type provider advanced patterns - requires manual translation strategy
Quick Reference
| F# | Scala | Notes |
|---|---|---|
| | Records → case classes |
| or custom sealed trait | Discriminated unions → sealed traits |
| | Direct mapping |
| or | Result → Either (preferred) |
| | Direct mapping (immutable) |
| or | Arrays or vectors |
| | Array syntax |
| or IO monad | Async → Future or effect systems |
| or | Lazy sequences |
| | Computation expressions → for-comprehensions |
| | Methods in classes/traits |
| or method chaining | Pipe operator → method chaining |
| | Function composition |
| | Attributes → annotations |
When Converting Code
- Analyze source thoroughly before writing target - understand F# idioms
- Map types first - create type equivalence table for domain models
- Preserve semantics over syntax similarity - embrace Scala's hybrid nature
- Adopt target idioms - don't write "F# code in Scala syntax"
- Handle edge cases - null safety, error paths, resource cleanup
- Test equivalence - same inputs → same outputs
- Consider platform differences - .NET BCL → JVM stdlib/libraries
Type System Mapping
Primitive Types
| F# | Scala | Notes |
|---|---|---|
| | Direct mapping |
| | 32-bit signed integer |
| | 64-bit signed integer |
/ | | 64-bit floating point |
/ | | 32-bit floating point |
| | Direct mapping |
| | Direct mapping |
| | 8-bit unsigned (Scala: signed) |
| | Unit type |
| or | Base object type |
| | Arbitrary precision decimal |
| | Arbitrary precision integer |
Note on byte: F#
byte is unsigned (0-255), Scala Byte is signed (-128-127). Use Int if unsigned semantics are critical.
Collection Types
| F# | Scala | Notes |
|---|---|---|
/ | | Immutable linked list |
/ | | Mutable array |
/ | | Immutable indexed sequence (preferred) |
| (Scala 2.13+) | Lazy evaluation |
| | One-time iteration |
| | Immutable set |
| | Immutable map |
| or | Mutable list |
(tuple) | | Tuple syntax |
| | Multi-element tuple |
Composite Types
| F# Pattern | Scala Pattern | Notes |
|---|---|---|
| | Records → case classes |
| | Type alias |
| | Simple unions → sealed traits with objects |
| | Discriminated unions → sealed traits |
| (built-in) | Built-in in both |
Single-case union: | or Scala 3 opaque types | Wrapper types |
| | Interface → trait |
| | Abstract members |
| Value classes or case classes | Struct types → value classes (limited) |
Generic Type Mappings
| F# | Scala | Notes |
|---|---|---|
| or | Generic type parameter |
| | Generic collections |
| (type class) | Constrained generics |
| | Upper bound |
| Type classes via implicits | SRTP → type classes |
Idiom Translation
Pattern 1: Records to Case Classes
F#:
type Person = { FirstName: string LastName: string Age: int } let person = { FirstName = "Alice"; LastName = "Smith"; Age = 30 } let older = { person with Age = 31 } let fullName person = $"{person.FirstName} {person.LastName}"
Scala:
case class Person( firstName: String, lastName: String, age: Int ) val person = Person("Alice", "Smith", 30) val older = person.copy(age = 31) def fullName(person: Person): String = s"${person.firstName} ${person.lastName}"
Why this translation:
- Case classes provide automatic
,copy
,equals
,hashCodetoString - Scala uses camelCase for field names (F# uses PascalCase)
- Copy-and-update syntax is similar:
in F#,with
in Scalacopy - String interpolation: F# uses
, Scala uses$""s""
Pattern 2: Discriminated Unions to Sealed Traits
F#:
type PaymentMethod = | Cash | CreditCard of cardNumber: string | DebitCard of cardNumber: string * pin: int let processPayment method = match method with | Cash -> "Processing cash" | CreditCard cardNumber -> $"Processing card {cardNumber}" | DebitCard (cardNumber, _) -> $"Processing debit {cardNumber}"
Scala:
sealed trait PaymentMethod case object Cash extends PaymentMethod case class CreditCard(cardNumber: String) extends PaymentMethod case class DebitCard(cardNumber: String, pin: Int) extends PaymentMethod def processPayment(method: PaymentMethod): String = method match { case Cash => "Processing cash" case CreditCard(cardNumber) => s"Processing card $cardNumber" case DebitCard(cardNumber, _) => s"Processing debit $cardNumber" }
Why this translation:
- Sealed traits ensure exhaustive pattern matching like F# discriminated unions
- Case objects for parameterless variants
- Case classes for variants with data
- Pattern matching syntax is similar but Scala uses
/matchcase - Compiler enforces exhaustiveness in both languages
Pattern 3: Option Type Handling
F#:
let findUser id = if id = 1 then Some { FirstName = "Alice"; LastName = "Smith"; Age = 30 } else None // Pattern matching let greet user = match user with | Some u -> $"Hello, {u.FirstName}" | None -> "Hello, stranger" // Option combinators let name = findUser 1 |> Option.map (fun u -> u.FirstName) |> Option.defaultValue "Anonymous"
Scala:
def findUser(id: Int): Option[Person] = { if (id == 1) Some(Person("Alice", "Smith", 30)) else None } // Pattern matching def greet(user: Option[Person]): String = user match { case Some(u) => s"Hello, ${u.firstName}" case None => "Hello, stranger" } // Option combinators val name = findUser(1) .map(_.firstName) .getOrElse("Anonymous")
Why this translation:
- Both have built-in Option types with Some/None
- F# uses
, Scala usesOption.map
(method).map - F#
→ ScalaOption.defaultValue.getOrElse - F# pipe
→ Scala method chaining|>. - Pattern matching syntax nearly identical
Pattern 4: Result Type to Either
F#:
type Result<'T,'E> = | Ok of 'T | Error of 'E let divide x y = if y = 0 then Error "Division by zero" else Ok (x / y) // Railway-oriented programming let workflow = divide 10 2 |> Result.bind (fun x -> divide x 5) |> Result.map (fun x -> x * 2)
Scala:
// Use Either[E, T] (right-biased) def divide(x: Int, y: Int): Either[String, Int] = { if (y == 0) Left("Division by zero") else Right(x / y) } // Railway-oriented programming val workflow = for { x <- divide(10, 2) y <- divide(x, 5) } yield y * 2 // Or with explicit flatMap/map val workflow2 = divide(10, 2) .flatMap(x => divide(x, 5)) .map(x => x * 2)
Why this translation:
- F# Result → Scala Either (right-biased in Scala 2.12+)
- F#
→ ScalaOk
, F#Right
→ ScalaErrorLeft - F#
→ ScalaResult.bind.flatMap - F#
→ ScalaResult.map.map - For-comprehensions replace chained bind/map calls
Pattern 5: Async Workflows to Futures
F#:
let fetchData url = async { printfn $"Fetching {url}..." do! Async.Sleep 1000 return $"Data from {url}" } let processUrls urls = async { let! results = urls |> List.map fetchData |> Async.Parallel return results |> Array.toList } // Run async let urls = ["url1"; "url2"; "url3"] processUrls urls |> Async.RunSynchronously
Scala (with Futures):
import scala.concurrent.{Future, Await} import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global def fetchData(url: String): Future[String] = Future { println(s"Fetching $url...") Thread.sleep(1000) s"Data from $url" } def processUrls(urls: List[String]): Future[List[String]] = { Future.sequence(urls.map(fetchData)) } // Run async val urls = List("url1", "url2", "url3") val result = Await.result(processUrls(urls), 10.seconds)
Scala (with Cats Effect IO):
import cats.effect.{IO, unsafe} import cats.syntax.parallel._ import scala.concurrent.duration._ def fetchData(url: String): IO[String] = for { _ <- IO.println(s"Fetching $url...") _ <- IO.sleep(1.second) } yield s"Data from $url" def processUrls(urls: List[String]): IO[List[String]] = { urls.traverse(fetchData) // Or urls.parTraverse for parallel } // Run IO val urls = List("url1", "url2", "url3") processUrls(urls).unsafeRunSync()
Why this translation:
- F#
→ Scalaasync { }
orFuture { }IO { } - F#
→ Scalado!
in for-comprehension_<- - F#
→ Scalalet!
in for-comprehensionx <- - F#
→ ScalaAsync.Parallel
orFuture.sequencetraverse - F#
→ ScalaAsync.RunSynchronously
(Future) orAwait.result
(IO)unsafeRunSync()
Pattern 6: Computation Expressions to For-Comprehensions
F#:
// Option computation expression let validateAge age = if age >= 0 && age <= 120 then Some age else None let validateName name = if String.IsNullOrWhiteSpace(name) then None else Some name let createPerson name age = option { let! validName = validateName name let! validAge = validateAge age return { FirstName = validName; LastName = ""; Age = validAge } }
Scala:
// Option for-comprehension def validateAge(age: Int): Option[Int] = { if (age >= 0 && age <= 120) Some(age) else None } def validateName(name: String): Option[String] = { if (name == null || name.trim.isEmpty) None else Some(name) } def createPerson(name: String, age: Int): Option[Person] = for { validName <- validateName(name) validAge <- validateAge(age) } yield Person(validName, "", validAge)
Why this translation:
- F# computation expressions → Scala for-comprehensions
- F#
→ Scalalet!
(bind/flatMap)<- - F#
→ Scalareturn
(map)yield - Both desugar to flatMap/map chains
- Scala for-comprehensions work with any type that has flatMap/map
Pattern 7: Pattern Matching with Guards
F#:
let classify n = match n with | x when x < 0 -> "negative" | 0 -> "zero" | x when x % 2 = 0 -> "even positive" | _ -> "odd positive" // List pattern matching let sumFirst list = match list with | [] -> 0 | [x] -> x | x :: xs -> x + sumFirst xs
Scala:
def classify(n: Int): String = n match { case x if x < 0 => "negative" case 0 => "zero" case x if x % 2 == 0 => "even positive" case _ => "odd positive" } // List pattern matching def sumFirst(list: List[Int]): Int = list match { case Nil => 0 case x :: Nil => x case x :: xs => x + sumFirst(xs) }
Why this translation:
- F#
guards → Scalawhen
guardsif - F#
→ Scala[]Nil - F#
→ Scalax :: xs
(same cons operator)x :: xs - Both support deep pattern matching
- Scala enforces exhaustiveness on sealed types
Pattern 8: Active Patterns to Custom Extractors
F#:
// Active pattern for even/odd let (|Even|Odd|) n = if n % 2 = 0 then Even else Odd match 42 with | Even -> "even" | Odd -> "odd" // Partial active pattern let (|Integer|_|) (str: string) = match System.Int32.TryParse(str) with | true, value -> Some value | false, _ -> None match "123" with | Integer n -> $"Number: {n}" | _ -> "Not a number"
Scala:
// Custom extractor for even/odd object Even { def unapply(n: Int): Option[Int] = if (n % 2 == 0) Some(n) else None } object Odd { def unapply(n: Int): Option[Int] = if (n % 2 != 0) Some(n) else None } 42 match { case Even(n) => "even" case Odd(n) => "odd" } // Partial extractor object IntegerString { def unapply(str: String): Option[Int] = { try { Some(str.toInt) } catch { case _: NumberFormatException => None } } } "123" match { case IntegerString(n) => s"Number: $n" case _ => "Not a number" }
Why this translation:
- F# active patterns → Scala custom extractors (
)unapply - F# parameterless active patterns → Scala objects with
unapply - F# partial active patterns returning
→ ScalaOption
returningunapplyOption - Both enable extensible pattern matching
- Scala extractors are more verbose but more flexible
Pattern 9: Units of Measure to Tagged Types
F#:
[<Measure>] type kg [<Measure>] type m [<Measure>] type s let distance = 100.0<m> let time = 10.0<s> let speed = distance / time // Type: float<m/s> // Prevents mixing units let mass = 50.0<kg> // let invalid = distance + mass // Compile error!
Scala (with Tagged Types):
// Using shapeless tagged types (library) import shapeless.tag._ import shapeless.tag trait Kg trait M trait S type Kilograms = Double @@ Kg type Meters = Double @@ M type Seconds = Double @@ S val distance: Meters = tag[M](100.0) val time: Seconds = tag[S](10.0) // val speed = distance / time // Would need custom operators // Or use value classes (zero runtime overhead) case class Kilograms(value: Double) extends AnyVal case class Meters(value: Double) extends AnyVal case class Seconds(value: Double) extends AnyVal val distance = Meters(100.0) val time = Seconds(10.0) // val invalid = distance.value + Kilograms(50.0).value // No type safety at operation level
Scala 3 (with Opaque Types):
object Units { opaque type Kilograms = Double opaque type Meters = Double opaque type Seconds = Double object Kilograms { def apply(value: Double): Kilograms = value extension (kg: Kilograms) def value: Double = kg } object Meters { def apply(value: Double): Meters = value extension (m: Meters) def value: Double = m } object Seconds { def apply(value: Double): Seconds = value extension (s: Seconds) def value: Double = s } } import Units._ val distance = Meters(100.0) val time = Seconds(10.0)
Why this translation:
- F# units of measure have no direct Scala equivalent
- Scala 2: Use tagged types (shapeless) or value classes
- Scala 3: Opaque types provide zero-cost abstraction
- F# provides compile-time dimension checking, Scala only type checking
- Trade-off: F# has better unit inference, Scala requires more manual work
Pattern 10: Type Providers to Code Generation
F#:
open FSharp.Data // Type provider infers schema from JSON sample type Weather = JsonProvider<""" { "temperature": 72.5, "condition": "sunny", "humidity": 65 } """> let weather = Weather.Load("weather.json") printfn $"Temperature: {weather.Temperature}°F"
Scala:
// No direct equivalent - use code generation or libraries // Option 1: Manual case classes case class Weather( temperature: Double, condition: String, humidity: Int ) // Option 2: Use circe for JSON (runtime decoding) import io.circe._ import io.circe.generic.semiauto._ case class Weather(temperature: Double, condition: String, humidity: Int) implicit val weatherDecoder: Decoder[Weather] = deriveDecoder[Weather] val json = """{"temperature":72.5,"condition":"sunny","humidity":65}""" val weather = parser.decode[Weather](json) // Option 3: Use sbt-swagger-codegen plugin for OpenAPI // Generates case classes from OpenAPI/Swagger specs at compile time // Option 4: Scala 3 macros (advanced) // Can generate types at compile time from external sources
Why this translation:
- F# type providers have no direct Scala equivalent
- Scala alternatives:
- Manual case classes - most common, explicit
- Runtime JSON libraries - circe, play-json, upickle
- Code generation plugins - sbt plugins for OpenAPI, Protobuf, etc.
- Scala 3 macros - can achieve similar results but more complex
- F# advantage: compile-time type safety from external data sources
- Scala advantage: more explicit, easier to debug, better tooling support
Error Handling
F# Result/Option → Scala Option/Either/Try
F# uses Result and Option types for error handling. Scala has Option, Either, and Try.
| F# | Scala | Use Case |
|---|---|---|
| | Value may be absent |
| | Typed errors (preferred) |
| | Exception wrapping |
F# Result translation:
type Result<'T,'E> = | Ok of 'T | Error of 'E let divide x y = if y = 0 then Error "Division by zero" else Ok (x / y) // Railway-oriented programming let calculation = divide 10 2 |> Result.bind (fun x -> divide x 5) |> Result.map (fun x -> x * 2)
Scala Either (preferred):
type Result[T] = Either[String, T] def divide(x: Int, y: Int): Either[String, Int] = { if (y == 0) Left("Division by zero") else Right(x / y) } // Railway-oriented programming val calculation = for { x <- divide(10, 2) y <- divide(x, 5) } yield y * 2 // Or explicit flatMap/map val calculation2 = divide(10, 2) .flatMap(x => divide(x, 5)) .map(x => x * 2)
Scala Try (for exception wrapping):
import scala.util.{Try, Success, Failure} def divide(x: Int, y: Int): Try[Int] = Try { if (y == 0) throw new ArithmeticException("Division by zero") x / y } divide(10, 2) match { case Success(value) => println(s"Result: $value") case Failure(exception) => println(s"Error: ${exception.getMessage}") }
Custom error types:
// F# type ValidationError = | EmptyString | InvalidFormat | OutOfRange let validateAge age : Result<int, ValidationError> = if age < 0 || age > 120 then Error OutOfRange else Ok age
// Scala sealed trait ValidationError case object EmptyString extends ValidationError case object InvalidFormat extends ValidationError case object OutOfRange extends ValidationError def validateAge(age: Int): Either[ValidationError, Int] = { if (age < 0 || age > 120) Left(OutOfRange) else Right(age) }
Concurrency Patterns
F# Async Workflows → Scala Futures/Effects
F# uses async workflows and Async module. Scala has multiple options: Futures (simple), Cats Effect (functional), ZIO (full effect system).
| F# | Scala (Future) | Scala (Cats Effect) | Scala (ZIO) |
|---|---|---|---|
| | | |
| | | |
| | | |
| (last expression) | | |
| | / | |
| | | |
Example: Parallel execution
F#:
let fetchUser id = async { do! Async.Sleep 100 return $"User {id}" } let fetchAll ids = async { let! users = ids |> List.map fetchUser |> Async.Parallel return users |> Array.toList } fetchAll [1; 2; 3] |> Async.RunSynchronously
Scala (Future):
import scala.concurrent.{Future, Await} import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ def fetchUser(id: Int): Future[String] = Future { Thread.sleep(100) s"User $id" } def fetchAll(ids: List[Int]): Future[List[String]] = { Future.sequence(ids.map(fetchUser)) } Await.result(fetchAll(List(1, 2, 3)), 10.seconds)
Scala (Cats Effect IO):
import cats.effect.IO import cats.syntax.parallel._ import scala.concurrent.duration._ def fetchUser(id: Int): IO[String] = for { _ <- IO.sleep(100.millis) } yield s"User $id" def fetchAll(ids: List[Int]): IO[List[String]] = { ids.parTraverse(fetchUser) // Parallel // or ids.traverse(fetchUser) for sequential } fetchAll(List(1, 2, 3)).unsafeRunSync()
Key differences:
- F# async is cold (doesn't run until explicitly started)
- Scala Future is hot (starts immediately upon creation)
- Cats Effect IO is cold (like F# async)
- Use IO/ZIO for resource-safe, referentially transparent effects
Common Pitfalls
1. PascalCase vs camelCase
Problem: F# uses PascalCase for everything, Scala uses camelCase for members.
// F# style type Person = { FirstName: string LastName: string } let GetFullName person = $"{person.FirstName} {person.LastName}"
Fix: Follow Scala conventions
// Scala style case class Person( firstName: String, // camelCase lastName: String ) def getFullName(person: Person): String = s"${person.firstName} ${person.lastName}"
2. Pipe Operator Overuse
Problem: Trying to replicate F# pipe operator (
|>) everywhere.
// ❌ Bad: Non-idiomatic def |>[A, B](a: A, f: A => B): B = f(a) val result = 5 |> (x => x + 1) |> (x => x * 2)
Fix: Use Scala's method chaining
// ✓ Good: Idiomatic Scala val result = 5 .pipe(x => x + 1) .pipe(x => x * 2) // Or even better with direct chaining val result = List(1, 2, 3) .map(_ + 1) .filter(_ > 2) .sum
3. Result Type Confusion
Problem: F# Result has Ok/Error, Scala Either has Right/Left (right-biased).
// ❌ Bad: Using Either like F# Result def divide(x: Int, y: Int): Either[Int, String] = { if (y == 0) Right("Division by zero") // Wrong: should be Left else Left(x / y) // Wrong: should be Right }
Fix: Remember Either is right-biased (Right for success)
// ✓ Good: Correct Either usage def divide(x: Int, y: Int): Either[String, Int] = { if (y == 0) Left("Division by zero") // Error on Left else Right(x / y) // Success on Right }
4. Discriminated Union Translation
Problem: Trying to use case classes like F# union cases.
// ❌ Bad: Single case class hierarchy without sealed trait case class Success(value: Int) case class Failure(error: String) def handle(result: Any): String = result match { case Success(v) => s"Got $v" case Failure(e) => s"Error: $e" // Missing: no exhaustiveness checking }
Fix: Use sealed traits for exhaustiveness
// ✓ Good: Sealed trait for ADT sealed trait Result case class Success(value: Int) extends Result case class Failure(error: String) extends Result def handle(result: Result): String = result match { case Success(v) => s"Got $v" case Failure(e) => s"Error: $e" // Compiler ensures exhaustiveness }
5. Computation Expression to For-Comprehension Mismatch
Problem: F# computation expressions have custom builders, Scala for-comprehensions require flatMap/map.
// F# custom computation expression type MaybeBuilder() = member _.Bind(x, f) = Option.bind f x member _.Return(x) = Some x member _.ReturnFrom(x) = x member _.Zero() = None let maybe = MaybeBuilder() let result = maybe { let! x = Some 10 let! y = Some 20 return x + y }
// Scala: Can only use types that have flatMap/map // Option already has these, so for-comprehension works val result = for { x <- Some(10) y <- Some(20) } yield x + y // For custom types, must implement flatMap/map class Maybe[A](value: Option[A]) { def flatMap[B](f: A => Maybe[B]): Maybe[B] = { value match { case Some(v) => f(v) case None => new Maybe(None) } } def map[B](f: A => B): Maybe[B] = { new Maybe(value.map(f)) } }
6. Async Workflow Startup Semantics
Problem: F# async is cold (lazy), Scala Future is hot (eager).
// ❌ Bad: Assuming Future is lazy like F# async val future = Future { println("Running expensive operation") expensiveComputation() } // Prints immediately! Future started as soon as it's created // Later in code... future.map(result => process(result)) // Operation already running
Fix: Use Cats Effect IO or ZIO for lazy async (or accept Future's eager semantics)
// ✓ Good: Using IO for lazy async (like F# async) import cats.effect.IO val io = IO { println("Running expensive operation") expensiveComputation() } // Nothing printed yet - IO is lazy // Later in code... io.map(result => process(result)) // Still not running // Must explicitly run io.unsafeRunSync() // Now it runs
7. Type Provider Expectations
Problem: Expecting Scala to have type providers like F#.
// F# has compile-time type generation open FSharp.Data type Users = JsonProvider<"users.json"> let users = Users.Load("users.json") users.Items.[0].Name // Full IntelliSense!
Fix: Use appropriate Scala alternatives
// Scala: Define types manually or use code generation // Option 1: Manual (most common) case class User(name: String, age: Int, email: String) import io.circe.generic.auto._ import io.circe.parser._ val json = """[{"name":"Alice","age":30,"email":"alice@example.com"}]""" val users = decode[List[User]](json) // Option 2: Use sbt plugins for code generation // plugins.sbt: // addSbtPlugin("io.swagger" % "sbt-swagger-codegen" % "0.1.0")
8. Railway-Oriented Programming Style
Problem: Overusing F#-style railway-oriented programming without leveraging Scala's for-comprehensions.
// ❌ Bad: Transliterating F# style val result = divide(10, 2) .flatMap(x => divide(x, 5)) .flatMap(x => divide(x, 1)) .map(x => x * 2)
Fix: Use for-comprehensions for readability
// ✓ Good: Idiomatic Scala val result = for { x <- divide(10, 2) y <- divide(x, 5) z <- divide(y, 1) } yield z * 2
Tooling
Build Tools
| F# | Scala | Notes |
|---|---|---|
.NET CLI () | sbt | Primary build tool |
| .fsproj | build.sbt | Project configuration |
| Paket | Coursier | Dependency resolution |
| FAKE | Mill | Alternative build tool |
| NuGet | Maven Central | Package repository |
Build comparison:
# F# dotnet build dotnet test dotnet run # Scala sbt compile sbt test sbt run
IDE Support
| Feature | F# | Scala |
|---|---|---|
| Visual Studio | ✓ | - |
| Visual Studio Code | ✓ (Ionide) | ✓ (Metals) |
| JetBrains | Rider | IntelliJ IDEA |
| Vim/Neovim | ✓ (coc.nvim) | ✓ (coc-metals) |
Testing Frameworks
| F# | Scala | Notes |
|---|---|---|
| Expecto | ScalaTest | BDD-style testing |
| xUnit.net | MUnit | xUnit-style testing |
| FsUnit | specs2 | Fluent assertions |
| FsCheck | ScalaCheck | Property-based testing |
Code Formatting
| F# | Scala | Command |
|---|---|---|
| Fantomas | Scalafmt | Auto-formatting |
# F# dotnet fantomas . # Scala sbt scalafmt
Useful Libraries
| Purpose | F# | Scala |
|---|---|---|
| JSON | FSharp.Data, Thoth.Json | circe, play-json, upickle |
| HTTP client | FsHttp | http4s, sttp, requests-scala |
| Effect system | - (built-in async) | Cats Effect, ZIO |
| Validation | FsToolkit.ErrorHandling | Cats Validated, ZIO Prelude |
| Testing | Expecto, FsCheck | ScalaTest, ScalaCheck, MUnit |
| Collections | FSharpPlus | Cats, Scalaz |
| Parsing | FParsec | fastparse, cats-parse |
Paradigm Translation
Functional-First (.NET) → Functional/OOP Hybrid (JVM)
F# is functional-first on .NET, Scala is a hybrid functional/OOP language on JVM.
Mental model shifts:
| F# Approach | Scala Approach | Key Insight |
|---|---|---|
| Modules with functions | Objects with methods or traits | Data and behavior can be separate or combined |
| Computation expressions | For-comprehensions or effect systems | Monadic composition is built-in to language |
| Type providers | Manual types or code generation | More explicit, less magic |
| Units of measure | Tagged types or value classes | Less type safety, more verbosity |
| Active patterns | Custom extractors | More boilerplate, more flexibility |
| Discriminated unions | Sealed traits + case classes/objects | More verbose but more powerful |
| Records | Case classes | Similar functionality, different syntax |
Object-oriented integration:
F# prefers module functions, Scala embraces both styles:
// F# module style (preferred) module UserService = let findById id = // ... let save user = // ...
// Scala: can use either style // Functional style (similar to F#) object UserService { def findById(id: Int): Option[User] = ??? def save(user: User): Unit = ??? } // OOP style (Scala-specific) trait UserService { def findById(id: Int): Option[User] def save(user: User): Unit } class UserServiceImpl extends UserService { def findById(id: Int): Option[User] = ??? def save(user: User): Unit = ??? }
When to use OOP in Scala:
- Dependency injection (traits as interfaces)
- Plugin architecture (trait hierarchies)
- State management (classes with mutable state)
- Java interop (Java expects classes/interfaces)
When to use FP in Scala:
- Pure transformations (map, filter, fold)
- Immutable data structures
- Error handling (Option, Either, Try)
- Effect management (IO, ZIO)
Examples
Example 1: Simple - Domain Model Translation
Convert a simple F# domain model to Scala.
Before (F#):
type EmailAddress = EmailAddress of string module EmailAddress = let create email = if email.Contains("@") then Ok (EmailAddress email) else Error "Invalid email format" let value (EmailAddress email) = email type Person = { Name: string Email: EmailAddress Age: int } let createPerson name email age = match EmailAddress.create email with | Ok validEmail -> Ok { Name = name; Email = validEmail; Age = age } | Error msg -> Error msg
After (Scala):
case class EmailAddress private (value: String) object EmailAddress { def create(email: String): Either[String, EmailAddress] = { if (email.contains("@")) Right(EmailAddress(email)) else Left("Invalid email format") } } case class Person( name: String, email: EmailAddress, age: Int ) def createPerson(name: String, email: String, age: Int): Either[String, Person] = { EmailAddress.create(email).map(validEmail => Person(name, validEmail, age) ) } // Or with for-comprehension def createPerson2(name: String, email: String, age: Int): Either[String, Person] = for { validEmail <- EmailAddress.create(email) } yield Person(name, validEmail, age)
Example 2: Medium - Result-Based Validation
Convert F# railway-oriented validation to Scala.
Before (F#):
type ValidationError = | EmptyName | InvalidAge | InvalidEmail type ValidatedPerson = { Name: string Email: string Age: int } let validateName name = if String.IsNullOrWhiteSpace(name) then Error EmptyName else Ok name let validateAge age = if age < 0 || age > 120 then Error InvalidAge else Ok age let validateEmail email = if email.Contains("@") then Ok email else Error InvalidEmail let validatePerson name email age = result { let! validName = validateName name let! validEmail = validateEmail email let! validAge = validateAge age return { Name = validName Email = validEmail Age = validAge } } // Usage match validatePerson "Alice" "alice@example.com" 30 with | Ok person -> printfn $"Valid: {person.Name}" | Error EmptyName -> printfn "Name is empty" | Error InvalidAge -> printfn "Age is invalid" | Error InvalidEmail -> printfn "Email is invalid"
After (Scala):
sealed trait ValidationError case object EmptyName extends ValidationError case object InvalidAge extends ValidationError case object InvalidEmail extends ValidationError case class ValidatedPerson( name: String, email: String, age: Int ) def validateName(name: String): Either[ValidationError, String] = { if (name == null || name.trim.isEmpty) Left(EmptyName) else Right(name) } def validateAge(age: Int): Either[ValidationError, Int] = { if (age < 0 || age > 120) Left(InvalidAge) else Right(age) } def validateEmail(email: String): Either[ValidationError, String] = { if (email.contains("@")) Right(email) else Left(InvalidEmail) } def validatePerson(name: String, email: String, age: Int): Either[ValidationError, ValidatedPerson] = for { validName <- validateName(name) validEmail <- validateEmail(email) validAge <- validateAge(age) } yield ValidatedPerson(validName, validEmail, validAge) // Usage validatePerson("Alice", "alice@example.com", 30) match { case Right(person) => println(s"Valid: ${person.name}") case Left(EmptyName) => println("Name is empty") case Left(InvalidAge) => println("Age is invalid") case Left(InvalidEmail) => println("Email is invalid") }
Example 3: Complex - Async Workflow with Error Handling
Convert a complete F# async application with error handling to Scala.
Before (F#):
open System type ApiError = | NetworkError of message: string | NotFound | InvalidResponse of message: string type User = { Id: int Name: string Email: string } type UserRepository = abstract member FindById: int -> Async<Result<User, ApiError>> abstract member Save: User -> Async<Result<unit, ApiError>> type HttpClient = abstract member Get: string -> Async<Result<string, ApiError>> abstract member Post: string -> string -> Async<Result<string, ApiError>> let parseUserJson (json: string) : Result<User, ApiError> = try // Simplified JSON parsing let user = { Id = 1 Name = "Alice" Email = "alice@example.com" } Ok user with | ex -> Error (InvalidResponse ex.Message) let fetchAndUpdateUser (client: HttpClient) (repo: UserRepository) userId = async { // Fetch user from repository let! userResult = repo.FindById userId match userResult with | Error err -> return Error err | Ok user -> // Fetch additional data from API let! apiResult = client.Get $"https://api.example.com/users/{userId}" match apiResult with | Error err -> return Error err | Ok json -> match parseUserJson json with | Error err -> return Error err | Ok apiUser -> // Update and save let updated = { user with Email = apiUser.Email } let! saveResult = repo.Save updated match saveResult with | Error err -> return Error err | Ok () -> return Ok updated } // Better with computation expression let fetchAndUpdateUserCE (client: HttpClient) (repo: UserRepository) userId = async { let! userResult = repo.FindById userId return! match userResult with | Error err -> async { return Error err } | Ok user -> async { let! apiResult = client.Get $"https://api.example.com/users/{userId}" return! match apiResult with | Error err -> async { return Error err } | Ok json -> match parseUserJson json with | Error err -> async { return Error err } | Ok apiUser -> let updated = { user with Email = apiUser.Email } let! saveResult = repo.Save updated return match saveResult with | Error err -> Error err | Ok () -> Ok updated } }
After (Scala with Cats Effect):
import cats.effect.IO import cats.syntax.either._ import cats.syntax.flatMap._ import cats.syntax.functor._ sealed trait ApiError case class NetworkError(message: String) extends ApiError case object NotFound extends ApiError case class InvalidResponse(message: String) extends ApiError case class User( id: Int, name: String, email: String ) trait UserRepository { def findById(id: Int): IO[Either[ApiError, User]] def save(user: User): IO[Either[ApiError, Unit]] } trait HttpClient { def get(url: String): IO[Either[ApiError, String]] def post(url: String, body: String): IO[Either[ApiError, String]] } def parseUserJson(json: String): Either[ApiError, User] = { try { // Simplified JSON parsing val user = User(1, "Alice", "alice@example.com") Right(user) } catch { case ex: Exception => Left(InvalidResponse(ex.getMessage)) } } def fetchAndUpdateUser( client: HttpClient, repo: UserRepository, userId: Int ): IO[Either[ApiError, User]] = { for { userResult <- repo.findById(userId) result <- userResult match { case Left(err) => IO.pure(Left(err)) case Right(user) => for { apiResult <- client.get(s"https://api.example.com/users/$userId") finalResult <- apiResult match { case Left(err) => IO.pure(Left(err)) case Right(json) => parseUserJson(json) match { case Left(err) => IO.pure(Left(err)) case Right(apiUser) => val updated = user.copy(email = apiUser.email) repo.save(updated).map(_.map(_ => updated)) } } } yield finalResult } } yield result } // Better with EitherT (monad transformer) import cats.data.EitherT def fetchAndUpdateUserET( client: HttpClient, repo: UserRepository, userId: Int ): IO[Either[ApiError, User]] = { val result = for { user <- EitherT(repo.findById(userId)) json <- EitherT(client.get(s"https://api.example.com/users/$userId")) apiUser <- EitherT.fromEither[IO](parseUserJson(json)) updated = user.copy(email = apiUser.email) _ <- EitherT(repo.save(updated)) } yield updated result.value } // Or with Cats Effect's built-in error handling def fetchAndUpdateUserSimple( client: HttpClient, repo: UserRepository, userId: Int ): IO[User] = { for { user <- repo.findById(userId).flatMap(IO.fromEither) json <- client.get(s"https://api.example.com/users/$userId").flatMap(IO.fromEither) apiUser <- IO.fromEither(parseUserJson(json)) updated = user.copy(email = apiUser.email) _ <- repo.save(updated).flatMap(IO.fromEither) } yield updated }
Scala (with ZIO):
import zio._ sealed trait ApiError case class NetworkError(message: String) extends ApiError case object NotFound extends ApiError case class InvalidResponse(message: String) extends ApiError case class User(id: Int, name: String, email: String) trait UserRepository { def findById(id: Int): IO[ApiError, User] def save(user: User): IO[ApiError, Unit] } trait HttpClient { def get(url: String): IO[ApiError, String] def post(url: String, body: String): IO[ApiError, String] } def parseUserJson(json: String): IO[ApiError, User] = ZIO.attempt { User(1, "Alice", "alice@example.com") }.mapError(ex => InvalidResponse(ex.getMessage)) def fetchAndUpdateUser( client: HttpClient, repo: UserRepository, userId: Int ): IO[ApiError, User] = for { user <- repo.findById(userId) json <- client.get(s"https://api.example.com/users/$userId") apiUser <- parseUserJson(json) updated = user.copy(email = apiUser.email) _ <- repo.save(updated) } yield updated // ZIO automatically handles error propagation with IO[E, A] // No need for Either wrapping or EitherT
See Also
For more examples and patterns, see:
- Foundational patterns with cross-language examplesmeta-convert-dev
- F# development patternslang-fsharp-dev
- Scala development patternslang-scala-dev
Cross-cutting pattern skills:
- Async workflows, actors, effects across languagespatterns-concurrency-dev
- JSON, validation patterns across languagespatterns-serialization-dev
- Type providers, macros, type classes comparisonpatterns-metaprogramming-dev