I step aside from classic teaching about functional programming to consider F# as a general purpose language for .NET development. We begin here with a classic OOP critique point concerning how data and functions are structured.
One year ago, I finished taking an academic course in functional programming with F#. It was fine and I learned what I needed for later courses and other work, but it almost felt like taking a course in OCaml. It felt like the language we were learning had a very niche academic applications for prototyping new programming languages, proof systems, advanced parsers and so on.
While that in itself makes F# a great tool for CS minded students, I feel like it unintentionally damages the motivation for F# if we don’t also think of it as a potential daily driver for solving software engineering problems.
The F# language is special because it is one of three ways to code within the .NET software framework. The others (at time of writing) are C# with clearly the widest adoption, and Visual Basic which used to be significant for Windows targeted development, but has fallen out of favor.
So this article will be the first of the series where I try to consider F# as a more general-purpose language for .NET projects. The idea is, at the risk of teaching sloppy skills,🔗 to arrive at a better standpoint for motivating functional programming and F# to software development students.
- What situations can arise in .NET C# project work that F# can solve more elegantly?
- How well does F# interoperate with other .NET libraries? Especially ASP.NET Core for web development projects.
- How can functional programming practices and related paradigms help us with software work more generally?
OOP sprawl, how data and functions relate
I expect that most readers of this text are familiar with how to work in the object oriented programming (OOP) paradigm as is common for Java and C#. Fundamentally, each instance of an object contains both the data fields and methods needed to fulfill its role. It is a very versatile and intuitive paradigm with well-known design principles for managing large software projects.
However, there are cases where this paradigm breaks down in terms of code comprehension and performance. A common case that I have encountered previously concerns data collections.
While there are generic collections (List, Set, Map, etc.) to hold collections of objects and get features to work, performant collections tend to have customized data structures or functions at the collection level that exploit patterns in that particular application, such as how many distinct strings a street address collection needs to store.
Such opportunities for good design can be hard to spot and exploit, especially if the structure has been obscured in a sprawl of OOP classes and files. To be clear: You can build that performant collection as an OOP class that can be instantiated, but it is harder to notice that you should.
The shape collection example in F#
As a more blunt walkthrough example, this Molly Rocket YouTube video🔗 shows how a collection of shape data can be structured far better as structs and functions, pulling ownership of the data structure and functions away from class instances. Watch this video first before reading the next part.
With F# and typical functional programming, we tend to naturally think of the relationship between data and functions in ways that promote this kind of understanding anyway.
For instance, here I have a console demo ready with a shape type, an area function, and higher-order list processing in just 24 lines of F# code:
open System
type Shape = // Discriminated union type
| Square of float
| Rectangle of float * float
| Triangle of float * float
| Circle of float
let area = // Shape -> float
function
| Square n -> n * n
| Rectangle (w, h) -> w * h
| Triangle (b, h) -> b * h * 0.5
| Circle r -> r * r * Math.PI
let shapes = [ // Shape list
Square 10.0;
Rectangle (10.0, 5.0);
Triangle (10.0, 5.0);
Circle 5.0
]
printfn "Area list: %A" (List.map area shapes)
printfn "Area sum: %f" (List.fold (fun acc s -> acc + area s) 0.0 shapes)
If I want to add a function for corners, I can do so with ease:
let corners = // Shape -> int
function
| Square _ -> 4
| Rectangle (_, _) -> 4
| Triangle (_, _) -> 3
| Circle _ -> 0
Advice for programming more generally
F# in this way feels similar to languages such as Rust, Go, C and parts of C++ that we consider for high performance; and to database related programming.
These languages generally call for the programmer to think in terms of “structs” and meaningful data structure design as a discipline in itself, and functions or methods that work at some level of these structures, ranging from single element methods to functions accessing entire data structures.
There is less segregation, the private access modifier is banished, and both ourselves and the compiler may find various ways to optimize our code more easily.
In this way, the point where we consider loose coupling and explicitly defined interfaces is shifted from classes to the package level. Go follows this principle through its simplified capital-for-public access modifier. F# achieves a similar paradigm with .fsi signature files🔗.
Again, such principles can also be applied efficiently within object oriented and imperative programming, but we have to allow ourselves more fancy behavior within collection classes than just acting as an aggregation of single entities.