BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles How Functional Programming Can Help You Write Efficient, Elegant Web Applications

How Functional Programming Can Help You Write Efficient, Elegant Web Applications

Key Takeaways

  • Maintaining an internal mutable state is challenging. We change the context for subsequent interactions each time we interact with a program.
  • Object Oriented Programming (OOP) and Functional Programming (FP) try to provide solutions for handling the software’s maintainability and complexity. OOP encapsulates away the complexity, while FP focuses on data and its transformations.
  • Functional programming concepts improve predictability, promote code reuse, and manage side effects—the FP’s emphasis on immutability and composability results in more robust and maintainable systems.
  • FP improves web applications by treating them as data transformation pipelines. It encourages using pure functions to transform input data into output data, leading to transparent data flow, easier testing, and more predictable behaviour.
  • Kotlin seamlessly blends object-oriented and functional programming, allowing for the gradual adoption of functional concepts in existing codebases. This results in more expressive, concise, and safer code that can improve maintainability and reduce errors.

Many things can make software more challenging to understand and, consequently, to maintain. One of the most difficult and problematic causes is managing internal mutable states. This issue is particularly troublesome because it directly impacts how the software behaves and evolves. It may seem obvious, but we change the context for subsequent interactions each time we interact with a program. This means the application must maintain an internal state for each user and update it continuously. When the internal state is poorly managed, the software behaves unexpectedly, leading to bugs and fixing, which introduces unnecessary complexity. Hence, it makes the software harder to debug and extend.

Functional programming might seem intimidating and overly academic at first, but once you get the hang of it, it's a game-changer and a lot of fun on top of it! To better understand how functional programming can help us build more maintainable software, let's start from the beginning and understand why a program becomes harder and harder to maintain as it becomes more significant.

We will start by saying that effectively managing the software’s mutable state is crucial for building reliable and maintainable software. I am not referring to some theoretical style but to code that is easy to change and where bugs can be easily identified.

Throughout the history of programming, each paradigm has had different approaches to handling the internal state of software.

In procedural programming, the internal state is mutable and globally accessible, making it easy to read and change whenever needed. This approach is similar to what happens internally in the CPU at a lower level, allowing for very fast compile and execute code. When the program is small, having everything mutable and global is not an issue because a developer can keep everything in mind and write the correct code.

However, fixing bugs and adding new features becomes challenging when projects become large. As software applications become more complex, maintaining the code becomes increasingly difficult, with many possible flows and tangled logic resembling "spaghetti code".

This problem can be overcome by breaking big problems into smaller ones, thus making them easier to handle. Modular programming lets you focus on one piece at a time, temporarily setting aside the rest.

Both functional and Object-Oriented Programming aim to make our code easier to manage and fix, even as projects grow. There are different approaches to writing high-quality code.

Since the 1990s, object-oriented design has been the most widely adopted solution. In its purest form, everything is an object. Objects can have their changeable state but keep it hidden from the rest of the application. They communicate only by sending messages to each other. This way, you give each object a job and trust it to do its part.

Java, for example, follows this approach, even though it isn’t purely object-oriented. The application's state is divided among objects, each responsible for a specific task. This way, even if the application grows, it remains easy to modify. Additionally, Java performs almost as well as, sometimes even better than, procedural languages, making it a valuable and successful compromise.

Functional programming adopts an entirely different approach. It focuses on data and its transformations using pure functions, which do not depend on any global context. In functional programming, states are unalterable, like indivisible atoms. Functions transform these atoms, combining simple actions into more complex operations. These functions are "pure", meaning they don't depend on other parts of the system.

In other words, the outcome of a pure function is determined solely by its inputs, making it predictable and easy to debug. Moreover, they are easy to reuse in different parts of the code since they are self-contained and easy to understand.

Another advantage of pure functions is that they are easy to test for the above reasons. There is no need to mock objects because every function depends only on its inputs, and there is no need to set up and verify internal states at the end of the tests because they don't have any.

Finally, using immutable data and pure functions dramatically simplifies the parallelisation of tasks across multiple CPUs and machines on the network. For this reason, many of the so-called "big data" solutions have adopted functional architectures.

However, there are no silver bullets in computer programming. Both the functional approach and the object-oriented approach have tradeoffs.

If your application has a very complex mutable state that is primarily local, it may take much work to model in a functional design. For example, complicated desktop interfaces, video games, and applications that work like simulations are usually better suited for object-oriented design.

On the other hand, functional programming particularly excels in scenarios where programs operate on well-defined input and output stream transformations, such as web development. In this paradigm, every task becomes a function that receives some data and returns some other data. This approach mirrors the operation of web services: they receive a request and send back a response.

Further, we will explore how functional programming can benefit web development (the code examples are provided in Kotlin). This approach can simplify the process while producing more accessible, easy-to-understand, and maintainable code. And let's not forget about the fun part—it makes coding more enjoyable, too!

So, what's so fun about accepting that functional programming can simplify some applications? Writing code functionally is like solving a logical puzzle. We have to break down the problem into a set of functions, each performing a specific transformation, and they must work together as a coherent whole.

We need to define the functions and the shape of the data on which they operate, ensuring they can be combined. Compared to a relaxed approach with global states and singletons, these are much stricter constraints, often making the application more complex. On the other hand, once it compiles, a functional application is likely to have fewer bugs because many potential sources of errors are eliminated by design.

For example, when writing a web application, we design all the necessary functions, each with a specific job, from pulling up user profiles to creating HTML pages. The true magic unfolds when these functions come together logically, resulting in an elegant and practical solution. When it works, it's like piecing together a puzzle perfectly, crafting a picture that feels right.

Functional programming is all about this. It’s more than just a way of writing code; it's a way of thinking and tackling problems, recognising and utilising the connections between pieces of data.

Traditional programming thinking involves procedures, functions, and methods as pieces of code that do something. Adopting a slightly different and more mathematical point of view in functional programming is helpful: "Pure functions are entities that transform an input into an output".

Let's start with some simple, pure functions in Kotlin to illustrate the concept.

fun celsiusToFahrenheit(celsius: Double): Double =
    celsius * 9.0 / 5.0 + 32.0

The celsiusToFahrenheit function takes a temperature in Celsius degrees and converts it to Fahrenheit. It doesn't rely on any external state or modify any external variables, so we can call it pure. It does a calculation, but this is a technical detail. It could also work using a table of predefined values. What matters is that it transforms the temperature from one unit to another.

In Kotlin, we can represent each function's type with an arrow. Any function can be described as an arrow from type A to type B, where A and B can also be the same type.

A -> B

In this case, it takes a Double as an argument and returns another Double with the converted temperature. We can describe its type as (Double) -> Double. This succinct arrow syntax for function types is one of the small details that make working functionally in Kotlin enjoyable. Compare this with Java, where the equivalent would be Function<Double, Double>, along with other types for different numbers of parameters, like BiFunction, Supplier, and Consumer.

Note also that almost infinite functions have the same type and signature. For example, this function also has the same signature:

fun square(x: Double): Double = x * x

Now, imagine that on top of our function from A to B, we have another function that goes from B to C, returning a new type, C.

The first example of composition is: "Can we combine these two functions?" "Yes! We can!" We need to approach handling functions as if they were pieces of data.

fun compose(fn1: (A)->B, fn2: (B)->C): (A)->C = ???

To implement this kind of function composition, we need to apply the second function to the result of the first one. Here’s how we can define it in Kotlin:

fun <A,B,C> compose(fn1: (A)->B, fn2: (B)->C): (A)->C = 
  {a -> fn2(fn1(a))}

This may seem odd at first glance because when we talk, we describe it as doing fn1 and then fn2, but in the implementation, the second function appears before the first one. This is because computers calculate functions from the inside out using the parenthesis syntax.

Let’s look at a practical example to demonstrate how we can use this to create a new function by combining two existing ones without the need to write any additional specific code:

val isInLeapYear = compose(LocalDate::parse, LocalDate::isLeapYear)
isInLeapYear("2024-02-01") //true

In this example, compose combines LocalDate::parse, which converts a String to a LocalDate, with LocalDate::isLeapYear, which checks if the given LocalDate is in a leap year. The resulting function, isInLeapYear, directly takes a String (the date) as input and returns a Boolean indicating whether it's a leap year.

Another way to achieve the same effect is to use let, one of the Kotlin scope functions.

Using scope functions, the previous example could be written as follows:

"2024-02-01"
    .let(LocalDate::parse)
    .let(LocalDate::isLeapYear) //true

The advantage of using let in Kotlin lies in its readability and its promotion of immutability. By chaining transformations within let blocks, the progression from type A to B to C is made explicit, enhancing the clarity and simplicity of your code.

Note that you can also use lambda blocks with scope functions, but using function references reveals the intent even more explicitly.

Function composition is a cornerstone of functional programming, marking a significant shift in perspective: it treats functions not merely as units of code that execute tasks but as first-class entities. These entities can be passed as arguments, returned from other functions, and combined in various ways. Such an approach is the bread and butter of the functional programmer's work, enriching the toolkit for solving complex problems elegantly.

Functional Dependency Injection

Imagine we have a function that fetches a user from a database. This function requires two things (remember there are no hidden dependencies or singleton in functional programming): a connection to the database and a user ID.

fun fetchUser(db: DbConnection, userId: UserId): User = // some code

Inside, it queries the database, retrieves the user, and returns this user to whoever called the function.

The function is perfect for our needs. However, we have a problem: the database connection is only available on the outer layer of our application, but we need to call it in the middle of our domain code, where we have the user ID but not the infrastructure details like the database connection.

It's possible, but awkward and impractical, to pass the database connection through several layers of code to reach the point where we need to use this function. In other words, the two inputs for this function—the user ID and the database connection—are available in distant parts of our code.

A clean solution to this common problem is to partially apply the function. We can first provide it with only one parameter (e.g., the database connection), and what we get back is a new function that only needs the remaining parameter (the user ID) to operate and return the desired outcome. This is like splitting the function into two stages: initially, we input the first parameter and then receive a new function that requires the second parameter to complete the process and deliver the response.

This concept might sound complicated at first, but a code example should make it a bit clearer:

fun userFetcherBuilder(db: DbConnection): (UserId)->User =
{ id: UserId -> fetchUser(db, id) }

The userFetcherBuilder function takes the DB connection but does not return a user. Its result is another function that will give the user a user ID. If this is unclear, look again at the code and the signature.

This technique can also be generalised to all functions that take two parameters. In other words, we have a function that takes A and B to return C. We want a function that takes A and returns another function that takes B to return C.

fun <A,B,C> partialApply(a: A, operation: (A, B)->C): (B)->C = 
    { b: B -> operation(a, b) }

Now, we can rewrite our function in a more concise form:

fun userFetcherBuilder(db: DbConnection): (UserId)->User =
    partialApply(db, ::fetchUser)

How can we use this partial application to solve our initial problem? Let's start with the original function that retrieves the user from the database. We then apply the database connection once we have access to our infrastructure (partial application) before entering the domain logic.

This produces a new function that only requires the user ID to fetch the user. The domain logic doesn't need to concern itself with database connection details; it can simply call this new function, which has the db connection embedded, to retrieve the user.

Instead of passing the connections through all the layers, we must pass our partially applied function. Passing the function is a much more flexible solution. It allows us to use different functions for various scenarios, such as a mock function for tests or another function fetching the user from a remote HTTP call instead of a database. The receiver of the function only cares about the implementation details if the function returns a user who is given a user ID.

In other words, by employing a partial application, we effectively decouple the domain logic from infrastructure concerns, streamlining the process of fetching data without cluttering the business logic with unnecessary details. This approach simplifies our code and enhances its modularity and maintainability.

fun fetchUserFromDb(db: DbConnection, userId: UserId): User? =
  //some db-related code here

   
fun initApplication() {
   // Assuming we have a database connection
   val dbConnection = DatabaseConnection(/* initialization */)

   // Partially apply the database connection to fetchUser function
   val fetchUser: (UserId) -> User? =
            partialApply(dbConnection, fetchUser)

  // Pass the function where it's needed
  settingUpDomain(fetchUser)
}

fun somewhereInDomain() {
    val userId = readUserId()
val user = fetchUser(userId)
doSomethingWithUser(user)
}

In this example, partialApply is a higher-order function that takes other functions as input and output. It takes the first parameter of the original function (dbConnection) ahead of time and returns a new function (fetchUser). This new function requires only the remaining parameter (UserId) to execute.

By doing this, we encapsulate the database connection details outside of our domain logic, allowing the domain logic to focus purely on business rules without concerning itself with infrastructure details like database connections. This makes our code cleaner, more modular, and easier to maintain.

Invokable Classes

This approach is elegant and practical, but thinking about functions abstractly can be challenging at times. Kotlin offers a more straightforward and more idiomatic way to achieve this by using objects.

We can create a class that inherits from a function type. It may initially sound strange, but it is quite a handy pattern. This technique allows us to use instances of the class wherever a standalone function is expected, providing a more intuitive and object-oriented way to handle functional programming concepts. Here's how it can be implemented:

data class UserFetcher(val db: DbConnection) : (UserId) -> User? {
    override fun invoke(id: UserId): User = fetchUser(db, id)
}

In this example, UserFetcher is a data class (but also a function!) that takes a DbConnection as a constructor parameter and inherits from the function type (UserId) -> User.

When we inherit from a function type, we have to define the class's invoke method, which has the same signature as the function we are inheriting. In our case, this function gets a user ID as input and returns a user.

UserFetcher can be used just like a regular function, taking a UserId and returning a User. This approach simplifies the code and offers a smoother learning curve for beginners in functional programming, blending familiar object-oriented concepts with functional paradigms.

As much as I love the simplicity of functional programming, object-oriented patterns can sometimes be very convenient. Kotlin leverages both paradigms.

The code above reveals some of the main differences between functional and object-oriented programming, especially regarding dependency management. In object-oriented programming, accomplishing this task might involve creating an interface with multiple methods and having a class implement it. This could introduce a complex web of dependencies for a relatively straightforward requirement.

Conversely, functional programming leans on lambda or anonymous functions for similar tasks. For instance, if we need to fetch a user by their ID, we simply pass the relevant function as an argument to another function responsible for generating the user's web page without bothering to define the complete interface of the database access layer.

This strategy minimises the need for interfaces, abstract classes, or other complex structures, streamlining code and eliminating unnecessary coupling.

A Web Server Example

Let’s demonstrate this with a practical example in Kotlin, showcasing how we can apply all the concepts we have seen so far to create a web application that displays some information about our "users". It has to get the user's ID from the HTTP path and then retrieve the details from the database. We will use Ktor, a widely used framework for web applications in Kotlin.

We start by implementing a page to show a page with the user's details. The URI will be /user/{userId} and will show the user's details with that userId, fetched from a database.

The "Hello World" in Ktor will look like this:

fun main() {
   embeddedServer(Netty, port = 8080) {
       routing {
           get("/") { // <- this block is associated with the / path
               call.respond(HtmlContent(HttpStatusCode.OK){
                body("Hello World")
                })
       }
   }.start(wait = true)
}

This is nice and clear, but how can we functionally implement our API?

If we look at how a web server works with a functional eye, we see a function that transforms an HTTP request (typically from the user browser) to an HTTP response (with the page HTML, for example).

The Transformation from Request to Response

How can we apply this intuition in our code? Let's think about what a function of type Request -> Response needs to do to produce a page with user details.

Considering the data flow, the journey begins with an HTTP request containing the user ID. This ID may be embedded in various parts of the request —a query parameter, a segment of the URL path, or another element—so the first step is to extract it.

private fun getId(parameters: Parameters): Outcome<Int> =
   parameters["id"]?.toIntOrNull()

Once we have the user ID, we can use something like the UserFetcher invokable class we met earlier to retrieve the corresponding user entity.

This is how we can use UserFetcher and a database connection to obtain the fetchUser function that we need:

val dbConnection = DbConnection(/* initialization */)
val fetchUser: (UserId)->User? = UserFetcher(dbConnection) 

The fetchUser function here is not pure: it will return a different user depending on the database data. However, the most crucial part is that we treat it as pure. This means the rest of our code remains pure, and we limit the impure areas to this function alone.

Other techniques (discussed in the book) can limit and mark impurity even more precisely. For example, functional programming patterns like monads or algebraic data types can help manage side effects better. However, as a first step, this approach already represents a significant improvement over a more relaxed approach to purity.

By isolating impurities, we make our codebase cleaner, more predictable, and easier to test. This first step is a giant leap towards writing more robust and maintainable code.

At this point, we have the user data. The next step is to convert this user entity into a format suitable for HTTP responses. In this example, we want to generate a minimal HTML representation of the user data.

fun userHtml(user: User): HtmlContent =
   HtmlContent(HttpStatusCode.OK) {
       body("Welcome, ${user.name}")
   }

We also need to generate an HTML page to show an error in case the user is not present in our database

fun userNotFound(): HtmlContent =
   HtmlContent(HttpStatusCode.NotFound) {
   body { "User not found!" }
}

Finally, we can create a function that chains all the above functions together and produces HtmlContent that Ktor needs to display a page:

fun userPage(request: ApplicationRequest): HtmlContent =
   getUserId(request)
       ?.let(::fetchUser)
       ?.let(::userHtml)
       ?: userNotFound()

Finally, we can call our function for the route for user details:

get("/user/{id}") {
   call.respond(userPage(call.parameters))
}

And that's it—we implemented our first functional API on the web server! Easy peasy. Is it not a joy working this way, one function and one type at a time?

Of course, this is not the end of the journey. We can still improve this code by handling the database connection more effectively, returning finely tuned errors, adding auditing, and so on.

However, as a first dip into the functional world, this is exceptionally instructive and gives you a taste of what it means to switch from thinking in terms of collaborating objects to thinking in terms of transformations.

Conclusions

Let's do a quick summary of what we did and how we broke up the request handling into four functions:

  1. Extract User ID from HTTP Request: The first step involves parsing the HTTP request to extract the user ID. Depending on the request structure, this might include working with URL paths, query parameters, or request bodies.
  2. Retrieve User Data: With the user ID in hand, we use a function that takes this ID and returns a domain representation of the user. This is where our earlier discussions on function composition and partial application come into play. We can design this function to be quickly composed with others for flexibility and reusability.
  3. Convert User Data to HTTP Response Format: After obtaining the user entity, we transform it into a format suitable for an HTTP response. Depending on the application's requirements, this could be HTML, JSON, or any other format.
  4. Generate HTTP Response: Finally, we encapsulate the formatted user data into an HTTP response, setting appropriate status codes, headers, and body content.

This small example shows why functional programming works exceptionally well in web server applications due to their nature of handling well-defined input and output transformations. This mirrors the operation of web services, where requests are received, processed, and responses are sent back.

What makes this approach particularly appealing is its modularity. Each step in the process—from extracting the user ID to generating the HTTP response—is a discrete, independent task. This modularity simplifies each task and enhances our code's clarity and maintainability. By breaking down the problem into smaller, manageable pieces, we transform a potentially complex process into a series of straightforward tasks.

Each task results in one or more clearly defined functions and specific types that can be tested in isolation and reused in other parts of the codebase without bringing along dependencies from the original context. This makes the code more maintainable and robust, as individual components can be developed, tested, and refined independently.

Moreover, this approach allows for flexibility and experimentation. While the general framework remains consistent, the specific methods of extracting the user ID, converting user data into a response-friendly format, and encapsulating the data in an HTTP response can vary. This flexibility encourages innovation and adaptation, ensuring our solution remains robust across different scenarios and requirements.

We embrace functional programming's modularity to create a more flexible and maintainable codebase. This makes the development process more enjoyable and leads to a more reliable and adaptable application capable of evolving alongside changing requirements and technologies.

About the Author

Rate this Article

Adoption
Style

BT