Basic principles of functional programming
Functional programming (FP) is a specific style of programming that focuses on using functions as blocks of code.
Unlike imperative programming, where the developer dictates the exact order of actions to the program, the functional approach works differently. The programmer doesn’t dictate the order of execution, but rather describes the rules of interaction and interrelationships between components. The program, in turn, determines the optimal way to achieve the result based on these rules.
Let us list the basic principles of FP.
1. Functions are full-fledged objects; they can be passed as arguments, returned from each other, and assigned to variables.
2. Immutability. All code elements are considered immutable: their values do not change once created. Absolutely all new elements are created based on existing ones.
3. No side effects. Functions should not modify the external environment, such as global variables, or have “hidden” effects. Their output is determined solely by their input data.
4. Recursion: a way of breaking a problem down into smaller subproblems that can be solved using the same sets of functions.
5. Some functions can use other functions as their arguments and then return them.
Overall, FP offers powerful concepts for reliable, readable, and maintainable code.
Functional programming languages
Functional programming languages (FPLs) are languages built on the principles of FP. Instead of the traditional approach of step-by-step state transformation, FPLs focus on using functions to transform code.
Examples of FYAP:
- Haskell is a functional language with a rich type system.
- Erlang is a language designed for creating fault-tolerant and concurrent systems.
- Scala is a hybrid language that combines functional and object-oriented approaches.
- Clojure is a Lisp dialect that runs on the JVM.
- F# is a functional language developed for the .NET Framework.
FLPs open up new possibilities for software development, allowing the creation of more reliable, efficient, and maintainable systems.
Purity of functions
In functional programming, one of the key concepts is function purity. A function is considered pure if it:
- has no side effects: does not change the state of the external environment, does not interact with the file system, does not send requests to the server, does not perform other actions that affect anything other than calculating the result.
- Deterministic – given the same input data, it always returns the same result.
Advantages of pure functions:
- Predictability – their behavior is always clear and easy to predict due to the absence of any side effects.
- Testability – it is easier to create unit tests when the behavior does not depend on the external environment.
- Parallelism – They are ideal for parallel processing because they do not compete for access to shared resources.
- Refactoring – changes in one pure function do not affect other parts of the code, making refactoring easier.
- composition – can be easily combined.
Examples of “unclean” operations:
- changing global variables.
- writing to a file.
- reading from the network.
- console output.
- interaction with DBMS.
How to make a function pure:
- avoid using global variables.
- use immutable data.
- do not interact with the external environment.
- perform only calculations without causing side effects.
Pure operations are a powerful tool that can make code more understandable, predictable, and easy to test.
Data immutability
In FP, all components are considered immutable: no value changes post factum.
Existing information is not changed; new information is created based on it. For example, instead of changing an array element, a new array is created containing the changed element.
Let’s list the advantages of immutability.
- Predictability. Immutability makes code more predictable, as no components can change, including in other parts of the program.
- Safety. Immutability prevents errors associated with concurrent access to mutable information, which is especially important in multithreaded applications.
- Testability. Immutability simplifies testing because functions don’t depend on the state of external data.
- Parallelism. Immutability makes code more suitable for parallel processing, since operations do not conflict with each other.
- Immutability encourages the creation of pure functions that cannot modify external data.
Examples of using immutability:
- Using immutable.js in JavaScript – The immutable.js library provides immutable structures: arrays and objects.
- Working with higher-order functions – in FP, higher-order functions (e.g., map, filter, reduce) often work with immutable datasets.
Data immutability requires a rethinking of the entire programming approach. Instead of changing datasets, new ones must be created, which may require some adaptation but ultimately leads to improved code quality.
Recursion and recursive data structures
Recursion is a powerful tool in FP, allowing a complex problem to be broken down into simpler subproblems solved by the same function. Recursive structures are defined using themselves. Examples of these include lists and trees.
The principle of recursion in FP is that a function calls itself within its definition, but with modified input data.
Let’s give an example of calculating a factorial.
“`
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n – 1)
“`
Recursive structures include:
- lists – can be defined as an empty list or as an element appended to another list.
- trees – can be defined as an empty tree or as a node containing data and references to other trees (descendants).
Advantages of recursion:
- Simplicity – recursive solutions are often simpler and more understandable than iterative ones.
- Elegance – Recursive solutions solve problems involving hierarchical data beautifully.
- FP compatibility – recursion fits naturally into the functional programming paradigm.
Examples of using recursion:
- Tree traversal – traversing a tree to find a node or calculate a sum of values.
- Sorting – Recursive sorting algorithms such as merge sort.
- Processing lists, trees, and other recursive structures.
Recursion requires careful consideration to avoid infinite loops. It’s essential to ensure that the recursive function has a base case—a condition that terminates the recursion.
Functional approaches to data processing
FP offers elegant, efficient approaches to dataset processing that depart from the traditional imperative style. Here are some of its features.
1. Data transformation:
- FP uses higher-order functions such as map, filter, and reduce, which apply operations to elements without changing their original state.
- Data is considered immutable, which prevents side effects and makes code more predictable.
- Functions are easily composed, creating chains of transformations that are easy to read and understand.
Let’s give an example in JavaScript.
const numbers = [1, 2, 3, 4, 5];
const squaredNumbers = numbers.map(number => number * number); // [1, 4, 9, 16, 25]
const evenNumbers = numbers.filter(number => number % 2 === 0); // [2, 4]
const sum = numbers.reduce((acc, number) => acc + number, 0); // 15
2. Processing data streams:
- Datasets are processed as streams, where operations are applied sequentially to each element, without the need to store all the data in memory.
- Calculations are performed only as needed, which allows for optimized processing of large datasets.
- The streaming approach is well suited for parallel data processing.
Another example, this time in Python.
from itertools import islice
def stream_process(data_stream):
for item in data_stream:
processed_item = process_item(item)
yield processed_item
def process_item(item):
# Data processing
return item * 2
data_stream = range(100000)
processed_data = slice(stream_process(data_stream), 10) # Process the first 10 elements
3. Processing complex structures:
- Recursive functions are suitable for efficient processing of complex structures: trees, lists.
- FP languages such as Haskell provide capabilities for more convenient and flexible extraction of data from complex structures.
The main advantages of functional approaches:
- Cleanliness and simplicity – the code becomes more understandable and readable.
- Testability – it’s easier to test individual functions without worrying about side effects.
- Parallelism – FP is well suited for developing parallel and distributed systems.
Functional approaches to data processing enable the creation of efficient, flexible, and easily maintainable code, making them a reliable tool for solving a wide variety of practical problems.
Benefits of Functional Programming
FP has a number of advantages over imperative programming. Let’s list the main ones.
1. Simpler code. FP makes maximum use of pure functions without side effects, making code more predictable, readable, and understandable. The absence of mutable components simplifies debugging and refactoring.
2. Easy testing. Pure functions are easy to test because their behavior doesn’t depend on the external environment.
3. Parallel use. FP is often used to develop parallel and distributed systems, as its immutability eliminates any issues with synchronous access by different users.
4. Efficiency. FP allows you to optimize code to improve performance, especially in resource-constrained environments.
5. Composition. Functions can be easily composed with each other, which promotes code reuse.
6. Ease of debugging code due to its purity, immutability, and absence of side effects.
7. Improved security. Immutability prevents accidental changes and errors related to concurrent access to data.
8. Increased flexibility. FP allows you to create more flexible, adaptable code that is easy to extend and modify.
Limitations of functional programming
FP has limitations that should be kept in mind when choosing a development style.
1. Difficulty for beginners: FP requires a rethinking of the usual approach to programming, which can be difficult for beginner developers.
2. Limitations in some tasks. Some tasks that require direct interaction with the external environment, such as working with a graphical interface, are sometimes more difficult to implement using functional programming than using an imperative approach.
3. Optimization complexity. Optimizing code in FP can be more difficult than in imperative programming, especially in cases where precise performance tuning is required.
4. FP is not universally applicable to all tasks. In some cases, especially those involving low-level data processing, an imperative approach is more efficient.
It’s important to remember that functional programming isn’t a magic wand that solves all problems. When choosing a development approach, it’s important to consider the context of the task, as well as the advantages and disadvantages of each approach.
Conclusion
Despite some limitations, functional programming is becoming increasingly popular in modern programming languages. While it requires specific knowledge and skills, it allows for the development of maintainable systems.
While functional programming may not be a solution for every practical problem, it is a valuable tool in a developer’s arsenal. Learning its fundamentals can open new possibilities for solving complex software development problems.






