Comparing F# to Java ☕️

jkone27
13 min readDec 19, 2022

OO, streams and reactive computations along the way

how dare you, kid?

F# and Java are clearly quite different languages and belong to separate language families, but a comparison could still be fun. I have been working with Java lately, so why not compare C# and Java instead? I think the reason is that these languages are already quite similar or similar enough, and you will find many guides online.

let ``fsharp is not so bad afterall`` () = 
"it's actually AMAZING!"

Why compare F# to Java then?

Getting familiar with F# while comparing it to J will give you a different perspective on programming and a wider knowledge of programming languages in general.

In addition I believe F# is awesome and unlike C# or Java, it might have some very unique and attractive features that you haven’t heard of before, for example, the |> pipe operator or automatic generalised type inference.

The secrets of good coffee? ☕️

coffee can have a round flavor, even in a flat white

Java is a statically typed OO language with garbage collection running on the JVM runtime. Its syntax inherits mostly from C++ with some differences.

C++ used to be called C with classes, and Java was the first “C with classes” language variant with a runtime and garbage collection, making it possible to “write once and run everywhere” without being too worried about the underlying OS differences.

new balance are the best shoes for developers, according to research

C# used to be the .NET Java implementation. Due to legal reasons this had to be changed and eventually it diverged from the original Java specs, becoming a language of its own. .NET can run everywhere (mac, linux, win) and is completely open source.

Nowadays C# shares more features with Kotlin than with Java in some regards. We could sum this up by saying that C# is mostly a child of Java, though a very progressive and rebellious one.

C-Like Languages and ML languages

An “Oh-CAMEL” developer in the wild, looking for .NET support

As a note, both Java, C# and for what matters C++ (and clearly C) pertain to the category of C-like languages, as code is more or less “similar” from one to another for the essential syntactic features of the language.

F# is a statically typed language with Functional and OOP characteristics, it runs on .NET runtime with JIT compilation but can run both as compiled (a bit like Java and C#) or as interpreted language (scripting language) like python.

Unlike Java, C# or C++ (C-like languages) F# comes from a totally different family of languages called ML-languages, which have strong functional characteristics and automatic generalised type inference.

Milner won a Turing Award for his work on a type inference “engine” within the standard ML language back in 1991. 🎁 🎄

let x = 5 // int

let addEleven y = // int -> int
y + 11

let someComplexFunction someFunction argument = // ('a -> int) -> 'a -> int
someFunction(argument) + 3

Getting Started 🚗

Java springboot initializer cli (using UI is similar…)

spring init --dependencies=webflux,lombok my-project

F# dotnet new with a webapi project

dotnet new webapi --lang F# 

In addition and to make beginnings easy, F# also comes as an interactive language so you can even run it as scripts from the CLI in the REPL, in which case you don’t need anything more than just your .fsx source file.

dotnet fsi myScript.fsx

Project Dependencies 📦

In regards of dependency management Java ecosystem uses different tools, one of which is maven and the mvn cli (another one is Gradle). Here is an example of how a dependency would look like in a pom.xml file in java

Maven pom.xmlfile (Java)

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>

And below is how it would look in a .fsproj file, where dotnet internally uses nuget commands for dependency management and resolution; C# dependencies in a .csproj file would look almost exactly the same.

<ItemGroup>
<PackageReference Include="Xunit" Version="2.4.1" />
</ItemGroup>

This instead is how it would look on a F# script .fsx file, which behaves a bit in a similar fashion to python scripts, except you don’t need to pip install your dependency before, because the library is automagically installed at editing time.

#r "nuget:Xunit"

open Xunit

Referencing packages 🥝

nothing unexpected here, all looks alike

Java

import some.namespace.from.pkg.x;

F#

open some.namespace.from.assembly.x

Hello world 🚀

Java

public class Main {
public static void main(String[] args) {
System.out.println("Hello World");
}
}

F#

Much shorter, now C# also has top-level statements and global using, so you could achieve a similar succinctness in C# (with some magic). But note this has always been the case for F#, nothing new or fancy.

printfn "Hello world"

Static method 🏛

And here is how to define and use a static method in Java

public class StringUtils {

public static String magic(string inputString){

return inputString.replace("magic","wow");
}
}

//usage in some other class/method..
StringUtils.magic("this is magic!"); // prints `this is wow!`

In F#, a static class corresponds 1:1 to a module, and all of its methods will be public and static, unless you provide a module interface (advanced). F# also has classes for encapsulation in addition to modules, but in the case of an helper function, a module is just right.

module StringUtils

let magic inputString =
inputString.Replace("magic", "wow")

//usage in some other class/method...
StringUtils.magic "this is magic!" // wrapping (...) is not required here

Defining a DTO Class (~ POJO/Bean) 📎

Java (with Lombok)

@Builder
@Data
public class Person {
private int age;
private String name;
private String surname;
}ja

Or as an alternative again in Java, to have it immutable with copy mutation..

@Value
public class Person {
@With int age;
@With String name;
@With String surname;
}

Yet, for the case without Lombok or “basic” Java, here we have a regular POJO: for a bean is a slightly different class, but you get the idea..

public class Person {

private final int age;
private final String name;
private final String surname;

public int getAge(){ return age; }
public String getName(){ return name; }
public String getSurname(){ return surname; }

public Person(int age, String name, String surname) {
this.age = age;
this.name = name;
this.surname = surname;
}
}

ps: here using the recent Java Record feature, Sergiy Yevtushenko, thanks for your comments:

public record Person (int age, String name, String surname) {}

Here in F# as an immutable Record.

[<CLIMutable>] // can be useful for .NET interop, not required
type Person = {
Age: Int
Name: String
Surname: String
}

In F# records come as actually Value types with With copy capabilities embedded in them, so that’s the default! They are immutable so they can be used both for DTOs but also for domain modeling.

Both C# and Java are late Record adopters following the ML-languages / immutability FP trend.

Note that ML languages always had records (even before having classes), for example they are present even in the relatively old standard ML.

OCaml later introduced OO programming in ML, thus adding classes, but Records are still the main data construct for all ML languages.

Thus unlike C#/Java in F# Records are the default way to model data, not a “good” practice, this is why F# is often and rightly called a functional-first language.

Below another example in F#, using a regular class type with setters, though most of times the record version is preferred. Indeed you can use classes and they will be quite succint and readeable too!

type Person(age,name,surname) = // class 
member val Age = age with get
member val Name = name with get
member val Surname = surname with set

Creating an Object 🐼

Java

Person john = new Person(31,"john","kennedy");

Person john = Person.builder() // with lombok @Builder attribute
.age(31)
.name("john")
.surname("kennedy")
.build();

F#

let john = { 
Age = 31
Name = "john"
Surname = "kennedy"
} // record

let mary = new Person(31, "mary", "kennedy") // when using a class type

Working with Streams 💦

Java (Stream)

var x = List.of(1,2,3)
.stream()
.filter(...)
.map(...);

in F#, the seq module is an alias for the .NET IEnumerable interface. This means that standard LINQ can be adopted as well. We also have a List and an Array module with similar functions.

let x = 
[1;2;3]
|> Seq.filter (...)
|> Seq.map (...)

In F# the pipe operator |> makes working with data elegant and beautiful! plus it’s also functional and can be applied to virtually anything, e.g.
The beauty of this approach, is that code “flows” from top to bottom, and from left to right, quite easilly and without occupying much brain space.

Let’s brew a Beer in Java and F# to visualise this! 🍺

the lean beer manifesto signatories, from wikipedia

Now imagine to have BeerUtils class with 3 static functions to make beer, Mash, Brew and finally, Bottle. I know probably a builder pattern could be used here, but I really just want to show only function application. This is how you would make a Beer in Java.

Beer beer = Bottle(Brew(Mash("malt")));

With this F# version of the same code, as you can see the pipe operator just places the next argument on the left side instead of the right of the function, making “piping” possible and natural. I think “brewing” in this way is much easier and simpler to reason about.

let beer =
"malt"
|> Mash
|> Brew
|> Bottle

In “simple” math we would write

beer(x) = bottle(brew(mash(x)))

but the same can also be seen, just applying the argument to the left, as

let beer x = x |> mash |> brew |> bottle

Dependency Injection 📩

Spring has quite smooth registration annotations that makes the process of registering a dependency quite nice and neat. One divergent note is that in spring “interfaces” are not really used that much, so registration happens direcly on the concrete class and in unit tests concrete classes are mocked by mockito framework.

@Component
@RequiredArgsConstructor
public class FulfillmentService {

private final OrderClient orderClient;

private final ProductClient productClient;

in .NET on the other hand it’s common sense to register dependencies as interfaces to reduce coupling and also because usually mocking frameworks like Moq don’t allow you to mock concrete classes, so that makes the dependency inversion principle apply slightly nicer in my view.

Dependency Inversion (S.O.L.I.D.)

This how we would register a dependency in .NET (both in C# or F#) in this case for a web application builder (but would work also for a normal application builder).

let builder = WebApplication.CreateBuilder(args)

builder.Services.AddTransient<IMyDependency, MyDependency>() |> ignore

let app = builder.Build()

app.Run()

And this is how you would consume that dependency via constructor injection, or via DI resolution: service “locator” pattern kind of flavor, not shown here..

type SomeService(myDependency: IMyDependency) = 
// ... service body using myDependency (constructor injection)

In F# functional dependency injection can also be used, using partial application and higher order functions. In C# this would require tons of Func<Func<…>> code, not sure about Java, but I am quite confident that it would look ugly if even possible. Best to keep this approach to F# code.

All F#/ML functions can be partially applied: one argument at a time can be passed instead of all required arguments, making injection a natural skill. A function requiring any dependency can be partially applied to any number of arguments, before applying additional arguments required for the final invocation, for example:

let dispatchOrder orderClient productClient orderNumber = ...

let app = dispatchOrder orderClient productClient

// final invocation
app "ORDER-001"

let test = dispatchOrder orderClientMock productClientMock

// test invocation
test "ORDER-001"

Service (or Component) 🚚

Java

Here we see an example with Springboot DI and Lombok annotations, and the reactive concurrent model of Spring webflux. Sadly async/await model for concurrency is not yet supported in Java, if not with some extension libraries like EA async, which though is not mantained anymore. The reactive model has both benefits and drawbacks for concurrency modeling in my view, it abstracts away the concurrency at “scheduler” level, but can also make code harder to read and compose: why? there is a pletora of non super easy abstractions for observable composition and ingestion, so the API of the reactive model is quite wide an not easy to learn.

@Component
@RequiredArgsConstructor
public class FulfillmentService {

private final OrderClient orderClient;

private final ProductClient productClient;

private bool isFinalizedOrder(OrderDto order) {

return order.Status == OrderStatus.Finalized;
}

public Flux<OrderDto> getFinalizedOrders() {

return orderClient
.getOrders()
.filter(isFinalizedOrder);
}

public Flux<ProductDto> getProductsForOrder(String orderNumber) {

return orderClient
.getOrder(orderNumber)
.flatMap(o -> o.productIds)
.map(id -> productClient.getProduct(id));
}
}

.NET languages support reactive programming via RX.NET package, but this approach is not the most common in the .net web programming world, though maybe could feel easier for Java devs, or if they wanted to migrate a codebase “almost as-is”, even though async/await does seem easier for everyone I think.

Anyways as a nice side note F# natively supports Observables from event and other sources (even without RX library). Hereafter we see F# Reactive style, hipster hat on… I must admit async/await synthax is a bit terser than rx after trying both worlds.

type FulfillmentService(orderClient, productClient) =

let isFinalizedOrder order =
order.Status = OrderStatus.Finalized

member this.GetFinalizedOrdersObservable(orderNumber) =
orderClient.getAllOrdersAsync
|> Observable.FromAsync
|> Observable.flatmapSeq (fun o -> o)
|> Observable.filter isFinalizedOrder

member this.GetProductsFromOrderObservable(orderNumber) =
(orderClient.getOrderAsync orderNumber)
|> Observable.FromAsync
|> Observable.flatmapSeq(fun o -> o.ProductIds)
|> Observable.flatmapTask productClient.getProductAsync

F# and .NET in general can make use of async/await as well using Task builder CE… in F# a regular .NET async (Task) is expressed using the task computation expression and an await is expressed using the bang operator ! within the expression (within the curly braces). It’s worth noting that F# already had async computatons way long before C# and many other languages, since version 1.0 actually in 2007.

Curly braces in F# are used only for CEs , as opposed to indentation that is adopted for variable scoping (like python and ruby)

type FulfillmentService(orderClient, productClient) =

let isFinalizedOrder order =
order.Status = OrderStatus.Finalized

member this.GetFinalizedOrdersAsync() =
task {

let! orders = orderClient.GetOrdersAsync()

return orders |> Seq.filter isFinalizedOrder
}

member this.GetProductsFromOrder(orderNumber) =
task {

let! order = orderClient.GetOrderAsync(orderNumber)

let! products =
order.ProductIds
|> Seq.map productClient.GetProductAsync
|> Task.WhenAll

return products
}

F# supports async await as well as reactive pipelines, so we can chose whatever we fancy the most, wheras java doesn’t have yet async/await support. In general async/await is a bit simpler to understand and to use with pleasure.

Inheritance VS Union types 🐲

Both Java and C# (as well as Kotlin) do not* support union types - except maybe recent version of Java, see later for a comment on Java sealed types - but mostly plain inheritance, so I will start with some Java inheritance example

//Animal.java
public class Animal { // abstract animal interface
void eat();
void sleep();
String getName();
}

//Dog.java
@RequiredArgsConstructor
public class Dog implements Animal {

@Override
public void eat() {...}

@Override
public void sleep() {...}

@Getter
private final String name;
}

//Cat.java
@RequiredArgsConstructor
public class Cat implements Animal {

@Override
public void eat() {...}

@Override
public void sleep() {...}

@Getter
private final String name;
}

Now imagine having a realistic situation baring more complexity, having 10–100 animal classes or maybe more, and potentially more levels of inheritance. Ok, multiple inheritance should be avoided to favour object composition according to “good design principles”, but not all of us are “good” developers unfortunately. Unless strictly prevented by some external tool, an exponential complexity explosion is likely to happen.

Here is how a more complex scenario could look like in UML.

UML class hierarchy diagram, source

OO Developers need to resort to external tools such as UML to visualize the whole type hierarchy. This is to be able to understand a complex domain and make things “easier” while changing or refactoring classes, or looking at changing requirements or for simple documentation purposes.

F# Union Types to the rescue! With union (or sum) types and product types (record or tuples..) we can create a beautiful, compact, self-documenting type hierarchy in very little lines of code!

Easier documentation and decreased maintenance!

module Animals 

type Animal =
| Dog of name:string
| Cat of name:string

let eat animal =
match animal with
| Cat -> ...
| Dog -> ...

let sleep animal =
match animal with
| Cat -> ...
| Dog -> ...

We can encapsulate and separate data from functions in the same domain using modules.

PS: recently a similar concept of Sealed Classes have been added to Java, which also models in a similar way the idea of union ADT, even though from what I can tell they are not much in use yet, thanks Sergiy Yevtushenko for spotting this! Seems atm is not yet possible to have compiler checks in Java on required cases for matching though, but possibly this will be introduced in future versions of Java.

In future versions of Java, the client code will be able to use a switch statement instead of if-else (JEP 375).

By using type test patterns, the compiler will be able to check that every permitted subclass is covered. Thus, there will be no more need for a default clause/case.

Testing

F# unlike many other languages lets you name things the way you like, how so? Using double back-ticks, test names can actually be decent again!

open XUnit // similar to JUnit...

[<Fact>]
let ``GIVEN i have dotnet-sdk WHEN i try F# THEN i might give it a go!`` () =
Assert.True()

There would be much more to cover on testing, but this is not the main topic here.

Sum Up

This concludes my rant while comparing Java to F#. If you want to see more detailed comparisons or I missed some hot spots that you were interested into, let me know, write in comments below! And have a great day and Xmas time!

Links

--

--