Flutter - Dart 기초(3)[Functional Programming]
함수형 프로그래밍 Functional Programming?
일련의 코딩 접근 방식으로서, 자료처리를 수학적 함수의 계산으로 취급하고 상태와 가변데이터를 멀리하는 프로그래밍 패러다임을 뜻한다
나도 솔직히 뭔 소린진 와닿진 않는다, 좀 객체지향 프로래밍과 대조적인 특징인데 다트는 객체지향 프로그래밍과 함수형 프로그래밍의 특징 모두 다 제공한다.
즉, 여러 함수들을 통해서 프로그래밍 할 수 있다는 것
1. List, Map, Set 자료형 변환
void main(){
List<String> newJeans = ['해린', '민지', '하니', '다니엘', "혜인", '해린'];
print(newJeans);
print(newJeans.asMap()); // 리스트를 맵으로 변환
print(newJeans.toSet()); // 리스트를 세트로 변환 1
// print(Set.from(newJeans)); // 리스트를 세트로 변환 2
print("----------------------------------");
Map newJeansMap = newJeans.asMap(); // 리스트를 맵으로
print(newJeansMap);
print(newJeansMap.keys);
print(newJeansMap.values);
print(newJeansMap.keys.toList()); // 맵의 키값들을 리스트로 변환
print(newJeansMap.values.toList()); // 맵의 밸류값들을 리스트로 변환
print("----------------------------------");
Set newJeansSet = Set.from(newJeans); // 리스트를 세트로
print(newJeansSet);
print(newJeansSet.toList()); // 세트를 리스트로 변환
}
위와 같이 다트에서 제공되는 여러 함수들을 통해서 자료형 변환이 자유롭게 활용된다(자유로워 보이는 자료형변환이 너무너무 좋다,,)
2. Map() 함수
map 함수는 어떠한 형태에서 다른 형태로 변환할 때 유용한데, map함수는 사용 하기 위해서 우선 함수 내 함수가 필요하다
void main(){
List<String> newJeans = ['해린', '민지', '하니', '다니엘', "혜인"];
final newNewJeans = newJeans.map((x){
return '뉴진스 $x';
});
// map 함수는 함수 내 함수가 필요, map()은 어떠한 형태에서 다른 형태로 변환할때 유용하다
// 뉴진스를 멤버명 앞에 붙여주기
print(newJeans);
print(newNewJeans);
}
리스트의 경우,
newNewJeans라는 변수를 만들고, newJeans.map해준 결과를 넣어서 출력해보면 멤버명 앞에 '뉴진스'가 붙는 것을 알 수 있다
즉, map에서 x는 위 리스트에서 값들을 하나하나 불러와서 새로운 리스트값으로 리턴해주는 것임을 알 수 있다.
final newNewJeans = newJeans.map((x){
return '뉴진스 $x';
});
// 2. final newNewJeans2 = newJeans.map((x) => '뉴진스 $x');
두 번째 방식으로 arrow 함수 방식으로 써도 된다
print(newJeans == newJeans);
print(newNewJeans == newJeans);
print(newNewJeans == newNewJeans2);
만약 타입을 검사해보면,
map 해서 만든 newNewJeans와 newNewJeans2가 각각 다르다는 것을 알 수 있다, 즉 map은 아예 새로운 리스트를 계속 만들어 낸다는 것을 알아야!
다트에서 제공하는 함수형 프로그래밍이 아주 편한 이유는 아래와 같다
void main(){
// 1.jpg, 3.jpg, 5.jpg, 7.jpg, 9.jpg
String number = '13579';
final parsed = number.split('').map((x) => '$x.jpg').toList();
print(parsed);
}
간단하게 함수 두개를 이용한 한 줄 만으로 새로운 리스트를 뚝딱 만들 수 있다.
Map 함수를 활용해 키와 밸류값을 가지는 맵 자료형도 변환가능하다
void main(){
Map<String, String> HarryPorter = { 'Harry Porter': '해리 포터',
'Ron Weasley': '론 위즐리',
'Hermione Granger': '엠마 왓슨'};
final Map<String, String> HarryPorter2 = HarryPorter.map(
(key, value) => MapEntry(
'Harry Porter Character $key',
'해리포터 캐릭터 $value'
)
);
print(HarryPorter2);
}
리스트를 변환하는것과 동일하지만 키와 밸류값으로 되어 있기에 MapEntry 함수를 통해서 변경해주어야 하는데, 위와 같이 작성한 후 실행해보면 결과값으로
잘 생성된다
물론 키값만 따로 변환하려 한다면 아래와 같다
final keys = HarryPorter.keys.map((x) => 'Harry Potter Character $x');
즉, 맵 자체를 다른 맵으로 바꾸는 방법과, 다른 리스트로 만들어버리는 방식 있다는 것
세트 자료형 변환은 아래와 같은데,
void main(){
Set chelsea = {
'메이슨 마운트',
'리스 제임스',
'세자르 아스필리쿠에타',
'티아고 실바'
};
final newChelsea = chelsea.map((x) => '첼시팬의 사랑 $x').toSet();
print(newChelsea);
}
리스트와 동일하다, 위 결과는 아래와 같은 세트를 보여준다
2. Where 함수로 값 유지, 삭제
where은 자료형을 루프를 돌면서 조건과 다르면 false, 일치하면 true를 리턴하며 false일 시 값 삭제, true일 시 값 유지를 할 수 있게 해주는 함수이다. 아래와 같이 사용가능하다
void main(){
List<Map<String, String>> people = [{
'name' : '로제',
'group' : '로제파스타'
},
{
'name' : '지수',
'group' : '로제파스타'
},
{
'name' : 'RM',
'group' : 'Relationship Management'
},
{
'name' : '정국',
'group' : 'Relationship Management'
}];
print(people);
// where은 리스트 멤버들을 루프를 돌면서 true, false 값을 돌려준다
// true일 시 값을 유지, false일 시 값을 제거
final blackpink = people.where((x) => x['group'] == '로제파스타').toList();
final bts = people.where((x) => x['group'] == 'Relationship Management').toList();
print(blackpink);
print(bts);
}
즉 리스트 안에 맵 형식으로 값을 가지는 리스트를 선언을 한 후에, group 값이 로제파스타인 경우와, 하나는 다른 경우로 블랙핑크와 bts 리스트를 만들어 보았고 결과를 본다면
위와 같이 그룹 명에 따라 값을 유지, 제거한 결과가 잘 나타난다(너무 좋은데?)
필터링 역할을 하기에 자주, 많이 사용된다하니 잘 기억해두자.
3. Reduce 함수로 합 구하기
int값으로 구성된 리스트를 하나 정의하고,
리스트에 reduce 함수를 사용해본다. map은 인자로 하나가 필요한 반면 reduce는 두개가 필요하다.
void main(){
List<int> numbers = [
1,
3,
5,
7,
9
];
final result = numbers.reduce((prev, next){
print('--------------');
print('prev : $prev');
print('next : $next');
print('total : ${prev + next}');
return prev + next;
});
// final result = numbers.reduce((prev, next) => prev + next); 와 동일 역할
print(result);
}
결과를 보면,
이렇게 나오는데, 즉 prev 값으로는 리스트 첫 번째 값이 먼저 들어가고 next값은 그 다음번째 리스트 값, 그리고 이후 prev값은 return된 값을 받는다, 그리고 next는 next에서 한 단계 더 뒤의 값을 가리킨다.
즉 이렇게 sum을 구할 수 있다. sum은 다른 방식으로 더 쉽게 만들수도 잇으니 다르게 한번 사용해보자,
다만 reduce은 실행 조건이 있다.
return되는 값이 태초가 되는 값의 자료형과 동일해야한다는 것이다. 즉, String으로 된 List를 돌면서 문자열 길이를 다 더해서 int로 리턴하거나 할 수는 없다는 것!
4. Fold함수
같은 타입을 무조건 반환해야 한다는 reduce의 조건을 보완한 함수이다.
List<int> numbers = [1, 3, 5, 7, 9];
final sum = numbers.fold<int>(0, (prev, next) => prev + next);
print(sum);
reduce와 거의 동일하게 형태를 가지고 있지만 return하는 자료형을 fold뒤에 명시해주고, prev, next 앞 인자는 처음 시작하는 prev값을 가리킨다. 즉 첫 번째 수행에서는 prev : 0, next : 1, 두 번째 수행에서는 prev : 1, next : 3, 세 번째 수행에서는 prev : 4, next : 5 이런 식인 것이다.
fold를 통해 아래처럼 아까전에 reduce로 불가했던 다른 자료형의 리턴값을 나타낼 수 있다.
5. Cascading Operator
Cascading Operator는 리스트 내 값들을 하나하나 풀어서 전달한다. 리스트 앞 ...을 붙여줌으로써 사용가능하고 아래와 같이 결과를 받을 수 있다.
print(even == [...even]); 으로, cascade 해 만든 리스트와 기존 리스트를 비교하면 false가 출력된다, 즉 아예 다른 리스트를 새로 하나 만드는 것이다
이러한 함수형 프로그래밍으로 실제로 데이터를 변환하는 예시?
만약, 아래와 같은 맵을 가지는 리스트 형식으로 데이터를 가져온다고 가정하자, 하지만 맵은 그 내부에 값들의 자유도가 높기에(형식이 정해지지 않았기에) 각 리스트 내 맵 값을 신뢰할 수 있는 구조인 클래스로 만들고 싶어하는 상황이다.
final List<Map<String, String>> people = [
{
'name' : '지수',
'group' : '블랙핑크'
},
{
'name' : '제니',
'group' : '블랙핑크'
},
{
'name' : 'RM',
'group' : 'BTS'
},
{
'name' : '뷔',
'group' : 'BTS'
},
];
함수형 프로그래밍으로 변환
void main(){
final List<Map<String, String>> people = [
{
'name' : '지수',
'group' : '블랙핑크'
},
{
'name' : '제니',
'group' : '블랙핑크'
},
{
'name' : 'RM',
'group' : 'BTS'
},
{
'name' : '뷔',
'group' : 'BTS'
},
];
print(people);
final parsedPeople = people.map(
(x) => Person(
name: x['name']!,
group: x['group']!
),
).toList();
print(parsedPeople);
for(Person person in parsedPeople){
print(person.name);
print(person.group);
print('----------------\n');
}
final bts = parsedPeople.where(
(x) => x.group == 'BTS');
print('--------bts--------');
print(bts);
}
class Person{
final String name;
final String group;
Person({required this.name,
required this.group});
@override
String toString(){
return 'Person(name: $name, group: $group)';
} // 클래스가 사용하는 toString을 instance의 합 방식이 아닌 개별 값으로 바꾸는 메서드
}
근데 이렇게 따로따로 과정을 거쳐가고, 출력하는 과정 말고 함수형 프로그래밍은 그 과정을 아래처럼 단축할 수 있다.
void main(){
final List<Map<String, String>> people = [
{
'name' : '지수',
'group' : '블랙핑크'
},
{
'name' : '제니',
'group' : '블랙핑크'
},
{
'name' : 'RM',
'group' : 'BTS'
},
{
'name' : '뷔',
'group' : 'BTS'
},
];
final result = people.map(
(x) => Person(
name : x['name']!,
group: x['group']!)
).where((x) => x.group == 'BTS');
print(result);
// !해주는 이유는 하지 않으면 name, group을 key값으로 가지고 있다는 것을 명확히 알 수 없기에
// 오류를 리턴 할 수 있는데, !로 강제해주는 것(우리는 알 수 있기에)
}
class Person{
final String name;
final String group;
Person({required this.name,
required this.group});
@override
String toString(){
return 'Person(name: $name, group: $group)';
} // 클래스가 사용하는 toString을 instance의 합 방식이 아닌 개별 값으로 바꾸는 메서드
}
위 코드의 결과로
Person 클래스로 정렬된 BTS 그룹의 값을 출력할 수 있다.
함수형 프로그래밍은 이렇게 무한하게 계속 리턴되는 값을 리턴하고, 리턴하고 함수를 계속 붙이면서 코드를 짤 수 있다.
다만 너무 짧게 코드를 짜는 천재적인 능력을 자랑하지말자 너무 복잡해질 수 있다.