View

300x250

오늘도 로직과 싸우다가 거의 질 뻔 했다. forEach 가 날 살렸ㄷr…

이번에 겪은 문제 상황은 [ 원본 배열을 순회하면서 각 원소의 값에 대해 연산 수행 → 다른 배열에 값을 업데이트 하는 코드 ]를 map으로 처리해주려 하는 상황이였다. 분명 배열에 값이 들어있는데 map 내부 함수가 실행이 되지 않고, 심지어 print 문도 찍히지 않는 매우매우 기묘한 현상이였다. 그런데 map으로 실행이 안되던게 forEach로 넣어버리니 바로 실행이 되어버리는게 아닌가??

🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔

결론부터 짧게 지르고 시작하자면, forEach는 순회를 하는 함수이고, map은 Iterator를 반환하는 함수이다.

forEach는 Eager Evaluation을 수행하는 함수이고, map은 Lazy Evaluation을 수행하는 함수이다.

 

Iterator는 List나 Array 같은 데이터를 순차적으로 담아둔 데이터 구조가 아니라, 데이터를 순회할 수 있게 하는 객체의 개념이다.

Lazy Evaluation이라는 말은, 코드가 작성되어있는 지점에 해당 코드를 실행(Evaluation)하는 것이 아니라, 실행 결과가 참조되는 시점(사용되는 시점)에 코드를 실행한다는 의미이다.

 

나는 이 2가지 개념을 몰랐고, 이로 인해 몇 시간동안 코드랑 싸우게 되었다.ㅤ

내가 겪었던 상황과 참고한 자료들로 오늘 새로 배운 것들을 조금 더 자세히 설명해보겠다. 호후


아래가 내가 겪은 상황이다. 간단히 말하자면, A 배열의 원소를 보고, 조건에 맞으면 B 배열의 특정 index 원소를 수정하는 코드였다.

final dList = List.generate(7, (index) => false);

// 이건 실행 안됨
vList.map((element) {
  print("DEBUG: log");
  final docsDate = (element의 요일 계산)
  if (요일이 유효한 값이라면) {
    dList[docsDate] = true;
  }
});

// 이건 실행됨
vList.forEach((element) {
  print("DEBUG: log");
  final docsDate = (element의 요일 계산)
  if (요일이 유효한 값이라면) {
    dList[docsDate] = true;
  }
});

return dList;

요걸 조금 더 간단하게 추상화를 하면 아래 코드처럼 된다.

List<int> vList = [0, 1, 2, 3, 4, 5];
List<bool> dList1 = List<bool>.generate(vList.length, (index) => false);
List<bool> dList2 = List<bool>.generate(vList.length, (index) => false);

void main() {
  vList.map((element) {
    if (element > 3) {
      dList1[element] = true;
    }
  });

  print("map");
  print(dList1);
  
  
  vList.forEach((element) {
    if (element > 3) {
      dList2[element] = true;
    }
  });
  
   print("forEach");
   print(dList2);  
}

요 코드는 dartpad에서 실행해볼 수 있고, 실제로 아래처럼 false, true가 제대로 업데이트 되지 않는 것을 볼 수 있다.

 

도대체 무엇이 문제인가 찾아보려고 열심히 “difference between map and forEach” 를 검색했지만, forEach는 직접 원소에 연산을 가해 변경할 때 사용하고, map은 원소에 대해 새로운 데이터를 만들 때 사용한다는 이야기밖에 없었다.

그러다가 아래 스택오버플로우 글을 확인하였다! 여기에서도 나랑 똑같이 map은 안되는데 forEach는 왜 되는지에 대해 질문을 하고 있었다.

https://stackoverflow.com/questions/44299842/why-is-map-not-working-but-foreach-is

이 글에서는 forEach와는 다른 map을 이렇게 설명하고 있다.

  • dart의 map은 Iterable을 반환하는 함수이다.
  • Iterable은 Lazy 하다.

C언어 등을 공부할 때에도 종종 Iterable type 이라는 녀석이 나왔었는데, 한 번도 이 Iterable 타입이 정확히 무엇을 하는 지에 대해서는 찾아본 적이 없었던 것 같다. 그냥 다른 array, queue 등의 자료형으로 쉽게 변경이 가능한 자료형 정도로만 생각하고 있었는데, 이게 또 내가 모르고 있던 중요한 포인트인 것 같아서, dart에서의 Iterable이 무엇인지 찾아보았다.

https://www.reddit.com/r/dartlang/comments/pfewmi/why_does_map_return_an_iterable_rather_than_a_list/

Iterable은 Lazy Evaluation을 하게 되는데, 이는 결과에 접근하려 할 때 연산이 실행된다는 의미이다. 코드가 있는 위치에 실행 흐름이 가면 실행이 되는게 아니라, Iterable의 값을 사용하려 할 때 그제서야 연산을 처리해 결과를 만들어 반환한다는 의미이다.

요게 꽤나 흥미로웠다!

위 아티클에서는 이런 코드를 예시로 들어줬는데, 이 코드는 map을 사용하는 방식에서는 아래처럼 실행이 된다고 한다.

[1,2,3]
  .map((it) => it + 2)
  .where((it) => it % 3 == 0)
  .map((it) => it - 2)
  .toList();
  
// in map
final result = [];
for (var it in [1,2,3]) {
  it = it+2;
  if (it % 3 == 0) {
    it = it-2;
    result.add(it);
  }
}

여기 코드에서 말하고자 하는 바는 [ map의 결과를 만들어냄 → 거기에서 where 코드를 실행함 → 그 결과에서 map 코드를 적용함 → 여기의 결과를 toList로 List 형태로 만들어냄 ] 의 방식으로 코드가 실행되는게 아니라는 것이다. 오히려, map → where → map 이라는 “연산”을 하나의 형태로 합치고, 이 연산의 결과를 받아 처리하는 toList() 함수가 호출했을 때 연산을 처리한다는 점이다.

만약에 전자처럼 결과를 받아 연산을 하고, 결과를 받아 연산을 하는 형태였다면 아래와 같은 방식으로 구현이 되었어야 한다. 그리고 이 방식이 forEach를 사용하는 방식이다.

var list1 = [];
for (var it in [1,2,3]) {
  list1.add(it+2);
}
var list2 = [];
for (var it in list1) {
  if (it % 3 == 0) {
    list2.add(it);
  }
}
var list3 = [];
for (var it in list2) {
  list3.add(it-2);
}

두 방식의 차이를 봤을 때, 확실히 Iterable을 사용하는 map 방식이 임시 변수나 반복문을 덜 사용하기 때문에 메모리 효율성이나 실행 성능도 우수하다.

그리고 아래 아티클에서는 이런 Iterable의 특성으로 인해 발생할 수 있는 버그에 대해서도 알려주고 있다.

https://fanaro.io/articles/laziness_vs_eagerness_dart/laziness_vs_eagerness_dart.html#12-evaluation-order

Iterable은 결과에 access 하는 시점에 연산이 일어난다고 했는데, 한 번 실행하고 그 결과를 저장해두는 방식이 아니라, 접근할 때마다 새로 연산을 수행하는 형태로 처리된다. 이 때문에, map 코드 내부에 상태를 변경하는 코드가 포함되어있다면, side effect가 발생할 가능성이 높다.

void main() {
  List<int> iList = [1, 2, 3, 4, 5];
  int sum = 0;

  final iter = iList.map((element) {
    print(element);
    sum += element;
    return element + 10;
  });

  print(iter.toString());
  print("sum value is $sum\\n");
  
  print(iter.toString());
  print("sum value is $sum\\n");
  
  print(iter.toString());
  print("sum value is $sum\\n");
}

요 코드를 실행시켜보면, 같은 iter에 대해 toString을 사용할 때 마다 연산이 새로 실행되고, sum 값도 계속 증가해버리는 것을 확인가능하다.

 

 

이렇게 결과에 접근하는 시점에 iterator에 담긴 연산이 수행된다는 점은 잘 이용하면 연산량을 줄이는 유리한 도구로 활용할 수도 있다.

아래 코드는 iterator를 미리 만들어서, map으로 접근해 새로운 list를 만들어내는 코드 블럭을 담아둔 다음, iList의 길이가 5 보다 클 때에만 해당 결과를 list 타입으로 변환해 저장한다고 코드를 작성해준 예시이다.

여기에서는 iList의 길이가 5 이기 때문에, iter의 값이 아닌 [ ] 를 저장하게 되는데, 이 과정에서 iter 값에 접근을 하지 않기 때문에 iterator 를 실행하지 않게 된다. 이러한 점을 활용해 불필요한 연산을 줄여버릴 수 있다! 오호라 😮😮😮

v
oid main() {
  List<int> iList = [1, 2, 3, 4, 5];

  final iter = iList.map((element) {
    print("map executed");
    return element + 10;
  });

  final newList = iList.length > 5 ? iter.toList() : [];
  print("result");
  print(newList);
}

 


정리해보자면!

결국 내가 작성하던 프로젝트 코드에서 map이 실행이 되지 않았던 이유는 그 결과에 접근해 무언가 수행을 해주지 않았기 때문이다. 왜냐하면 map은 iterator를 반환하고, 요 iterator는 lazy evaluation이 되기 때문이다.

final dList = List.generate(7, (index) => false);

// 이건 실행 안됨
vList.map((element) {
  final docsDate = (element의 요일 계산)
  if (요일이 유효한 값이라면) {
    dList[docsDate] = true;
  }
});

return dList;

그리고, 애초에 map에서 내부 상태를 변경하려고 했던 것부터가 문제였다. map은 각 원소를 순회하며 새로운 객체를 만들어낼 때 사용하는 코드이고, forEach는 각 원소를 순회하며 로직을 처리할 때 사용하는 함수이다. 나는 지금까지 forEach로 만들어야 하는 기능을 map을 활용해 만들어오고 있었던 것이다!! 상당히 충격. ㅋㅋㅋ

map과 forEach의 실행 차이에 대해 열심히 구글링을 했을 때에 아래와 같은 글을 찾았었는데, 처음에는 내가 원하는 답이 아니라서 그냥 넘겼었다. 그런데, 다시 보니 여기에 내가 필요한 정보가 다 들어있었던 것 같다.

foreach iterates over a list and performs some operation with side effects to each list member (such as saving each one to the database for example)

map iterates over a list, creates a transformed element for each member of that list, and returns another list of the same size with the transformed elements (such as converting a list of strings to uppercase)

map

  • list를 순회하면서 각 원소에 연산을 적용한 뒤, 이를 담아 반환하는 iterator 객체를 반환함
  • Iterator는 Lazy Evaluation이기 때문에, 결과에 접근하려는 시점에 map 연산이 수행됨
  • map 함수는 기존 list에 대한 새로운 list를 만들어내기 위해 사용하는 메서드임. 따라서, 상태 변경의 목적으로 사용하지 않도록 유의해야함

forEach

  • list를 순회하면서 각 원소에 대해 연산을 적용함
  • forEach는 Eager Evaluation이기 때문에, 코드가 작성된 위치에서 연산이 바로 수행됨
  • forEach 함수는 각 원소를 순회하며 연산을 수행하는 함수임. 따라서 상태 변경이 필요할 때 사용하는 것이 바람직함.

 

 


샤라웃

https://stackoverflow.com/questions/44299842/why-is-map-not-working-but-foreach-is

https://www.reddit.com/r/dartlang/comments/pfewmi/why_does_map_return_an_iterable_rather_than_a_list/

https://fanaro.io/articles/laziness_vs_eagerness_dart/laziness_vs_eagerness_dart.html#12-evaluation-order

320x100
Share Link
reply
반응형
«   2024/06   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30