iTranslated by AI
From Procedural to Functional: A Language-by-Language Guide
Introduction
In this article, I will clarify the differences between procedural and functional programming and demonstrate both approaches for solving the same problem across multiple languages.
Basic Differences between Procedural and Functional
Procedural programming and functional programming take fundamentally different approaches to code writing and program design. Understanding the basic differences between these two styles is important for increasing programming flexibility and writing more effective code.
Procedural Programming
Procedural programming views a program as a sequence of instructions. In this approach, you create a series of procedures (functions or subroutines) to manipulate data structures and execute these procedures to change the state of the program. The characteristic of procedural programming lies in its focus on how the program is executed (algorithms and steps). C and Java (excluding object-oriented aspects) are typical examples of this paradigm.
Functional Programming
Functional programming treats computation as the evaluation of mathematical functions and focuses on avoiding side effects. In this paradigm, the use of immutability (data not being changed after creation) and pure functions (functions that always return the same output for the same input and have no side effects) is encouraged. Functional programming represents a program through data flow and transformation, focusing on "what" to do. This paradigm applies to languages like Haskell and Erlang, or when utilizing functional characteristics in multi-purpose languages like JavaScript and Python.
Purpose and Overview of the Article
In this article, we explore the differences between procedural and functional programming through concrete examples and show how each approach affects program design. We will compare different approaches to solving the same problem in multiple languages and discuss their respective advantages and use cases.
Example 1
- Calculate the average of numbers in a list
Java
Procedural Java Code
In the procedural approach, a loop is used to calculate the sum of the numbers in the list, which is then divided by the size of the list to find the average.
import java.util.Arrays;
import java.util.List;
public class AverageCalculatorProcedural {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
double sum = 0;
for (int number : numbers) {
sum += number;
}
double average = sum / numbers.size();
System.out.println("Average: " + average); //Average: 3.0
}
}
Functional Java Code
In the functional approach, we use streams to calculate the sum and average of the numbers. This makes the code more declarative and reduces side effects.
import java.util.Arrays;
import java.util.List;
public class AverageCalculatorFunctional {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
double average = numbers.stream()
.mapToInt(Integer::intValue)
.average()
.orElse(Double.NaN);
System.out.println("Average: " + average); //Average: 3.0
}
}
-
numbers.stream():
- Generates a stream from the
numberslist. A stream is a Java API that supports operations on sequential elements, designed to achieve a functional programming style.
-
.mapToInt(Integer::intValue):
- Maps each element in the stream (in this case,
Integerobjects) to anintvalue.Integer::intValueis a method reference that calls theintValue()method on eachIntegerobject to retrieve its primitiveintvalue. This converts the stream ofIntegerobjects into a stream ofintprimitives.
-
.average():
- Calculates the average of the stream. This operation returns an
OptionalDouble.OptionalDoubleis used when a result may or may not exist. In this case, the average is calculated if the stream is not empty.
-
.orElse(Double.NaN):
- Retrieves the value if the
OptionalDoublereturned by theaverage()method has a value. If the value does not exist (for example, if the list is empty), it returnsDouble.NaN("Not a Number"). This provides an appropriate fallback value when the average cannot be calculated.
JavaScript
Procedural JavaScript Code
const numbers = [1, 2, 3, 4, 5];
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
sum += numbers[i];
}
const average = sum / numbers.length;
console.log("Average:", average); //Average: 3
Functional JavaScript Code
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((acc, current) => acc + current, 0);
const average = sum / numbers.length;
console.log("Average:", average); //Average: 3
-
reducemethod: Calls thereducemethod on thenumbersarray.reduceexecutes a specified reducer function for each element of the array and generates a single result value (in this case, the total sum). The reducer function takes the accumulated value (acc) and the current value of the array (current) as arguments and returns the next accumulated value. -
Initial value: The second argument of
reducespecifies the initial value of the accumulator as0. This causes the calculation of the sum to start from0. -
Calculation of average: The average is calculated by dividing the total sum
sumby the length of thenumbersarray.
TypeScript
Procedural TypeScript Code
const numbers: number[] = [1, 2, 3, 4, 5];
let sum: number = 0;
for (let i = 0; i < numbers.length; i++) {
sum += numbers[i];
}
const average: number = sum / numbers.length;
console.log("Average:", average);//Average: 3
Functional TypeScript Code
const numbers: number[] = [1, 2, 3, 4, 5];
const sum: number = numbers.reduce((acc: number, current: number) => acc + current,0);
const average: number = sum / numbers.length;
console.log("Average:", average);//Average: 3
Python
Procedural Python Code
numbers = [1, 2, 3, 4, 5]
sum = 0
for number in numbers:
sum += number
average = sum / len(numbers)
print("Average:", average)#Average: 3.0
Functional Python Code
numbers = [1, 2, 3, 4, 5]
total = sum(numbers)
average = total / len(numbers)
print("Average:", average)#Average: 3.0
Rust
Procedural Rust Code
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let mut sum = 0;
for number in &numbers {
sum += number;
}
let average = sum as f64 / numbers.len() as f64;
println!("Average: {}", average);//Average: 3
}
Functional Rust Code
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let sum: i32 = numbers.iter().sum();
let average = sum as f64 / numbers.len() as f64;
println!("Average: {}", average);//Average: 3
}
It creates an iterator with the iter() method and calculates the sum of the numbers with the sum() method. It then calculates the average by dividing by the size of the list.
Alternative Solution
Type inference can also be used.
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let sum = numbers.iter().sum::<i32>();
let average = sum as f64 / numbers.len() as f64;
println!("Average: {}", average); // Average: 3
}
Example 2
- Doubling the elements of an array
Python
Procedural Python Code
numbers = [1, 2, 3, 4, 5]
doubled_numbers = []
for number in numbers:
doubled_numbers.append(number * 2)
print(doubled_numbers) #[2, 4, 6, 8, 10]
Functional Python Code
numbers = [1, 2, 3, 4, 5]
doubled_numbers = map(lambda x: x * 2, numbers)
print(list(doubled_numbers))#[2, 4, 6, 8, 10]
-
Use of
mapfunction and lambda expression:
Themapfunction takes two arguments. The first argument is a function, and the second argument is an iterable (in this case, thenumberslist).
The lambda expressionlambda x: x * 2defines an anonymous function where each element of the list is received as argumentx, and the value of that element multiplied by two is returned.
Themapfunction applies the lambda function to each element of thenumberslist and returns a new iterator consisting of the resulting values. -
Converting the iterator to a list using the
listfunction:
The result of themapfunction is an iterator, which is converted to a list by passing it to thelistfunction. This allows us to obtain the contents ofdoubled_numbersas a list and output it.
Java
Procedural Java Code
import java.util.ArrayList;
import java.util.List;
public class DoubleTheArrayElementsProcedural {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(4);
numbers.add(5);
List<Integer> doubledNumbers = new ArrayList<>();
for (int number : numbers) {
doubledNumbers.add(number * 2);
}
System.out.println(doubledNumbers);//[2, 4, 6, 8, 10]
}
}
Functional Java Code
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class DoubleTheArrayElementsFunctional {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> doubledNumbers = numbers.stream()
.map(number -> number * 2)
.collect(Collectors.toList());
System.out.println(doubledNumbers);//[2, 4, 6, 8, 10]
}
}
-
numbers.stream(): Generates a stream from thenumberslist. -
.map(number -> number * 2): Applies the lambda expressionnumber -> number * 2to each element of the stream, doubling each element. -
.collect(Collectors.toList()): Collects the elements of the transformed stream into a new list.
TypeScript
Procedural TypeScript Code
function doubleTheArrayElementsProcedural(numbers: number[]): number[] {
const doubledNumbers: number[] = [];
for (const number of numbers) {
doubledNumbers.push(number * 2);
}
return doubledNumbers;
}
const numbers = [1, 2, 3, 4, 5];
const doubledNumbers = doubleTheArrayElementsProcedural(numbers);
console.log(doubledNumbers);//[ 2, 4, 6, 8, 10 ]
Functional TypeScript Code
function doubleTheArrayElementsFunctional(numbers: number[]): number[] {
return numbers.map(number => number * 2);
}
const numbers = [1, 2, 3, 4, 5];
const doubledNumbers = doubleTheArrayElementsFunctional(numbers);
console.log(doubledNumbers);//[ 2, 4, 6, 8, 10 ]
-
return numbers.map(number => number * 2);: Uses themapmethod to apply the lambda expressionnumber => number * 2to each element of the array, doubling each element. Themapmethod executes the specified function on each element of the array and stores the results in a new array.
Example 3
- Extracting a specific ID from specific classes.
Java
Java Class
public class Person {
private int id;
private String name;
public Person(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
}
Procedural Java Code
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person(1, "Alice"));
people.add(new Person(2, "Bob"));
people.add(new Person(3, "Charlie"));
int targetId = 2;
Person targetPerson = null;
for (Person person : people) {
if (person.getId() == targetId) {
targetPerson = person;
break;
}
}
if (targetPerson != null) {
System.out.println("Found person with ID " + targetId + ": " + targetPerson.getName());//Found person with ID 2: Bob
} else {
System.out.println("No person found with ID " + targetId);
}
}
}
Functional Java Code
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class Main {
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person(1, "Alice"));
people.add(new Person(2, "Bob"));
people.add(new Person(3, "Charlie"));
int targetId = 2;
Optional<Person> targetPerson = people.stream()
.filter(person -> person.getId() == targetId)
.findFirst();
targetPerson.ifPresentOrElse(
person -> System.out.println("Found person with ID " + targetId + ": " + person.getName()),
() -> System.out.println("No person found with ID " + targetId)
);//Found person with ID 2: Bob
}
}
In this code, we use the stream() method to create a stream from the people list and select only the elements where the ID matches targetId using the filter() method. The findFirst() method generates an Optional<Person> object that returns the first matching element. Finally, we use the ifPresentOrElse() method to output the person's name if found, and an appropriate message if not found.
Python
Procedural Python Code
class Person:
def __init__(self, id, name):
self.id = id
self.name = name
people = [
Person(1, "Alice"),
Person(2, "Bob"),
Person(3, "Charlie")
]
target_id = 2
target_person = None
for person in people:
if person.id == target_id:
target_person = person
break
if target_person:
print(f"Found person with ID {target_id}: {target_person.name}")#Found person with ID 2: Bob
else:
print(f"No person found with ID {target_id}")
Functional Python Code
class Person:
def __init__(self, id, name):
self.id = id
self.name = name
people = [
Person(1, "Alice"),
Person(2, "Bob"),
Person(3, "Charlie")
]
target_id = 2
target_person = next((person for person in people if person.id == target_id), None)
if target_person:
print(f"Found person with ID {target_id}: {target_person.name}")
else:
print(f"No person found with ID {target_id}")
target_person = next((person for person in people if person.id == target_id), None): Here, we use a generator expression (person for person in people if person.id == target_id) to retrieve the first Person object from the people list where the id matches target_id. The next() function retrieves the next element from the generator, returning None if no matching element is found.
TypeScript
Procedural TypeScript Code
class Person {
constructor(public id: number, public name: string) {}
}
const people: Person[] = [
new Person(1, "Alice"),
new Person(2, "Bob"),
new Person(3, "Charlie")
];
const targetId = 2;
let targetPerson: Person | undefined;
for (const person of people) {
if (person.id === targetId) {
targetPerson = person;
break;
}
}
if (targetPerson) {
console.log(`Found person with ID ${targetId}: ${targetPerson.name}`);
} else {
console.log(`No person found with ID ${targetId}`);
}
Functional TypeScript Code
class Person {
constructor(public id: number, public name: string) {}
}
const people: Person[] = [
new Person(1, "Alice"),
new Person(2, "Bob"),
new Person(3, "Charlie")
];
const targetId = 2;
const targetPerson = people.find(person => person.id === targetId);
if (targetPerson) {
console.log(`Found person with ID ${targetId}: ${targetPerson.name}`);
} else {
console.log(`No person found with ID ${targetId}`);
}
We use the find method to search for the first Person object in the array whose ID matches targetId. If found, the person's name is output; otherwise, an appropriate message is output.
Example 4
- Updating values based on dictionary keys
Java
Procedural Java Code
In the procedural approach, a loop is used to calculate the sum of the numbers in the list, and then the average is calculated by dividing by the size of the list.
import java.util.HashMap;
import java.util.Map;
public class MapUpdateProcedural {
public static void main(String[] args) {
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 90);
scores.put("Bob", 80);
scores.put("Charlie", 85);
// Check the key and update the value if it matches the condition
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
if ("Bob".equals(entry.getKey())) {
scores.put(entry.getKey(), entry.getValue() + 5); // Increase Bob's score by 5 points
}
}
System.out.println("Updated scores: " + scores);
}
}
Functional Java Code
The "functional" approach in Java is achieved by updating the contents of the Map using the Stream API, but it is not possible to process the Map directly by streaming it. Instead, it is common to create a new Map with the updated entries.
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
public class MapUpdateFunctional {
public static void main(String[] args) {
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 90);
scores.put("Bob", 80);
scores.put("Charlie", 85);
// Create a new Map with updated values for keys that match the condition
Map<String, Integer> updatedScores = scores.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> "Bob".equals(entry.getKey()) ? entry.getValue() + 5 : entry.getValue()));
System.out.println("Updated scores: " + updatedScores);
}
}
-
scores.entrySet().stream(): Converts the entry set of thescoresmap (key-value pairs) into a stream. -
.collect(Collectors.toMap(...)): Performs operations on each entry of the stream and collects the results into a new Map. -
Map.Entry::getKey: A method reference for obtaining the key of an entry (in this case, the person's name). -
entry -> "Bob".equals(entry.getKey()) ? entry.getValue() + 5 : entry.getValue(): A lambda expression for updating the value of an entry (in this case, the score). If the key matches "Bob", 5 is added to the value (score). Otherwise, the value is used as is.
TypeScript
Procedural TypeScript Code
let scores: Map<string, number> = new Map([
["Alice", 90],
["Bob", 80],
["Charlie", 85]
]);
// Check the key and update the value if it matches the condition
scores.forEach((value, key) => {
if (key === "Bob") {
scores.set(key, value + 5);
}
});
console.log("Updated scores:", scores);
Functional TypeScript Code
In TypeScript, there is no function to directly stream a Map, but a functional approach can be achieved by converting the Map entries into an array using Array.from and processing that array.
let scores: Map<string, number> = new Map([
["Alice", 90],
["Bob", 80],
["Charlie", 85]
]);
// Create a new Map with updated values for keys that match the condition
let updatedScores: Map<string, number> = new Map(
Array.from(scores.entries()).map(([key, value]) => {
return [key, key === "Bob" ? value + 5 : value];
})
);
console.log("Updated scores:", updatedScores);
-
Array.from(scores.entries()): Converts the entries of thescoresmap (key-value pairs) into an array. -
.map(([key, value]) => { ... }): Applies an arrow function to each element (entry) of the converted array. This function receives the key and value of the entry through destructuring and calculates the new value. -
key === "Bob" ? value + 5 : value: Uses a ternary operator to add 5 to the value (score) if the key matches "Bob", and uses the value as is otherwise. -
new Map(...): Converts the array of updated entries back into a new Map. This new Map is assigned to the variableupdatedScores.
Python
Procedural Python Code
scores = {
"Alice": 90,
"Bob": 80,
"Charlie": 85
}
# Check the key and update the value if it matches the condition
if "Bob" in scores:
scores["Bob"] += 5
print("Updated scores:", scores)
Functional Python Code
scores = {
"Alice": 90,
"Bob": 80,
"Charlie": 85
}
# Create a new dictionary with updated values for keys that match the condition
updated_scores = {key: value + 5 if key == "Bob" else value for key, value in scores.items()}
print("Updated scores:", updated_scores)
updated_scores = {key: value + 5 if key == "Bob" else value for key, value in scores.items()}: A new dictionary updated_scores is created using dictionary comprehension. This comprehension loops through each key and value in the original dictionary scores, adding 5 to the value if the key matches "Bob", and using the value as is otherwise. A new dictionary is then created from the resulting key-value pairs.
Example 5
- Extracting elements starting with a specific character from a list of strings
Java
Procedural Java Code
import java.util.ArrayList;
import java.util.List;
public class StringFilterProcedural {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Charlie", "David");
List<String> filteredNames = new ArrayList<>();
for (String name : names) {
if (name.startsWith("C")) {
filteredNames.add(name);
}
}
System.out.println("Names starting with 'C': " + filteredNames);
}
}
Functional Java Code
import java.util.List;
import java.util.stream.Collectors;
public class StringFilterFunctional {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Charlie", "David");
List<String> filteredNames = names.stream()
.filter(name -> name.startsWith("C"))
.collect(Collectors.toList());
System.out.println("Names starting with 'C': " + filteredNames);
}
}
TypeScript
Procedural TypeScript Code
let scores: Map<string, number> = new Map([
["Alice", 90],
["Bob", 80],
["Charlie", 85]
]);
// Check the key and update the value if it matches the condition
scores.forEach((value, key) => {
if (key === "Bob") {
scores.set(key, value + 5);
}
});
console.log("Updated scores:", scores);
Functional TypeScript Code
const names: string[] = ["Alice", "Bob", "Charlie", "David"];
const filteredNames: string[] = names.filter((name: string) => name.startsWith("C"));
console.log("Names starting with 'C':", filteredNames);
Python
Procedural Python Code
names = ["Alice", "Bob", "Charlie", "David"]
filtered_names = []
for name in names:
if name.startswith("C"):
filtered_names.append(name)
print("Names starting with 'C':", filtered_names)
Functional Python Code
names = ["Alice", "Bob", "Charlie", "David"]
filtered_names = list(filter(lambda name: name.startswith("C"), names))
print("Names starting with 'C':", filtered_names)
filtered_names = list(filter(lambda name: name.startswith("C"), names)):
The filter function returns an iterator that holds all elements for which the specified function (in this case, a lambda expression) returns True.
The lambda expression lambda name: name.startswith("C") checks if each element (name) starts with "C". The startswith method returns True if the string starts with the specified prefix.
The result of the filter function is an iterator, so it is converted into a list using the list function.
Comparison of Procedural and Functional Programming
In programming, procedural and functional represent two major paradigms. These styles take different approaches to code design and implementation. Below, we analyze the advantages and disadvantages of both paradigms.
Performance
- Procedural: When dealing with large-scale data or complex algorithms, a procedural approach often achieves high performance through direct memory manipulation and tight loops.
- Functional: Due to immutability and pure functions, functional programming can result in fewer side effects and make it easier to optimize for parallel processing and lazy evaluation. However, because of frequent function calls and the creation of temporary objects, performance may be lower than procedural in some cases.
Readability
- Procedural: Since clear steps and algorithms are directly reflected in the code, it can be easy to follow its behavior. However, readability decreases when there are many complex conditional branches and state management.
- Functional: High-level abstraction and a declarative style make the intent of the code easier to read. The use of pure functions reduces side effects and increases code predictability, but concepts and operators specific to functional programming can be difficult for beginners to understand.
Maintainability
- Procedural: Because it is necessary to track changes in variable states, identifying and fixing bugs in large codebases can become difficult.
- Functional: Immutability and pure functions reduce side effects and decrease dependencies between modules, leading to higher maintainability. It is easier to test, and expanding functionality or refactoring becomes simpler.
Summary
In this article, we explored the differences between procedural and functional programming through code examples in Java, JavaScript, TypeScript, and Python. Understanding the advantages and limitations of each paradigm is important for choosing a more effective programming style.
- Procedural: Provides high performance in specific scenarios through intuitive and concrete operations, but code complexity and maintainability can become issues in large-scale applications.
- Functional: Improves code readability and maintainability through high levels of abstraction and immutability, though performance and the learning curve should be considered.
Choose the optimal approach by considering project requirements, team experience, and the characteristics of the language being applied.
Discussion