| name | convert-fsharp-scala |
| description | Convert F# code to idiomatic Scala. Use when migrating F# projects to Scala, translating functional-first .NET patterns to JVM functional programming, or refactoring F# codebases to Scala. Extends meta-convert-dev with F#-to-Scala specific patterns for discriminated unions, computation expressions, and type providers. |
Convert 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
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: 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 - Reverse conversion (Scala → F#) - see
convert-scala-fsharp - Type provider advanced patterns - requires manual translation strategy
Quick Reference
| F# | Scala | Notes |
|---|---|---|
type Person = { Name: string; Age: int } |
case class Person(name: String, age: Int) |
Records → case classes |
type Result<'T,'E> = Ok of 'T | Error of 'E |
Either[E, T] or custom sealed trait |
Discriminated unions → sealed traits |
Option<'T> |
Option[T] |
Direct mapping |
Result<'T,'E> |
Either[E, T] or Try[T] |
Result → Either (preferred) |
List<'T> |
List[T] |
Direct mapping (immutable) |
Array<'T> |
Array[T] or Vector[T] |
Arrays or vectors |
'T [] |
Array[T] |
Array syntax |
async { ... } |
Future { ... } or IO monad |
Async → Future or effect systems |
seq { ... } |
LazyList or Iterator |
Lazy sequences |
let! x = ... |
for { x <- ... } yield ... |
Computation expressions → for-comprehensions |
member _.Method() |
def method(): Unit |
Methods in classes/traits |
|> |
.pipe(_) or method chaining |
Pipe operator → method chaining |
>> |
andThen |
Function composition |
[<Attribute>] |
@annotation |
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 |
|---|---|---|
string |
String |
Direct mapping |
int |
Int |
32-bit signed integer |
int64 |
Long |
64-bit signed integer |
float / double |
Double |
64-bit floating point |
float32 / single |
Float |
32-bit floating point |
bool |
Boolean |
Direct mapping |
char |
Char |
Direct mapping |
byte |
Byte |
8-bit unsigned (Scala: signed) |
unit |
Unit |
Unit type |
obj |
Any or AnyRef |
Base object type |
decimal |
BigDecimal |
Arbitrary precision decimal |
bigint |
BigInt |
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 |
|---|---|---|
list<'T> / 'T list |
List[T] |
Immutable linked list |
array<'T> / 'T [] |
Array[T] |
Mutable array |
array<'T> / 'T [] |
Vector[T] |
Immutable indexed sequence (preferred) |
seq<'T> |
LazyList[T] (Scala 2.13+) |
Lazy evaluation |
seq<'T> |
Iterator[T] |
One-time iteration |
Set<'T> |
Set[T] |
Immutable set |
Map<'K,'V> |
Map[K, V] |
Immutable map |
ResizeArray<'T> |
mutable.ListBuffer[T] or mutable.ArrayBuffer[T] |
Mutable list |
'T * 'U (tuple) |
(T, U) |
Tuple syntax |
'T * 'U * 'V |
(T, U, V) |
Multi-element tuple |
Composite Types
| F# Pattern | Scala Pattern | Notes |
|---|---|---|
type Person = { Name: string; Age: int } |
case class Person(name: String, age: Int) |
Records → case classes |
type alias UserId = int |
type UserId = Int |
Type alias |
type Color = Red | Green | Blue |
sealed trait Color; case object Red extends Color; ... |
Simple unions → sealed traits with objects |
type Result<'T> = Success of 'T | Failure of string |
sealed trait Result[T]; case class Success[T](value: T) extends Result[T]; case class Failure[T](error: String) extends Result[T] |
Discriminated unions → sealed traits |
type Option<'T> = Some of 'T | None |
Option[T] (built-in) |
Built-in in both |
Single-case union: type EmailAddress = EmailAddress of string |
case class EmailAddress(value: String) or Scala 3 opaque types |
Wrapper types |
interface ILogger |
trait Logger |
Interface → trait |
type ILogger with member Log : string -> unit |
trait Logger { def log(message: String): Unit } |
Abstract members |
[<Struct>] type Point = { X: float; Y: float } |
Value classes or case classes | Struct types → value classes (limited) |
Generic Type Mappings
| F# | Scala | Notes |
|---|---|---|
'T |
T or A |
Generic type parameter |
list<'T> |
List[T] |
Generic collections |
'T when 'T : comparison |
T: Ordering (type class) |
Constrained generics |
'T when 'T :> IDisposable |
T <: AutoCloseable |
Upper bound |
^T when ^T : (static member Parse : string -> ^T) |
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,hashCode,toString - Scala uses camelCase for field names (F# uses PascalCase)
- Copy-and-update syntax is similar:
within F#,copyin Scala - String interpolation: F# uses
$"", Scala usess""
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
match/case - 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
Option.map, Scala uses.map(method) - F#
Option.defaultValue→ Scala.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#
Ok→ ScalaRight, F#Error→ ScalaLeft - F#
Result.bind→ Scala.flatMap - F#
Result.map→ Scala.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#
async { }→ ScalaFuture { }orIO { } - F#
do!→ Scala_<-in for-comprehension - F#
let!→ Scalax <-in for-comprehension - F#
Async.Parallel→ ScalaFuture.sequenceortraverse - F#
Async.RunSynchronously→ ScalaAwait.result(Future) orunsafeRunSync()(IO)
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#
let!→ Scala<-(bind/flatMap) - F#
return→ Scalayield(map) - 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#
whenguards → Scalaifguards - F#
[]→ ScalaNil - F#
x :: xs→ Scalax :: xs(same cons operator) - 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
Option→ ScalaunapplyreturningOption - 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 |
|---|---|---|
Option<'T> |
Option[T] |
Value may be absent |
Result<'T,'E> |
Either[E, T] |
Typed errors (preferred) |
Result<'T,exn> |
Try[T] |
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) |
|---|---|---|---|
async { } |
Future { } |
IO { } |
ZIO.attempt { } |
do! |
_ <- future |
_ <- io |
_ <- zio |
let! |
x <- future |
x <- io |
x <- zio |
return |
x (last expression) |
IO.pure(x) |
ZIO.succeed(x) |
Async.Parallel |
Future.sequence |
traverse / parTraverse |
ZIO.collectAllPar |
Async.RunSynchronously |
Await.result |
unsafeRunSync() |
Unsafe.run |
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 (dotnet) |
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:
meta-convert-dev- Foundational patterns with cross-language exampleslang-fsharp-dev- F# development patternslang-scala-dev- Scala development patternsconvert-scala-fsharp- Reverse conversion (Scala → F#)
Cross-cutting pattern skills:
patterns-concurrency-dev- Async workflows, actors, effects across languagespatterns-serialization-dev- JSON, validation patterns across languagespatterns-metaprogramming-dev- Type providers, macros, type classes comparison