Dart - 함수형 프로그래밍
본 내용은 인프런 중 [코드팩토리] [입문] Dart 언어 4시간만에 완전정복 강의 내용을 정리한 내용입니다.
Functional Programming(함수형 프로그래밍)
void main() {
List<String> blackPink = ['로제', '지수', '리사', '제니', '제니'];
print('blackPink: $blackPink');
print('blackPink.asMap(): ${blackPink.asMap()}');
print('blackPink.toSet(): ${blackPink.toSet()}');
print('');
Map blackPinkMap = blackPink.asMap();
print('blackPinkMap.keys.toList(): ${blackPinkMap.keys.toList()}');
print('blackPinkMap.values.toList(): ${blackPinkMap.values.toList()}');
print('');
Set blackPinkSet = Set.from(blackPink);
print('blackPinkSet.toList(): ${blackPinkSet.toList()}');
}
Console 출력창
blackPink: [로제, 지수, 리사, 제니, 제니]
blackPink.asMap(): {0: 로제, 1: 지수, 2: 리사, 3: 제니, 4: 제니}
blackPink.toSet(): {로제, 지수, 리사, 제니}
blackPinkMap.keys.toList(): [0, 1, 2, 3, 4]
blackPinkMap.values.toList(): [로제, 지수, 리사, 제니, 제니]
blackPinkSet.toList(): [로제, 지수, 리사, 제니]
Map
멤버 이름 앞에 그룹명 붙히기
리스트에 담겨있는 변수들을 특정 이름을 붙혀서 출력해주는 코드를 작성해보자. 파이썬의 lambda 기능과 유사한 성격을 띄는 방식으로 보면 편할 것이다.
void main() {
List<String> blackPink = ['로제', '지수', '리사', '제니'];
final newBlackPink = blackPink.map((x){
return '블랙핑크 $x';
}); // 멤버들 이름 앞에 블랙핑크를 붙혀서 반환
print(blackPink);
print(newBlackPink.toList()); // .toList()를 사용하지 않으면 () 형태로 반환해준다.
}
Console 출력창
[로제, 지수, 리사, 제니]
[블랙핑크 로제, 블랙핑크 지수, 블랙핑크 리사, 블랙핑크 제니]
newBlackPink를 조금 더 간편하고 직관적으로 고쳐보자.
void main() {
List<String> blackPink = ['로제', '지수', '리사', '제니'];
final newBlackPink = blackPink.map((x){
return '블랙핑크 $x';
});
final newBlackPink2 = blackPink.map((x) => '블랙핑크 $x');
print(blackPink);
print(newBlackPink.toList());
print(newBlackPink2.toList());
}
Console 출력창
[로제, 지수, 리사, 제니]
[블랙핑크 로제, 블랙핑크 지수, 블랙핑크 리사, 블랙핑크 제니]
[블랙핑크 로제, 블랙핑크 지수, 블랙핑크 리사, 블랙핑크 제니]
이미지 이름 지정해주기
우리에게 ‘13579’라는 String 타입의 숫자가 주어졌다. 이 숫자를 1.jpg, 3.jpg…. 와 같은 형태로 변환하고 싶다. 확인해보자.
void main() {
String number = '13579';
final parsed = number.split('').map((x) => '$x.jpg').toList();
print(parsed);
}
Console 출력창
[1.jpg, 3.jpg, 5.jpg, 7.jpg, 9.jpg]
해리포터 한영이름 지정하기
리스트 내 변수들 이름 앞에 추가로 이름을 붙히는게 가능한 것처럼, 딕셔너리 형태에도 사용이 가능하다.
void main() {
Map<String, String> harryPotter = {
'Harry Potter': '해리포터',
'Ronm WEasly': '론 위즐리',
'Hermione Granger': '헤르미온느 그레인저'
};
final result = harryPotter.map(
(key, value) => MapEntry(
'Harry Potter Character $key',
'해리포터 캐릭터 $value',
),
);
print(harryPotter);
print('');
print(result);
}
Console 출력창
{Harry Potter: 해리포터, Ronm WEasly: 론 위즐리, Hermione Granger: 헤르미온느 그레인저}
{Harry Potter Character Harry Potter: 해리포터 캐릭터 해리포터, Harry Potter Character Ronm WEasly: 해리포터 캐릭터 론 위즐리, Harry Potter Character Hermione Granger: 해리포터 캐릭터 헤르미온느 그레인저}
key 값과 value 값을 각각 지정해주어서 map 기능을 이용하여 이름을 변경해주었다. 허나, 이 활용법은 그리 많이 쓰이지 않는다. 대신, key값만 또는 value 값만 수정하는 경우는 발생하게 된다. 아래와 같이 활용하면 쉽게 사용이 가능하다.
//..
final keys = harryPotter.keys.map((x) => 'HPC $x').toList();
final values = harryPotter.values.map((x) => '해리포터 $x').toList();
print(keys);
print(values);
}
Console 출력창
[HPC Harry Potter, HPC Ronm WEasly, HPC Hermione Granger]
[해리포터 해리포터, 해리포터 론 위즐리, 해리포터 헤르미온느 그레인저]
Set
Map 기능과 크게 다르지 않으나, set의 고유 기능인 중복값은 정리해주는 기능을 가지고 있다.
void main() {
Set blackPinkSet = {
'로제',
'로제',
'지수',
'제니',
'리사',
};
final newSet = blackPinkSet.map((x) => '블랙핑크 $x').toSet();
print(newSet);
}
Console 출력창
{블랙핑크 로제, 블랙핑크 지수, 블랙핑크 제니, 블랙핑크 리사}
출력된 내용을 보면 블랙핑크 로제는 한번만 나오는 것을 확인할 수 있다.
where
where 함수는 필터링 역할을 해줄 수 있다. 아래와 같이 블랙핑크, BTS 두개의 아이돌 그룹이 존재하는 리스트 속에서 블랙핑크만, 그리고 BTS만 따로 뽑아내는 코드를 작성해보자.
void main() {
List<Map<String, String>> people = [
{
'name': '로제',
'group': '블랙핑크'
},
{
'name': '지수',
'group': '블랙핑크'
},
{
'name': 'RM',
'group': 'BTS'
},
{
'name': 'RM',
'group': 'BTS'
},
];
print(people);
final blackPink = people.where((x) => x['group'] == '블랙핑크').toList();
final bts = people.where((x) => x['group'] == 'BTS').toList();
print(blackPink);
print(bts);
}
Console 출력창
[{name: 로제, group: 블랙핑크}, {name: 지수, group: 블랙핑크}, {name: RM, group: BTS}, {name: RM, group: BTS}]
[{name: 로제, group: 블랙핑크}, {name: 지수, group: 블랙핑크}]
[{name: RM, group: BTS}, {name: RM, group: BTS}]
Reduce
Reduce는 두개의 인자를 받는 함수이다. List안에 있는 숫자를 더하는 함수를 만들어보자.
void main() {
List<int> numbers = [1, 3, 5, 7, 9];
final result = numbers.reduce((prev, next) {
print('--------------');
print('previous : $prev');
print('next : $next');
print('total : ${prev + next}');
return prev + next;
});
print(result);
}
Console 출력창
--------------
previous : 1
next : 3
total : 4
--------------
previous : 4
next : 5
total : 9
--------------
previous : 9
next : 7
total : 16
--------------
previous : 16
next : 9
total : 25
25
코드를 유심히 살펴보면 첫 번째의 경우에는 prev가 1, next가 3이지만, 두 번째부터는 prev가 4, next가 5로 나온다. 이는 두번째 reduce부터는 prev값이 return 받은 prev + next 값이기 때문이다.
String List도 가능하다.
//..
List<String> words = [
'안녕하세요 ',
'저는 ',
'코드팩토리입니다.',
];
final sentence = words.reduce((prev, next) => prev + next);
print(sentence);
}
Console 출력창
//..
안녕하세요 저는 코드팩토리입니다.
reduce의 주의사항이라면, 입력받은 값과 return하는 값의 타입이 동일해야한다는 점이다.
Fold
Fold는 Reduce의 비슷한 기능을 가지고 있다. 아래 예시 코드를 확인해보자.
void main() {
List<int> numbers = [1, 3, 5, 7, 9];
final sum = numbers.fold<int>(0, (prev, next){
print('------------');
print('prev : $prev');
print('next : $next');
print('total : ${prev + next}');
return prev + next;
});
print(sum);
}
Console 출력창
------------
prev : 0
next : 1
total : 1
------------
prev : 1
next : 3
total : 4
------------
prev : 4
next : 5
total : 9
------------
prev : 9
next : 7
total : 16
------------
prev : 16
next : 9
total : 25
25
출력창을 보면 Reduce와 매우 흡사해보이지만 첫번 째 단계가 다른 것을 확인할 수 있다. 이는 첫번 째 prev가 리스트에 있는 1이 아닌, 함수의 첫 번째 파라미터인 0을 받은 것이다.
또한 Fold는 Reduce와 다르게 입력받은 변수 타입과 출력되는 변수타입이 달라도 문제가 없다. 글자를 합치는 것과 글자 수를 반환해주는 코드를 작성해보자.
void main() {
List<String> words = [
'안녕하세요 ',
'저는 ',
'코드팩토리입니다.',
];
final sentence = words.fold<String>('', (prev, next) => prev + next);
print(sentence);
final count = words.fold<int>(0, (prev, next) => prev + next.length);
print(count);
}
Console 출력창
안녕하세요 저는 코드팩토리입니다.
18
Cascading Operator
Cascading operator는 기존 리스트를 해체 후, 안에 담겨있는 변수를 새로운 리스트 형태로 담아두는 함수이다. 사용법은 변수명 앞에 …을 선언하면 된다.말로 설명했을 때는 어려워 보일 수 있다. 아래 예시를 확인해보자.
void main() {
List<int> even = [
2,
4,
6,
8,
];
List<int> odd = [
1,
3,
5,
7,
];
print('cascading 미적용 : ${[even, odd]}');
print('cacading 적용 : ${[...even, ...odd]}'); //even과 odd 앞에 ... 을 붙힘
}
Console 출력창
cascading 미적용 : [[2, 4, 6, 8], [1, 3, 5, 7]]
cacading 적용 : [2, 4, 6, 8, 1, 3, 5, 7]
주의해야 할 점은 Cascading을 적용한 리스트와 기존 리스트는 다른 개체로 인식한다.
//..
print(even);
print([...even]);
print('두 개의 even이 같은가? : ${even == [...even]}');
}
Console 출력창
//..
[2, 4, 6, 8]
[2, 4, 6, 8]
두 개의 even이 같은가? : false
Functional Programming 해보기
where 함수에서 사용했던 예문을 바탕으로 진행을 해보도록 한다. 우선, 클래스를 생성한다. 그 이유는 map의 경우는 자유도가 매우 높다. 즉, 우리가 map에서 받는 값이 name과 group이 맞는지 알기 어려우며 오탈자 확인도 프로그램으로 확인하기 어렵다. 클래스 형태로 구조화하면 들어오는 데이터에 대한 신뢰도를 확보할 수 있다.
void main() {
List<Map<String, String>> people = [
{
'name': '로제',
'group': '블랙핑크'
},
{
'name': '지수',
'group': '블랙핑크'
},
{
'name': 'RM',
'group': 'BTS'
},
{
'name': 'RM',
'group': 'BTS'
},
];
print(people)
}
class Person {
final String name;
final String group;
Person({
required this.name,
required this.group,
});
}
Console 출력창
[{name: 로제, group: 블랙핑크}, {name: 지수, group: 블랙핑크}, {name: RM, group: BTS}, {name: RM, group: BTS}]
이제 people을 클래스의 형태로 만들어보도록 한다.
//..
final parsedPeople = people
.map(
(x) => Person(
name: x['name']!, // 뒤에 느낌표를 붙히는 이유는 프로그래밍 상 map에 어떤 값이 존재하는지
group: x['group']!, // 확신할 수 없기에 느낌표를 붙힘으로서 값이 존재함을 확인시켜준다.
),
)
.toList();
print(parsedPeople);
//..
@override
String toString(){
return 'Person(name : $name, group : $group)';
}
}
Console 출력창
[Person(name : 로제, group : 블랙핑크), Person(name : 지수, group : 블랙핑크), Person(name : RM, group : BTS), Person(name : RM, group : BTS)]
이렇게 해놓으면 유용한 활용을 할 수 있다. 예시로 loop활용을 해보자.
//..
print(parsedPeople);
for(Person person in parsedPeople){
print(person.name);
print(person.group); // 각 name과 group을 번갈아가며 출력
}
final bts = parsedPeople.where(
(x) => x.group == 'BTS', // parsing된 것 중에서 BTS만 출력
);
print(bts);
//..
Console 출력창
//..
[Person(name : 로제, group : 블랙핑크), Person(name : 지수, group : 블랙핑크), Person(name : RM, group : BTS), Person(name : RM, group : BTS)]
로제
블랙핑크
지수
블랙핑크
RM
BTS
RM
BTS
(Person(name : RM, group : BTS), Person(name : RM, group : BTS))
지금껏 과정들을 유심히 살펴보면 함수를 생성하고 그 안에 생성되었던 함수를 사용하여 새로운 코드들을 작성하였다. 그래서 위에서 길고 장황하게 작성한 코드를 아래와 같이 간략하게 표현이 가능하다.
//..
final result = people.map(
(x) => Person(
name: x['name']!,
group: x['group']!,
),
).where((x) => x.group == 'BTS');
print(result);
}
//..
Console 출력창
//..
(Person(name : RM, group : BTS), Person(name : RM, group : BTS))
즉, Functional Programming의 강점은 선언했던 함수들을 유기적으로 연결하여 이용함으로써 코드의 간결함을 가져갈 수 있다는 것이다.
Functional Programming의 기본은 크게 3가지로 나눌 수 있다.
- 실행하는 대상(list, map, set)과 완전히 다른 새로운 값을 생성한다.
- 함수들을 체이닝을 할 수 있다.
- 연결성에 취해 너무 많은 함수들을 남용해여 직관적이고 간략하지 못한 코드 작성을 피한다.
간혹 OOP와 Functional Programming 중 어느게 더 낫냐는 의견을 물을 때가 있는데, 그때 그때 마다 상황에 맞는 프로그래밍 기법을 하는것이 제일 좋다.