Comparing Kotlin to F#

jkone27
16 min readJul 25, 2023

learning some Kotlin along the way

lo-fi meme rework

What are they? ๐Ÿช ๐Ÿ˜

Kotlin is a language which runs on the JVM, as well as can be deployed on a variety of platforms and be used for frontend development (and has wasm experimental support) and app development (Android especially) as well, as well as backend development on Springboot or other web server frameworks. Kotlin can also now run natively.

F# is a dotnet language, it can run on .NET, but can be compiled in a variety of other platforms (node.js, python, rust, swift and even PHP) be used for frontend development in js and WASM, and app development as well, as well as backend development on Aspnetcore or other web server frameworks (Suave) and meta-frameworks (like Giraffe, Falco, Saturn). NET8 apps can now also be compiled to run natively via AOT, and some F# can also be compiled to Rust.

src
src

Hello World! ๐Ÿ™Œ

fun main() {
println("Hello, world!")
}

I will try to follow the Kotlin tour / getting started guide, and compare step by step with F#.

Below is hello world in F#.

printfn "Hello, world!"

Immutable and mutable variables in Kotlin โœ

val popcorn = 5    // val immutable
var customers = 10 // var mutable

customers = customers + 1

println("There are ${customers} customers")
// there are 11 customers

// variable explicitly typed and initialized
val e: String = "hello"

In F#

let popcorn = 5 // let is immutable by default
let mutable customers = 10 // mutable adds mutation

// mutation operator `<-` is not the same as "assignment" `=` in f#!
customers <- customers + 1
printfn $"There are {customers} customers"

// variable explicitly typed and initialized
let e: string = "hello"

Lists and list operation in F# ๐ŸŒ, ๐ŸŽ ,๐ŸŠ ,๐Ÿฅ

let square x = x * x
let isOdd x = x % 2 <> 0

let sumOfOddSquares nums =
nums
|> List.filter isOdd
|> List.sumBy square

// mutable list (C# standard List)
let shapes = [ "triangle"; "square"; "circle" ] |> ResizeArray

[<EntryPoint>] // can also have an entry point function (main) like kotlin
let main args =
let numbers = [1; 2; 3; 4; 5] // lists are immutable by default in F#

let sum = sumOfOddSquares numbers

printfn "The sum of the odd squares in %A is %d" numbers sum

shapes.Add("pentagon")

0

List operations In Kotlin

fun square(x: Int) = x * x
fun isOdd(x: Int) = x % 2 != 0

fun sumOfOddSquares(nums: List<Int>): Int =
nums.filter { isOdd(it) }
.sumBy { square(it) }

val shapes = mutableListOf("triangle", "square", "circle")

fun main() {
val numbers = listOf(1, 2, 3, 4, 5) // immutable
val sum = sumOfOddSquares(numbers)

println("The sum of the odd squares in $numbers is $sum")

// add an element to a mutable list
shapes.add("pentagon")
}

Sets and Maps in Kotlin ๐Ÿ—บ

// read-only set
val readOnlyFruit = setOf("apple", "banana", "cherry", "cherry")

// mutable set
val fruit = mutableSetOf("apple", "banana", "cherry", "cherry")

fruit.add("dragonfruit")
println(fruit) // [apple, banana, cherry, dragonfruit]

fruit.remove("dragonfruit")
println(fruit) // [apple, banana, cherry]

// read-only map
val readOnlyJuiceMenu = mapOf("apple" to 100, "kiwi" to 190, "orange" to 100)
println("This map has ${readOnlyJuiceMenu.count()} key-value pairs")

// mutable
val juiceMenu = mutableMapOf("apple" to 100, "kiwi" to 190, "orange" to 100)
juiceMenu.put("coconut", 150)
println(juiceMenu)
// {apple=100, kiwi=190, orange=100, coconut=150}

juiceMenu.remove("orange")
println(juiceMenu)
// {apple=100, kiwi=190, coconut=150}

Sets and Maps In F#

F# immutable types are also Sets and Maps, but to allow mutation F# uses .NET System.Collections.Generic namespace usually, so HashSet<T> and Dictionary<TKey,TValue> (for maps) for example.

open System
open System.Collections.Generic

// read-only set (all is immutable by default)
let readOnlyFruit =
["apple"; "banana"; "cherry"; "cherry"]
|> set

// mutable set is in System.Collections.Generic namespace (~C#)
let fruit = new HashSet<string>(readOnlyFruit)

fruit.Add("dragonfruit")
printfn "%A" fruit // seq ["apple"; "banana"; "cherry"; "dragonfruit"]

fruit.Remove("dragonfruit")
printfn "%A" fruit // seq ["apple"; "banana"; "cherry"]

// read-only map / dictionary
let readOnlyJuiceMenu =
[
"apple", 100
"kiwi", 190
"orange", 100
] |> Map.ofList // can also use IDictionary with |> dict

printfn "This map has %d key-value pairs" (readOnlyJuiceMenu |> Map.count)

// mutable map / dictionary is in System.Collections.Generic (~C#)
let juiceMenu =
readOnlyJuiceMenu |> Dictionary

juiceMenu.["coconut"] <- 150
printfn "%A" juiceMenu
// [("apple", 100); ("kiwi", 190); ("orange", 100); ("coconut", 150)]

juiceMenu.Remove("orange")
printfn "%A" juiceMenu
// [("apple", 100); ("kiwi", 190); ("coconut", 150)]

If else statements in Kotlin ๐Ÿšฆ

val check = true

val d = if (check) {
d = 1
} else {
d = 2
}

println(d)
// 1

val a = 1
val b = 2

println(if (a > b) a else b) // no ? ternary op but if expr

If else statements in F#

let check = true

let d =
if check then
1
else
2

printfn "%d" d
// 1

let a = 1
let b = 2

let result = if a > b then a else b // also no ternary ? op but if expr
printfn $"{result}"

Pattern matching in Kotlin ๐Ÿ’  ๐Ÿ”ฃ ๐Ÿ‘ฝ

val obj = "Hello"

when (obj) {
"1" -> println("One")
"Hello" -> println("Greeting")
else -> println("Unknown")
}
// Greeting


val temp = 18

val description = when {
temp < 0 -> "very cold"
temp < 10 -> "a bit cold"
temp < 20 -> "warm"
else -> "hot"
}
println(description)
// warm

Pattern Matching in F#

As an ML language F# comes bundled with advanced pattern matching capabilities plus, along with traditional pattern matching, F# allows to define โ€œdynamic union/functionsโ€ with active patterns, see more below.

let obj = "Hello"

match obj with
| "1" -> printfn "One"
| "Hello" -> printfn "Greeting"
| _ -> printfn "Unknown"
// Greeting

let temp = 18

let description =
match temp with
| _ when temp < 0 -> "very cold"
| _ when temp < 10 -> "a bit cold"
| _ when temp < 20 -> "warm"
| _ -> "hot"

printfn "%s" description
// warm

// alternative solution using `active patterns` in F#

let (|Cold|Cool|Warm|Hot|) temp =
if temp < 0 then Cold
elif temp < 10 then Cool
elif temp < 20 then Warm
else Hot

let description temp =
match temp with
| Cold -> "very cold"
| Cool -> "a bit cold"
| Warm -> "warm"
| Hot -> "hot"

let temp = 18
let result = description temp
printfn "%s" result
// warm

Looping over elements (iterator) in Kotlin ๐Ÿ‡

for (number in 1..5) { 
print(number)
}
// 12345

val cakes = listOf("carrot", "cheese", "chocolate")

for (cake in cakes) {
println("Yummy, it's a $cake cake!")
}
// Yummy, it's a carrot cake!
// Yummy, it's a cheese cake!
// Yummy, it's a chocolate cake!

Looping over elements (IEnumerable / seq ) in F#

for number in 1 .. 5 do
printf "%d" number
// 12345

let cakes = ["carrot"; "cheese"; "chocolate"]

for cake in cakes do
printfn "Yummy, it's a %s cake!" cake
// Yummy, it's a carrot cake!
// Yummy, it's a cheese cake!
// Yummy, it's a chocolate cake!

F# Extra โ€” List/Seq Comprehensions (like Python but more!) ๐Ÿ‘ฝ

let listComprehension =
[
for x in [1..100] do
let z = "hello" // just to show you can!
if x % 2 = 0 then
yield x + 1 // value is returned whenever we want
]

in F# List, Seq and Array modules all have a great expressivity via their corresponding Computation Expressions (CE). They can be extended or you can define your own for any type you wish (advanced topic).

More on comparing F# to Python here.

Common F# computation expressions in use in code:

seq { ... } // seq (IEnumerable in .NET)
[ .... ] // list
[| ... |] // array

query { ... } // IQueriable in .NET - expr tree for a DB query

task { .... } // computation expression for .NET Task async/await in F# (Hot)

async { ... } // old/native F# async await (Cold)

Here is a quick catch on how those are created, and how to define your own in a previous medium article I wrote.

While and Do in Kotlin ๐Ÿ“ฝ

var cakesEaten = 0
var cakesBaked = 0
while (cakesEaten < 3) {
println("Eat a cake")
cakesEaten++
}
do {
println("Bake a cake")
cakesBaked++
} while (cakesBaked < cakesEaten)
// Eat a cake
// Eat a cake
// Eat a cake
// Bake a cake
// Bake a cake
// Bake a cake

While and Do in F#

let mutable cakesEaten = 0
let mutable cakesBaked = 0

while cakesEaten < 3 do
printfn "Eat a cake"
cakesEaten <- cakesEaten + 1

do while cakesBaked < cakesEaten do
printfn "Bake a cake"
cakesBaked <- cakesBaked + 1

// Eat a cake
// Eat a cake
// Eat a cake
// Bake a cake
// Bake a cake
// Bake a cake

Functions in Kotlin (fun) ๐ŸŽ

fun hello(){
return println("Hello, world!")
}

fun sum(x: Int, y: Int): Int {
return x + y
}

fun printMessageWithPrefix(message: String, prefix: String = "Info") {
println("[$prefix] $message")
}

fun printMessageWithPrefix(message: String, prefix: String = "Info") {
println("[$prefix] $message")
}

fun printMessage(message: String) {
println(message)
// `return Unit` or `return` is optional
}

// single-expression function
fun sum(x: Int, y: Int) = x + y

// lambda or anonymous function assigned to a variable
val upperCaseString = { string: String -> string.uppercase() }

// specifying a function type (~ lambda-calculus notation) x -> y -> z
// function returning a function Int -> Int, given a String
fun toSeconds(time: String): (Int) -> Int = when (time) {
"hour" -> { value -> value * 60 * 60 }
"minute" -> { value -> value * 60 }
"second" -> { value -> value }
else -> { value -> value }
}

// the initial value is zero.
// the operation sums the initial value with every item
// in the list cumulatively.
println(listOf(1, 2, 3).fold(0, { x, item -> x + item })) // 6

// alternatively, in the form of a trailing lambda
println(listOf(1, 2, 3).fold(0) { x, item -> x + item }) // 6

// write test names with whitespaces in backticks
fun `test equal` (x: Int, y: Int) = x == y

Functions in F# (no new keywords! Still let)

As we have seen for Kotlin, as for most regular languages, functions are defined with a different keyword fun in this case.

This is not the case for ML languages (including F#), where everything from functions to constants to variables can be bound to a symbol with an expression, using a single keyword: let.

This fact alone, in my opinion, makes F# a truly functional language.

Both in F# and Kotlin is possible to name functions as symbols with spaces, which makes it extra useful for unit and integration test definitions, without needing external libraries to โ€œtagโ€ them for a nicer display in execution reports.

// both "variables" and "functions" in F# use a single keyword let
// because they are just expression bindings!
// () is `unit`/void like Kotlin has Unit, in F# is always explicit.

let hello () = // unit -> unit
printfn "Hello, world!"

let sum x y =
x + y

// in F# only methods in classes (ref types) support default parameters
// in this case we just provided an optional parameter (Some x or None)
let printMessageWithPrefix message (prefix: string option) =
let evalPrefix = if prefix.IsSome then prefix.Value else ""
printfn "[%s] %s" evalPrefix message

let printMessage message =
printfn "%s" message

let sum x y = (+) // in F# >everything< is a "function" also +!

// fun is used also in F# but only for lambda expressions
let upperCaseString = fun (str: string) -> str.ToUpper()

// specifying a function type (~ lambda-calculus notation) x -> y -> z
// function returning a function int -> int, given a string
let toSeconds (time: string) : int -> int =
match time with
| "hour" -> fun value -> value * 60 * 60
| "minute" -> fun value -> value * 60
| "second" -> fun value -> value
| _ -> fun value -> value

let result1 =
[1; 2; 3]
|> List.fold (fun x item -> x + item) 0

printfn "%d" result1 // 6

let result2 =
[1; 2; 3]
|> List.fold (+) 0 // also, List.sum

printfn "%d" result2 // 6

// we can also do this in F#, even for variables!
let ``test sum`` (x: int) (y: int) =
x = y

Classes (reference types) in Kotlin ๐Ÿ›

class Customer

class Contact(val id: Int, var email: String)

class Contact(val id: Int, var email: String) {
val category: String = ""
}

data class User(val name: String, val id: Int)


fun main() {
val contact = Contact(1, "mary@gmail.com")

println(contact.email)
// mary@gmail.com

contact.email = "jane@gmail.com"

println(contact.email)
// jane@gmail.com

println(user)
// User(name=Alex, id=1)

val user = User("Alex", 1)
val secondUser = User("Alex", 1)
val thirdUser = User("Max", 2)

println("user == secondUser: ${user == secondUser}")
// user == secondUser: true

println("user == thirdUser: ${user == thirdUser}")
// user == thirdUser: false

println(user.copy())
// User(name=Alex, id=1)

println(user.copy("Max"))
// User(name=Max, id=1)

println(user.copy(id = 3))
// User(name=Alex, id=3)
}

Classes and Records in F#

type Customer = class end

// F# Poco Class
// in contrast to Kotlin, by default parameters in constructor
// are private readonly, better default in my opinion...
type Contact(id: int, email: string) =

// expose public properties with `val` keyword
member val Id = id with get
member val Email = email with get, set
member val Category = "" with get

// public `member` object functions/variables
// can be exposed just with `member this.`
// Optional parameters are supported in Ref types! with `param?`
// they are Option<T> by deafult, later on the subject of options...
member this.NotUsedMethod(param1: string, param2? : string option) =
$"hello {param1}" + param2 |> Option.default ""

// record type for User (equivalent to Kotlin data class)
type User = {
Name: string
Id: int
}

let contact = Contact(1, "mary@gmail.com")

printfn "%s" contact.Email
// mary@gmail.com

// in F# we use mutation `<-` operator to mutate mutables,
// very handy to see right away where `impure` mutation happens
contact.Email <- "jane@gmail.com"

printfn "%s" contact.Email
// jane@gmail.com

let user = { Name = "Alex"; Id = 1 }

// in F# `;` or `new line` can be used
// to separate `items` or `properties` when defining types,
// also works with lists and other types
let secondUser = {
Name = "Alex"
Id = 1
}

let thirdUser = { Name = "Max"; Id = 2 }

printfn "user = secondUser: %b" (user = secondUser)
// user = secondUser: true

printfn "user = thirdUser: %b" (user = thirdUser)
// user = thirdUser: false

// in F# we don't a a copy without change by default
// but this can be achieved like this... (not specified fields are copied)
printfn "%A" ( { user with Name = user.Name })
// {Name = "Alex"; Id = 1;}

// creates a copy of user with name: "Max"
printfn "%A" ({ user with Name = "Max" })
// {Name = "Max"; Id = 1;}

// creates a copy of user with id: 3
printfn "%A" ({ user with Id = 3 })
// {Name = "Alex"; Id = 3;}

Extra โ€” F# Discriminated Unions

Kotlin atm of writing does NOT support discriminated unions or DUs, like is the case of C#. In the JVM world, Scala has DUs like F#.

A bit similar in some regards to TypeScript tagged unions, which are not full DUs though.

Some extension libraries in C# or Kotlin try to add support for them.

Thatโ€™s not exactly an authentic DU, but if itโ€™s good enough for you then no complaints.

Below is what an actual union type looks like in functional languages with true Algebraic Data Types (ADTs) like OCaml, Rust, Reason, Elm, F#, Haskell, and Scala.

We will also meet DUs again later, since null in F# is handled with the Option discriminated union, to avoid null reference exceptions most times.

Another useful default F# union is the Result union.

// single case union for a seat, a seat number is not a string!
type Seat = Seat of nr:string

// single case for flight info
type Flight = Flight of nr:string

// a price is a record with sale and purchase info
type Price = {
SalePrice: decimal
PurchasePrice: decimal
}

// single case union
type Insurance = Insurance of supplierCode:str

// same but another type
type CarSupplier = CarSupplier of supplierCode:str

// example of a product union, alternative to inheritance and classes
type Product =
| FlightProduct of flightNumber:Flight * seats: Seat list * price:Price
| BaggageProduct of flightNumber: Flight * price: Price
| FlightInsurance of insurance: Insurance * price: Price
| RentalCar or carSupplier: CarSupplier * price: Price
| Voucher of deductionAmount: decimal

// usage is like shown later for option,
// each "case" is a constructor, and we can also pattern match
// using the same constructor in "revese" in the matching pattern

// create a rental car, that also is a product DU
let car = Product.RentalCar(CarSupplier.CarSupplier("ACME"), {
SalePrice = 1000m,
PurchasePrice = 200m
})

// Function to print the product price, example of pattern matching
// when using the same variable multiple cases can move down
// within a single function expressing them all for a common property
let printSaleProductPrice product =
match product with
| FlightProduct (_, _, price)
| BaggageProduct (_, price)
| FlightInsurance (_, price)
| RentalCar (_, price) ->
printfn "Product price: SalePrice = %f" price.SalePrice
| Voucher (deductionAmount) ->
printfn "Voucher deduction amount: %f" -deductionAmount

In Kotlin or languages with data class aka record types (also with C#), you can achieve similar functionality with very concise record declarations and class inheritance.

// note that `sealed` in Kotlin is closer to `abstract` in C#/.NET
// as `sealed` in C# has a different meaning (concrete and no inheritors)
sealed class Product


// data classes here or record types mimic a closed union by inheriting from
// a common abstract ancestor
data class Seat(val nr: String) : Product()
data class Flight(val nr: String) : Product()

data class Price(val salePrice: Double, val purchasePrice: Double)

data class Insurance(val supplierCode: String) : Product()
data class CarSupplier(val supplierCode: String) : Product()

data class FlightProduct(val flightNumber: Flight,
val seats: List<Seat>, val price: Price) : Product()

data class BaggageProduct(val flightNumber: Flight, val price: Price)
: Product()

data class FlightInsurance(val insurance: Insurance, val price: Price)
: Product()

data class RentalCar(val carSupplier: CarSupplier, val price: Price)
: Product()

data class Voucher(val deductionAmount: Double) : Product()

// Function to print the sale price or voucher discount of the product
fun printSalePriceOrVoucherDiscount(product: Product) {
when (product) {
is FlightProduct, is BaggageProduct, is FlightInsurance, is RentalCar -> {
val salePrice = when (product) {
is FlightProduct -> product.price.salePrice
is BaggageProduct -> product.price.salePrice
is FlightInsurance -> product.price.salePrice
is RentalCar -> product.price.salePrice
else -> 0.0
}
println("Sale price: $salePrice")
}
is Voucher -> {
val voucherDiscount = -product.deductionAmount
println("Voucher discount: $voucherDiscount")
}
}
}

fun main() {
val car = RentalCar(CarSupplier("ACME"), Price(1000.0, 200.0))
val voucherProductPositive = Voucher(50.0)
val voucherProductNegative = Voucher(-30.0)

printSalePriceOrVoucherDiscount(car)
printSalePriceOrVoucherDiscount(voucherProductPositive)
printSalePriceOrVoucherDiscount(voucherProductNegative)
}

Null Safety in Kotlin, similar to C# Nullable (?) ๐Ÿ˜˜ โ˜ ๏ธ

fun describeString(maybeString: String?): String {
if (maybeString != null && maybeString.length > 0) {
return "String of length ${maybeString.length}"
} else {
return "Empty or null string"
}
}

fun lengthString(maybeString: String?): Int? = maybeString?.length

fun main() {
// neverNull has String type
var neverNull: String = "This can't be null"

// throws a compiler error
neverNull = null

// nullable has nullable String type
var nullable: String? = "You can keep a null here"

// this is OK
nullable = null

// by default, null values aren't accepted
var inferredNonNull = "The compiler assumes non-null"

// throws a compiler error
inferredNonNull = null

// notNull doesn't accept null values
fun strLength(notNull: String): Int {
return notNull.length
}

println(strLength(neverNull)) // 18
println(strLength(nullable)) // Throws a compiler error

var nullString: String? = null
println(describeString(nullString))
// Empty or null string

var nullString: String? = null
println(nullString?.uppercase())
// null

var nullString: String? = null
println(nullString?.length ?: 0)
// 0
}

In F# we have Options! (no Nullable ref atm.)

In F# nulls and errors are generally treated with some core library generally available discriminated union definitions are functions, called Option<T> and Result<TOk,TErr>.

module Null =
let toOption n =
if n = null then
None
else
Some(n)

type Valid = | String of string
with static member Parse(str) =
if str |> System.String.IsNullOrWhiteSpace then
failwith "given input string was null or whitespace"
else
Valid.String(str)

// neverNull has String type
let neverNull = "This can't be null"

// in F# this compiles, as null is a possible value returned by .NET runtime
// but we can easilly avoid it wrapping it into an Option
let canBeNullWatchOut: string = null

// nullable has nullable String type (using option type)
let nullableStr: string option =
null |> Null.toOption

match nullableStr with
| Option.None -> "this string is null" // aka .IsNone
| Option.Some(s) -> "this string is not null: {s}" // aka .IsSome

// the compiler cannot infer nullability of string i F#
// but you can wrap it in a custom type assured to be not null
// at runtime
let notNullOrWhiteSpaceString =
"The compiler doesn't know yet, but we can help him"
|> Valid.Parse

match notNullOrWhiteSpaceString with
| Valid.String(s) -> $"this string is not null or empty {s}"
// no other cases because it's a single case union!

// or using option module
let validString =
nullableStr
|> Option.defaultValue "NOW IS NOT NULL"
|> Valid.Parse

Extra โ€” Async code in F# โฉ

F# has been one of the early pioneers and model languages for async/await since back in its first versions, dating back to 2007.

F# language designers genuinely invented async/await back in 2007, one might say.

Since those days it evolved and adapted to the general .NET framework Task<T> type which is a hot async/await compared to the cold original F# async await.

The original cold async computation can still be used and converted to System.Threading.Tasks.Task with ease if one wants to.

This a beautiful article exploring the differences between original async and task CEs here if you are interested.

open System
open System.Threading.Tasks

// Task function to simulate an asynchronous operation
let asyncOperation (input: int) : Task<int> =
task {
printfn "Starting async operation with input %d" input
do! Task.Delay(1000)
return input * 2
}

// Another task function awaiting the previous one with let! operator and returning a result
let combinedAsyncOperations (input: int) : Task<int> =
task {
let! result1 = asyncOperation input
let! result2 = asyncOperation (input + 1)
return result1 + result2
}

[<EntryPoint>]
let main argv =
task {
let! combinedResult = combinedAsyncOperations 5
printfn "Combined result: %d" combinedResult
return 0
} |> Async.AwaitTask

Async code in Kotlin

import kotlinx.coroutines.*

// Coroutine function to simulate an asynchronous operation
suspend fun asyncOperation(input: Int): Int {
println("Starting async operation with input $input")
delay(1000)
return input * 2
}

// Another coroutine function awaiting the previous one
// with async/await and returning a result
suspend fun combinedAsyncOperations(input: Int): Int {
val result1 = async { asyncOperation(input) }
val result2 = async { asyncOperation(input + 1) }
return result1.await() + result2.await()
}

fun main() = runBlocking {
val combinedResult = combinedAsyncOperations(5)
println("Combined result: $combinedResult")
}

If you have suggestions or comparisons that would be nice to include in the article let me know.

If I made any mistakes feel free to point them out and Iโ€˜ll try to amend the article, cheers.

This concludes this intro to Kotlin compared to F#, cheers!

Have a nice week! ๐Ÿ˜บ

Resources ๐Ÿ“š

--

--