Comparing Scala 🪜 to F# 🦔

jkone27
14 min readOct 6, 2024

--

FP languages in JVM vs .NET

Scala lang official logo

Scala and F# are both versatile languages that incorporate functional programming concepts, but F# has quite many features that make it at its core a “functional first” language (ML family of languages), while Scala follows mixing and matching many object orientation concepts with some Haskell concepts like higher order kinds and type classes, both relying on traits.

REPL 👩🏻‍🏫

both F# and Scala have a Read Evaluate and Print Loop (REPL), which is awesome for fast prototyping and quick POCs, and learning a new language! So you can get started easilly

$ scala
Welcome to Scala 3.5.1 (1.8.0_322, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.

scala> 1 + 1
val res0: Int = 2

and F# via dotnet fsi notice in F# you need a double semicolon to execute a command ;; this is because the REPL is somewhat inherited from OCaml language (together with ; keyword as well).

$ dotnet fsi
Microsoft (R) F# Interactive version 11.0.0.0 for F# 5.0
Copyright (c) Microsoft Corporation. All Rights Reserved.

For help type #help;;

> let square x = x * x;;
val square : x:int -> int

To exit the repl in F# write #quit;; in your terminal.

Expression Based Languages 🧑🏾‍🎨

both F# and Scala are expression based languages with higher order functions, and in virtue of this, functions are first class citizens.

F# adopts thet let keyword to define all expressions both functions and values, let mutable for mutable values.

Scala uses val for immutable values, var for mutable values and def keyword for methods and functions.

F# has also the member keyword within types (classes), but its usage is less common, is frequent to expose a method using a simple let binding from a module in F# (same as a static class in C#).

Both F# and scala share the concept of unit type, with value () to represent an empty set (close but not equal to void in other languages).

val fruits =
List("apple", "banana", "avocado", "papaya")

val countsToFruits = // count how many 'a' in each fruit
fruits.groupBy(fruit => fruit.count(_ == 'a'))

for (count, fruits) <- countsToFruits do
println(s"with 'a' × $count = $fruits")

class Abc(var a: String = "A", var b: Int) {
println("Hello world from Abc")
}

class Cde extends Abc("A", 10) { }

var x = new Abc("hello", 11)
var y = new Cde()

def someFunc() = "hello"

var z = someFunc()
println(z)

The same code in F# could be, for example

let fruits = 
["apple"; "banana"; "avocado"; "papaya"]

let countsToFruits =
fruits
|> List.groupBy (fun fruit -> fruit |> Seq.filter ((=) 'a') |> Seq.length)

for (count, fruits) in countsToFruits do
printfn "with 'a' × %d = %A" count fruits

type Abc(a: string, b: int) =
do printfn "Hello world from Abc"

type Cde() = inherit Abc("A", 10)

let x = Abc("hello", 11)
let y = Cde()

let someFunc() = "hello"
let z = someFunc()
printfn "%s" z

outputs:

// Scala output: 

with 'a' × 1 = List(apple)
with 'a' × 2 = List(avocado)
with 'a' × 3 = List(banana, papaya)
Hello world from Abc
Hello world from Abc

// F# output:

with 'a' × 1 = [apple]
with 'a' × 3 = [banana; papaya]
with 'a' × 2 = [avocado]
Hello world from Abc
Hello world from Abc

Scala Only: Traits 🪜

Inheritance over composition…(?) Traits are used to share interfaces and fields between classes. They are similar to Java 8’s interfaces. Classes and objects can extend traits, but traits cannot be instantiated and therefore have no parameters. src

  • The difference in multiple inheritance can lead to different design patterns and idioms being used in Scala compared to .NET. Scala developers might leverage traits to create more abstract systems, while .NET developers might rely on composition over inheritance and interfaces (essentially purely abstract classes).
  • Traits multiple inheritance can introduce complexity such as the “diamond problem” (where two parent classes define the same method), Scala handles this via a linearization process. F# (.NET) avoids the issue by disallowing multiple class inheritance altogether.
trait Show[A] {
def show(value: A): String

// Concrete method
def defaultShow(): String = "Default Implementation"
}

class IntShow extends Show[Int] {
def show(value: Int): String = value.toString
}

object Main extends App {
val intShow = new IntShow
println(intShow.show(42)) // Output: 42
println(intShow.defaultShow()) // Output: Default Implementation
}

F# Interfaces vs Abstract Classes

F# does not really suport the concept of “mixins” or “trait” and probably is a design decision, C# recently introduced default interface implementations in recent version of the language.

The closest thing to a trait in F# is an interface, but it has no state and no default members. abstract classes in .NET on the other hand only allow single time inheritance, not multiple inheritance.

F# separates interface implementation synthax using interface X with in contrast with inherit for abstract and concrete class inheritance, making it pleasantly distinct and clear.

type IShow<'T> =
abstract member Show : 'T -> string

type IntShow() =
member this.DefaultShow() = "Default Implementation"
interface IShow<int> with
member this.Show(value) = value.ToString()

// Usage
let intShow = IntShow()
printfn "%s" ((intShow :> IShow<int>).Show(42)) // Output: 42
printfn "%s" (intShow.DefaultShow()) // Output: Default Implementation

We see that allthough .NET does not envy traits for any of the default usages, but can do just with multiple interface inheritance, and individual abstract class implementation always favouring composition over inheritance, Traits offer Scala quite a few special/advanced typesystem features that the language shares with Haskell.

Weather those extra advanced type system features will be useful or not depends on the context and the style of your programs, and some argue in favor, others against. Are they easy to mantain, undertsand and evolve?

Scala Only: Type Classes (via traits) 🪜

In Scala, type classes are a powerful and flexible way to achieve ad-hoc polymorphism.

They enable you to define behavior for types without modifying them directly. The concept comes from Haskell and has been adopted in Scala, allowing for more extensibility and separation of concerns in your code.

A typeclass is typically defined as a trait with methods that provide the required functionality.

You provide instances of the typeclass for specific types using implicit values, allowing Scala to automatically resolve the appropriate implementation when needed.

New types can be made to conform to a typeclass by creating new implicit instances without modifying existing code.

Here’s a simple example of a typeclass that defines a Show typeclass for string representation:

trait Show[A] {
def show(value: A): String
}

object Show {
implicit val intShow: Show[Int] = new Show[Int] {
def show(value: Int): String = value.toString
}
implicit val stringShow: Show[String] = new Show[String] {
def show(value: String): String = s""""$value""""
}
}

def printValue[A](value: A)(implicit s: Show[A]): Unit = {
println(s.show(value))
}

// Usage
printValue(42) // Output: 42
printValue("hello") // Output: "hello"

In this example:

  • The Show typeclass defines a method show for converting a value to a string representation.
  • Implicit instances are created for Int and String, enabling the printValue function to print any type that has a corresponding Show instance.

Scala Only: Higher-Kinded Types (via traits) 🪜

Scala supports higher-kinded types, also inherited from Haskell, which allow types to be parameterized by other type constructors. This is especially useful for creating abstractions over type constructors, such as List, Option, or custom types.

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

case class Box[A](value: A)

object Box {
implicit val boxFunctor: Functor[Box] = new Functor[Box] {
def map[A, B](box: Box[A])(f: A => B): Box[B] = Box(f(box.value))
}
}

// Usage
val box = Box(5)

val mappedBox: Box[String] = boxFunctor.map(box)(_.toString)

println(mappedBox) // Output: Box(5)

F# and also other ML-languages like OCaml, PureScript, Elm or Reason, do not support this construct either, since it’s a feature that originates in Haskell.✨

In this example:

  • Functor[F[_]] is a type constructor that takes one type parameter F.
  • The Box type is made to be a functor by providing a concrete implementation of the map method.

Only F#: Type Providers ✨ 🦔

F# type providers allows a programmatic and extensible way to inject external knowledge within the F# compiler, having both fully generated types, thus accessible by other languages in .NET like swagger provider, and “on the fly” types (compiler erased only accessible from F#), this is a unique and outstanding feature of the F# language. For example FSharp.Data

#r "nuget: FSharp.Data"
open FSharp.Data

type WeatherJson = JsonProvider<"""{
"current": {
"temp_c": 15.0,
"condition": {
"text": "Partly cloudy"
}
}
}""">

let weather = WeatherJson.GetSample()

printfn "Current temperature: %.1f °C" weather.Current.TempC
printfn "Condition: %s" weather.Current.Condition.Text

Scala has somewhat similar concepts for meta programming and type generations from data (like Rust for example) via Macros, but the ease of use and extensibility in both cases does not really match fully the one offered by F# type providers, which won an ACM mention award 🥇 not by chance.

Comprehensions (aka Monads Magic) 🧞‍♀

if you are a monad wizard, don’t worry, both F# and Scala got you covered.

In Scala, you can extend the usage of for-comprehensions by defining custom types that implement certain methods, specifically the map, flatMap, and filter methods. When you create a custom monad or a type that follows these conventions, you can use it with for-comprehensions just like built-in types. Here’s a simple example of creating a custom monad in Scala:

case class Box[A](value: A) {
def map[B](f: A => B): Box[B] = Box(f(value))
def flatMap[B](f: A => Box[B]): Box[B] = f(value)
}

// Using the Box monad in a for-comprehension
val boxResult = for {
a <- Box(5)
b <- Box(a * 2)
} yield b
println(boxResult) // Output: Box(10)

F# provides another integrated and powerful mechanism for creating custom “monads”, aka computation expressions. After defining your custom CE builder in F# you can use specific keywords in your custom expression such as let!, do!, and return!. Here's a simplified example of defining a custom CE in F# called my. A bit more on this in this article

type MyBuilder() =
member this.Bind(x, f) = f x // !
member this.Return(x) = x

let my = MyBuilder()

let result = my {
let! x = 5
return x * 2
}

printfn "%d" result // Output: 10

Asynchronous Code (comprehensions) ⚡️

F# computation expressions are used to create workflows for asynchronous code with async(cold/lazy) or task (hot/eager) which is the .NET default (~! C# async/await). More awesome variants in the awesome IcedTask Library.

It’s extra noteworthy here that the whole idea of async/await was born at Microsoft with the inception of the F# language itself! 🏅

let asyncWorkflow = 
async {
let! result1 = async { return 5 }
let! result2 = async { return 10 }
return result1 + result2
}
|> Async.RunSynchronously // Output: 15

In this example, the async CE is used to create an asynchronous workflow. The let! keyword allows the code to wait for the result of each operation in a non-blocking manner.

Scala uses for-comprehensions to handle asynchronous workflows. Here using the Future type via for keyword…

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

val futureWorkflow = for {
result1 <- Future { 5 }
result2 <- Future { 10 }
} yield result1 + result2

futureWorkflow.foreach(println) // Output: 15 (asynchronously)

Scala’s for keyword is used to compose multiple Future operations. The <- symbol extracts values from futures, similar to let! in F#. The yield keyword returns the final result of the combined computations similarly to F# return in this case.

Collection Comprehensions (built in) 📦

Collection Comprehensions via F# computational expressions can be used to generate collections in a natural, declarative manner, for lazy sequences (.NET IEnumerable) seq can be used.. A similar synthax is available for all major collection types ( e.g. also list, array)

let sequence = seq {
for i in 1..5 do
i * i // yield is implicit, not required but can be added
}

// Output: seq [1; 4; 9; 16; 25]

In this example, the seq CE is used to create a sequence by iterating over the range 1..5 and yielding the square of each element.

Scala’s for-comprehension can be used for generating sequences as well.

val sequence = for {
i <- 1 to 5
} yield i * i

// Output: Vector(1, 4, 9, 16, 25)

Scala uses for with the yield keyword to generate a sequence. In this case, the to method generates a range, and yield is used to create the final collection of squared numbers.

Currying / Partial Application 🥘

In Scala currying is available (like in Haskell) but needs to be made explicit due to the nature of signatures in scala: it requires multiple parameter lists to achieve currying.

def add(x: Int)(y: Int): Int = x + y  // Curried function

val addFive = add(5) _ // Partially applying the function

println(addFive(3)) // Output: 8

In F#, functions are curried by default, which makes partial application more natural, like in other ML languages and Haskell as well. Types signatures can be added only when required to add custom constraints to parameters.

let add x y = x + y  // Function is curried by default

let addFive = add 5 // Partial application
printfn "%d" (addFive 3) // Output: 8

Discriminated Unions 👩‍❤️‍👩

In F# Discriminated unions are a first-class citizens as a member of ML languages, while in Scala we can implement a similar functionality using sealed traits and case classes. Similar additions have been followed recently to C# and Java to support abstract records and allow for emulation of discriminated unions via objects inheritance. Both approaches allow to represent a domain quite succintly and expressively.

Here is some F# Pet discriminated union code:

type Pet =
| Dog of string * int // Name and age
| Cat of string * bool // Name and isIndoor

let describePet pet =
match pet with
| Dog (name, age) -> sprintf "This is a dog named %s, age %d." name age
| Cat (name, isIndoor) -> sprintf "This is a cat named %s. Indoor: %b." name isIndoor

let myPet = Dog("Buddy", 5)

printfn "%s" (describePet myPet)

And here is how you would achieve someting similar with Scala traits

sealed trait Pet

case class Dog(name: String, age: Int) extends Pet
case class Cat(name: String, isIndoor: Boolean) extends Pet

def describePet(pet: Pet): String = pet match {
case Dog(name, age) => s"This is a dog named $name, age $age."
case Cat(name, isIndoor) => s"This is a cat named $name. Indoor: $isIndoor."
}

val myPet: Pet = Dog("Buddy", 5)

println(describePet(myPet))

Records (F#) vs Case Classes

type Person = { name: string; age: int, ssn: string } 

let person = { name = "Tim"; age = 28; ssn = "123-45-6789" }

let olderPerson = { person with age = 31 }

Scala doesn’t seem to have a record type concept like in F# or other ML languages. Instead, case classes are reused for the same purpose, All case classes automatically get a copy method mixed in.

case class Person(name: String, age: Int, ssn: String) 

val person = Person("Tim", 28, "123-45-6789")

val oderPerson = person.copy(age => 31)

Pipeline Operator |> vs Chaining . 🪈

F# thanks to currying by default supports natively the |> pipeline operator for chaining function applications via static module functions or any even methods if currying is available

let square x = x * x
let addOne x = x + 1

5
|> square
|> addOne // This becomes addOne(square(5))

Scala insted adopts the object method chaining . approach by default, also called extension methods, fluent extension or dot notation, simiarly to C#, a def. more Object Oriented approach.

case class Point(x: Int, y: Int) {
def add(other: Point): Point = Point(x + other.x, y + other.y)
def scale(factor: Int): Point = Point(x * factor, y * factor)
}

val point = Point(1, 2)
val result = point.add(Point(2, 3)).scale(2) // This becomes Point(6, 10)

This means that in C# and Scala one has to define chaining methods usually (extension methods in .NET) to obtain the dot notation useful to express the “fluent navigation” approach, while in F# chaining (piping) is natively available to all “standard” functions.

Some generic pipe extension seems to be available though also for general usage in Scala, even though it probably works by generically extending any type with extension methods, it might serve similar purposes.

Some example default chaining available for Scala collections:

val result = List(1, 2, 3, 4)
.filter(_ % 2 == 0)
.map(_ * 3)

While in F# |> (“inherited” by Erlang/Elixir) can be used for all collection modules:

[1; 2; 3; 4]
|> List.filter (fun x -> x % 2 = 0)
|> List.map (fun x -> x * 3)

Tail Recursion Support 🧵

Both F# and Scala support tail recursive optimization of code to some extent, by using rec in F# and [<TailCall>] attribute F# compiler can add extra checks to the function to make sure that tail recursion is respected or throw a compiler warning if not. In Scala a @tailrec annotation is available to instruct the compiler for tail recursion optimization if available.

Compilation to JS ☎️

Regarding Javascript compilation F# shines with Fable project, also targeting a variety of other languages like typescript, python and rust.

Scala on the other hand has support for javascript compilation via scala.js

F# allows for great DSL synthax thanks to its native support for list higher order functions via nested list computation expressions, which make native F# code look a lot like pure HTML.

F# vs Scala for HTML-Like DSL 🌏

Feliz library allows F# developers to write code that closely resemble HTML, using F#’s higher-order list functions and list comprehensions and a clean syntax.

In F# we are not required to terminate each new line with a semicolon ; in most declaration synthax if we have a new line (e.g. lists and records). This leads to a clean, visually appealing structure where HTML-like code can be easily represented as F# lists. Example (Feliz in F#):

Html.div [     
Html.h1 "Hello, World!"
Html.p "This is a paragraph."
Html.button [
prop.onClick (fun _ -> printfn "Button clicked!")
prop.text "Click me"
]
]

In F#, using vertical alignment and indentation, you can avoid semicolons entirely, making the structure look similar to standard HTML tags.

In Scala with Scalajs-react building similar components involves a custom slightly more verbose and less standard syntax compared to F#'s HTML-like DSL. Unfortunately the for comprehension cannot be reused for this purpose here , even thought it tecnically provides similar features is not a feature parity with F# collection comprehensions.

val component = ScalaComponent.builder[Unit]("HelloWorld")   
.renderStatic(
<.div(
<.h1("Hello, World!"),
<.p("This is a paragraph."),
<.button(
^.onClick --> Callback(println("Button clicked!")),
"Click me"
)
)
).build

Here, <.div, <.h1, etc., are parts of the Scalajs-react library, and the syntax feels less like HTML and more Scala-specific, with additional symbols to denote attributes like ^..

Summary 🎬

Both F# and Scala are great languages, weather you are on the JVM or .NET will be the biggest choice for weather you can use one or the other.

While F# focuses on a functional first approach and is a proud member of the ML family of languages, Scala follows a more object first approach while also combining many advanced features from a Haskell-like type system but implemented with traits and case classes.

If in doubt, try both and have fun! 🦔💜🪜 you might have noticed many concept can be transposed easilly and they are both beautiful and elegant FP programming languages for modern GC runtimes.

References

--

--

Responses (1)