Over the past years, the benefits of functional programming and type systems have become ever more apparent to software developers. While the former allows you to express intricate patterns concisely, the latter afford you great assurance of reducing or eliminating runtime errors in your program.
At Datarisk, we fully embrace these two in our technology stack. We develop systems using the .NET environment, with F# as the language of choice. Over the past months, we have collected great dividends by picking this strong foundation for the tech unit. This article clarifies the reasoning behind these choices, as well as elaborates on practical aspects regarding adopting this stack on our day-to-day work.
When the limestone of imperative programming has worn away, the granite of functional programming will be revealed underneath.
Functional programming, in contrast, relies on Lambda Calculus as a foundation. Just as the name describes, functions become the centerpiece of a program. Functional languages eschew explicit control flow and loops in favor of function composition and recursion. It is known that these allow you to build programs in a more declarative, more concise style. It also allows the programmers to write code in a data-oriented manner.
Our language of choice, F#, is a multi-paradigm language, but the usage of functional idioms is greatly encouraged. This manifests itself in the language syntax, as well as libraries and core operators provided by the language.
Furthermore, since F# is built on the .NET ecosystem, we can easily share libraries with the C# world. This is a very strong point in favor of F#; not only it is a safe choice due to the mature and widely used ecosystem, it is a first-class functional language with most of the powerful features you expect from the paradigm.
Functional programming also greatly alleviates the complexity of patterns in our code. While object-oriented programming relies on an extensive catalog of design patterns, functional languages allow you to capture repeating patterns very easily as functions and combinators. The advantage of this approach is that instead on relying on error-prone, ad-hoc patterns, we transfer the burden of abstracting patterns towards the language, guaranteeing, for example, that applying them correctly is enforced by the compiler.
As programs become larger in size and complexity, clarity, correctness, and reliability become ever graver concerns. On large codebases, small changes have the potential to break the entire system. Even more perversely, such breakages can be insidious and reveal themselves only when the system is up and running.
The Hindley-Milner type system of F# greatly reduces these concerns. Not only does it provide strong type checking, it also allows you to rely on type inference when you are unsure about a value’s type. While the type system of F# allows us to be confident in our system’s reliability and correctness, the cognitive burden on developers is greatly reduced. We can be sure that small changes to the code won’t break the system.
A strong type system also invites you to think carefully about the structures involved in your systems. Instead of distinguishing between options with basic strings or untagged booleans, functional programmers are encouraged to define their own types to deal with the situation at hand. This goes hand-in-hand with making illegal states unrepresentable. Whenever illegal states may come up in your program, the type-system manifests that as an error at compile-time. No more relying on carefully watching the terminal as you run the program to catch runtime errors!
While this seems like a huge amount of work, it gives us huge benefits. The main thing is that we can to iterate fast on a solid foundation without breaking existing features. This point cannot be understated; the Hindley-Milner type system is perhaps the feature that provides the most leverage to the Tech Unit. We are able to use the tooling in our favor, and as a result, we can deliver software that is robust, reusable, maintainable, and readable.
An ongoing pattern in our codebase is the use of type providers to interface with diverse data sources in a type-safe manner. Through this mechanism, F# allows us to automatically get a type that describes the data in an SQL table, for instance. This goes a long way to solving SQL impedance mismatch in our codebase. As a result, we have cleaner, less error-prone code.
Since we are a data science company, type providers also provide great business value by providing correctness guarantees when dealing with diverse data sources. We can think of it as extending the benefits of the F# type system to adjacent domains, such as external APIs and data sources.
Mutable variables and references can be a great source of confusion in large programs. When you pass a variable to a function, for instance, it may not be immediately obvious whether the function changed that variable. Furthermore, as applications scale, shared mutable state can become an enormous source of problems when dealing with multi-threaded or asynchronous tasks. You often have to coordinate an intricate dance with locks to deal with these variables correctly. This not only poses a burden on developers, the sheer amount of situations where failure can happen results in, at best, a subpar experience for customers, and at worse significant human loss.
By default, F# bindings are immutable, meaning that you know that by default the variables you pass to a function won’t be tampered with. You also have to find other ways to track state. While this may seem like a handicap at first, it forces developers to think in a more explicit, declarative style. Managing state in such a manner also simplifies immensely the complexity of code, since it allows one to reason on a more case-by-case basis.
While we are convinced of the strength of statically-typed functional programming, selling it to managers and stakeholders can be tough. At the start of 2021, we started building the first F# team at Datarisk. Selling it as cutting-edge technology, our first F# project was done in 3 weeks. We have therefore achieved an acceptable time to market and greatly reduced our expected maintenance burden.
In April 2021 we were contracted for a major project with a big customer. We felt safe in our decision to go full F# because of the results and the expertise built with the MVP. In part because of this decision we achieved success with the project (the client renewed the contract) despite a team that was not used to strongly typed functional programming.
This success story greatly raised our confidence in our technology. It also highlights the maturity of F# tooling and libraries for production usage. Senior Engineer
We’ve elaborated on the key technical choices on the Datarisk Technology Unit. We come to these choices not through the enthusiasm of trying new things, but by having a key objective in mind: to maintain truth as the basis of our choices. Through experience and solid evaluation, we’ve come to choose statically typed functional programming as our foundation.
This is not a choice that is taken lightly. Choosing this foundation requires us to go against the grain of software engineering common practice. We see this resistance as an opportunity to enhance our craft and to deliver higher-quality results.
Our experiences ensure that adopting functional programming in production is not only possible, but it also affords several technology and business benefits.
At Datarisk, we have a dedicated development team keen on custom data processing solutions and technologies. We are ready to help you create a robust solution. isit our site to see how we solve real world problems.
Thanks to Alesson Bernardo and Eduardo Bellani for providing feedback for this article.
Being able to move fast while assuring safety of the codebase is one of the dream capabilities of a development team. To achieve this goal, we...
O futebol é o esporte mais popular do mundo. Ele une pessoas de idades, sexos, grupos sociais e culturas diferentes. Para ter ideia da...