Claude Code Plugins

Community-maintained marketplace

Feedback

convert-fsharp-scala

@aRustyDev/ai
0
0

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.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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

  1. Analyze source thoroughly before writing target - understand F# idioms
  2. Map types first - create type equivalence table for domain models
  3. Preserve semantics over syntax similarity - embrace Scala's hybrid nature
  4. Adopt target idioms - don't write "F# code in Scala syntax"
  5. Handle edge cases - null safety, error paths, resource cleanup
  6. Test equivalence - same inputs → same outputs
  7. 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: with in F#, copy in Scala
  • 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 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 → Scala Right, F# Error → Scala Left
  • 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 { } → Scala Future { } or IO { }
  • F# do! → Scala _<- in for-comprehension
  • F# let! → Scala x <- in for-comprehension
  • F# Async.Parallel → Scala Future.sequence or traverse
  • F# Async.RunSynchronously → Scala Await.result (Future) or unsafeRunSync() (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 → Scala yield (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# when guards → Scala if guards
  • F# [] → Scala Nil
  • F# x :: xs → Scala x :: 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 → Scala unapply returning Option
  • 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:
    1. Manual case classes - most common, explicit
    2. Runtime JSON libraries - circe, play-json, upickle
    3. Code generation plugins - sbt plugins for OpenAPI, Protobuf, etc.
    4. 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 examples
  • lang-fsharp-dev - F# development patterns
  • lang-scala-dev - Scala development patterns
  • convert-scala-fsharp - Reverse conversion (Scala → F#)

Cross-cutting pattern skills:

  • patterns-concurrency-dev - Async workflows, actors, effects across languages
  • patterns-serialization-dev - JSON, validation patterns across languages
  • patterns-metaprogramming-dev - Type providers, macros, type classes comparison