본 내용은 인프런 중 [코드팩토리] [입문] 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가지로 나눌 수 있다.

  1. 실행하는 대상(list, map, set)과 완전히 다른 새로운 값을 생성한다.
  2. 함수들을 체이닝을 할 수 있다.
  3. 연결성에 취해 너무 많은 함수들을 남용해여 직관적이고 간략하지 못한 코드 작성을 피한다.

간혹 OOP와 Functional Programming 중 어느게 더 낫냐는 의견을 물을 때가 있는데, 그때 그때 마다 상황에 맞는 프로그래밍 기법을 하는것이 제일 좋다.