1. 함수형 프로그래밍 (Functional Programming)
Dart는 객체 지향 프로그래밍과 함수형 프로그래밍, 비동기 프로그래밍을 모두 제공하는 언어입니다.
이번 포스팅에서는 함수형 프로그래밍(Functional Programming, FP)에 대해 소개하겠습니다.
함수형 프로그래밍은 순수 함수와 불변성을 기반으로 하는 프로그래밍 방식이에요.
순수 함수와 불변성에 대해서는 아래 특징에서 설명드릴게요.
2. 특징
1) 순수 함수 (Pure Function)
순수 함수는 동일한 입력에 대해 항상 동일한 결과를 반환하고, 외부 상태를 변경하지 않는 함수입니다.
int add(int a, int b) {
return a + b;
}
이 함수는 a와 b를 인자로 받아 두 수를 더하는 함수예요.
a = 1, b = 2를 넣으면 항상 3을 반환하겠죠?
또는, a = 3, b = 4를 넣으면 항상 7을 반환해요.
이렇게 입력이 같으면 결과가 항상 같은 함수를 순수 함수라고 해요.
외부 상태를 변경하지 않기 때문에 부작용(Side Effect)도 없어요.
부작용은 외부 상태를 변경하거나 예상치 못한 동작을 일으키는 것을 말해요.
즉, 함수 내부에서 전역 변수 변경, API 호출, 데이터베이스 업데이트와 같은 동작을 하면 부작용이 발생해요.
그럼 비순수 함수를 볼까요?
void main() {
print(greet()); // Hello, Bob
name = 'John';
print(greet()); // Hello, John
}
String name = 'Bob';
String greet() => 'Hello, $name';
처음으로 print(greet())를 실행하면 Hello, Bob이 출력됩니다.
하지만, 두 번째로 실행하면 Hello, Joh이 출력되죠.
순수 함수는 입력이 같으면 항상 동일한 결과 값을 반환한다고 했잖아요?
두 함수를 호출하는데 동일한 입력값을 갖고 있으나 반환 값이 다릅니다.
그렇기 때문에 이 함수는 순수 함수가 아닙니다.
이제 이 함수를 순수 함수로 바꿔보겠습니다.
void main() {
print(greet('John')); // Hello, John
print(greet('Bob')); // Hello, Bob
}
String greet(String name) => 'Hello, $name';
이전코드의 name이라는 외부 변수에 의존하지 않고
매개변수를 사용하여 항상 동일한 입력에 대해 동일한 값을 반환하고 있습니다.
순수 함수를 사용하면 뭐가 좋을까요?
같은 입력이면 항상 같은 결과가 나오기 때문에 코드가 예측 가능해져요!
또 외부 상태를 변경하지 않기 때문에 예상치 못한 버그가 발생하지 않아요.
그래서 Dart로 프로그래밍을 할 때는 순수 함수로 코드를 작성하는 연습을 하는 것이 좋습니다.
2) 불변성 (Immutable Data)
한번 생성된 데이터는 변경되지 않으며, 항상 새로운 데이터를 반환하는 방식입니다.
이는 아래 예제를 보면 더 잘 이해할 수 있어요!
3) 함수는 1급 시민 (First-Class Citizen)
1급 시민이란 다음 조건을 만족하는 요소를 뜻해요.
- 변수에 저장 가능
- 함수의 매개변수로 전달 가능
- 함수의 반환값으로 사용 가능
즉, 함수를 변수처럼 저장하거나, 다른 함수의 인자로 넘길 수 있는 특징을 가졌어요.
void greet() {
print("Hello, Dart!");
}
void main() {
var sayHello = greet; // 함수를 변수에 저장 가능
sayHello(); // Hello, Dart!
}
4) 고차 함수
고차 함수는 함수를 매개변수로 받거나 반환하는 함수를 의미합니다.
Dart에서 함수가 1급 시민이므로, 고차함수도 자연스럽게 사용할 수 있어요.
5) 메서드 체이닝 (Method Chaining)
두 개 이상의 함수를 결합하여 새로운 함수를 만드는 과정입니다.
이를 메서드 체이닝 (Method Chainig)이라고 불러요.
메서드 체이닝은 .을 사용해서 여러 개의 함수를 하나로 연결하는 방식입니다.
여러 함수를 조합하기 때문에 코드의 가독성이 향상된다는 장점이 있습니다.
var numbers = [1, 2, 3, 4, 5];
var result = numbers
.map((n) => n * 2) // 각 요소를 2배로 변환
.where((n) => n > 5) // 5보다 큰 숫자만 필터링
.toList(); // List로 변환
print(result); // [6, 8, 10]
3. 많이 사용하는 함수
1) 형변환 함수
- toString()
toString()은 String 타입으로 변환한 값을 반환합니다.
int number = 8;
var result = number.toString();
print(result.runtimeType); // String
- int.parse(' ')
String 타입의 값을 int 타입으로 변환한 후, 값을 반환해요.
String number = '12';
var result = int.parse(number);
print(result); // 12
print(result.runtimeType); // int
- double.parse(' ')
String 타입의 값을 double 타입으로 변환한 후, 값을 반환해요.
String number = '12.3';
var result = double.parse(number);
print(result); // 12.3
print(result.runtimeType); // double
- toList()
특정 Collection 타입의 값을 List 타입으로 변환한 값을 반환합니다.
Set<String> fruitSet = {'사과', '바나나', '딸기'};
var fruitList = fruitSet.toList();
print(fruitList); // [사과, 바나나, 딸기]
print(fruitList.runtimeType); // List<String>
주의할 점은 Map은 toList를 사용하지 못한다는 것 기억해 주세요!
- toSet()
특정 Collection 타입의 값을 Set 타입으로 변환한 값을 반환합니다.
참고로 Set은 중복값을 허용하지 않기 때문에 Collection 값에 중복된 값이 있으면 중복된 값을 제외한 Set를 반환해요.
List<String> fruitList = ['사과', '바나나', '딸기', '사과'];
var fruitSet = fruitList.toSet();
print(fruitSet); // {사과, 바나나, 딸기}
print(fruitSet.runtimeType); // LinkedSet<String>
마찬가지로 Map에는 적용하지 못합니다.
- asMap()
특정 Collection 타입의 값을 Map 타입으로 변환한 값을 반환합니다.
Map이 Key와 Value를 쌍으로 저장하는 데이터 구조라는 것 기억하시나요?
List는 하나의 요소를 저장하고 있기에 Key는 인덱스, Value는 List의 요소가 저장됩니다.
List<String> fruitList = ['사과', '바나나', '딸기'];
var fruitMap = fruitList.asMap();
print(fruitMap); // {0: 사과, 1: 바나나, 2: 딸기}
여기서도 주의할 점이 있어요!
Set은 순서가 없기 때문에 index를 적용할 수 없습니다.
그렇기 때문에 asMap()은 사용이 불가능해요.
하지만, Set도 Map으로 변환하는 방법이 있는데요!
그것은 Set을 List로 변환한 후, List를 Map으로 바꿔주는 방법입니다!
Set<String> fruitSet = {'사과', '바나나', '딸기'};
var fruitList = fruitSet.toList();
var fruitMap = fruitList.asMap();
print(fruitMap); // {0: 사과, 1: 바나나, 2: 딸기}
2) 고차 함수
고차 함수는 앞서 설명했었는데요.
함수를 매개변수로 받거나 반환하는 함수를 의미합니다.
주로 Collection 타입의 데이터에 있는 요소를 처리하거나 변환할 때 사용해요.
- map()
Collection 타입인 데이터의 각 요소에 특정 함수를 적용한 새로운 Collection 타입의 데이터를 반환합니다.
map(([매개변수]) { return [매개변수에 적용할 동작] });
List<int> numbers = [1, 2, 3];
var doubledNumbers = numbers.map((n) {
return n * 2;
});
print(doubledNumbers); // (2, 4, 6)
// 매개변수에 적용할 동작을 한줄로 표현 가능한 경우에만 사용 가능
List<int> numbers = [1, 2, 3];
var doubledNumbers = numbers.map((n) => n * 2);
print(doubledNumbers); // (2, 4, 6)
map()은 원본 데이터를 직접 가공하지 않고, 특정 동작을 적용한 새로운 데이터를 반환해요.
- where()
Collection 타입의 데이터에 있는 각 요소들을 특정 조건에 넣었을 때 참인 요소들만 필터링한 새로운 Collection 타입의 데이터를 반환해요.
where(([매개변수]) { return [조건식] });
List<int> numbers = [1, 2, 3, 4, 5, 6];
var result = numbers.where((number) {
return number > 5;
});
print(result); // (6)
// 매개변수에 적용할 동작을 한줄로 표현 가능한 경우에만 사용 가능
List<int> numbers = [1, 2, 3, 4, 5, 6];
var result = numbers.where((n) => n > 5);
print(result); // (6)
where()도 원본 데이터를 직접 가공하지 않고, 특정 함수를 적용한 새로운 데이터를 반환해요.
또한, 조건식이 참인 요소가 없는 경우에는 빈 값을 반환하기도 합니다.
- firstWhere()
Collection 타입의 데이터에 있는 각 요소들을 특정 조건에 넣었을 때 참인 요소들 중 첫 번째 요소를 반환해요.
List<int> numbers = [1, 2, 3, 4, 5, 6, 7];
var result = numbers.firstWhere((number) {
return number > 5;
});
print(result); // 6
// 매개변수에 적용할 동작을 한줄로 표현 가능한 경우에만 사용 가능
List<int> numbers = [1, 2, 3, 4, 5, 6, 7];
var result = numbers.firstWhere((n) => n > 5);
print(result); // 6
firstWhere()도 원본 데이터를 직접 가공하지 않고, 특정 함수를 적용한 새로운 데이터를 반환합니다.
또한, 조건식이 참인 요소가 없는 경우에는 오류가 발생해요.
- lastWhere()
Collection 타입의 데이터에 있는 각 요소들을 특정 조건에 넣었을 때 참인 요소들 중 마지막 요소를 반환해요.
List<int> numbers = [1, 2, 3, 4, 5, 6, 7];
var result = numbers.lastWhere((number) {
return number > 5;
});
print(result); // 7
// 매개변수에 적용할 동작을 한줄로 표현 가능한 경우에만 사용 가능
List<int> numbers = [1, 2, 3, 4, 5, 6, 7];
var result = numbers.lastWhere((n) => n > 5);
print(result); // 7
lastWhere()도 원본 데이터를 직접 가공하지 않고, 특정 함수를 적용한 새로운 데이터를 반환해요.
또한, 조건식이 참인 요소가 없는 경우에는 오류가 발생해요.
- reduce()
Collection 타입의 데이터에 있는 요소들을 하나의 값으로 결합해요.
List<int> numbers = [1, 2, 3];
var result = numbers.reduce((a, b) {
return a + b;
});
print(result); // 6
// 매개변수에 적용할 동작을 한줄로 표현 가능한 경우에만 사용 가능
List<int> numbers = [1, 2, 3];
var result = numbers.reduce((a, b) => a + b); // 6
첫 번째 매개변수에는 이전 실행에서 반환된 값 (첫 번째 실행에서는 이전 실행이 없기 때문에 Collection 타입의 데이터에 있는 첫번째 값) 이 할당되고,
두 번째 매개변수 에는 Collection 타입의 데이터에 있는 다음 값이 할당됩니다.
reduce()는 Collection 타입의 데이터와 같은 타입으로만 반환할 수 있어요.
또한, Collection 타입의 데이터에 요소가 없는 경우에는 오류가 발생합니다.
- fold()
Collection 타입의 데이터에 있는 요소들을 하나의 값으로 결합해요.
reduce와 다른 점은 초기값이 있다는 점이에요.
List<int> numbers = [1, 2, 3];
var result = numbers.fold(1, (a, b) {
return a + b;
});
print(result); // 7
// 매개변수에 적용할 동작을 한줄로 표현 가능한 경우에만 사용 가능
List<int> numbers = [1, 2, 3];
var result = numbers.fold(1, (a, b) => a + b); // 7
첫 번째 매개변수에는 이전 실행에서 반환된 값 (첫 번째 실행에서는 초기값) 이 할당되고,
두 번째 매개변수에는 Collection 타입의 데이터에 있는 다음 값이 할당돼요.
fold()는 Collection 타입의 데이터와 다른 타입으로도 반환이 가능하며,
Collection 타입의 데이터에 요소가 없어도 오류가 발생하지 않아요.
4. 참고자료
Functional Programming in Dart and Flutter with the 'dartz' Package
Introduction
medium.com
https://levelup.gitconnected.com/functional-programming-in-dart-foundation-part-0-7e932517b824
Functional Programming in Dart: Foundation [Part 0]
In this article series, we’ll go on a journey to understand Functional Programming and apply it using Dart.
levelup.gitconnected.com
Pure functions & Side effects in Dart [Functional Programming — Part 1]
In this article, we’ll look at pure functions and side effects, and how they help to minimize unexpected bugs in our code.
yogi7y.medium.com
https://dart.dev/language/functions
Functions
Everything about functions in Dart.
dart.dev
'Dart' 카테고리의 다른 글
[Dart] 제네릭 (Generics) - 제네릭 함수 & 클래스 (0) | 2025.03.12 |
---|---|
[Dart] 함수 (Functions) (0) | 2025.03.11 |
[Dart] 열거형 (Enumerated types) (0) | 2025.03.11 |
[Dart] 컬렉션 (Collections) - List, Set, Map (0) | 2025.03.11 |