VesselWheel

고차함수(Map, filter, reduce) 본문

Xcode Study

고차함수(Map, filter, reduce)

JasonYang 2023. 11. 21. 15:18

고차 함수

맵(map)

  • map은 자신을 호출할 때 매개변수로 전달된 함수를 실행하여 그 결괏값을 다시 반환해주는 함수이다.
  • map을 사용하기 위해서는 Swift의 Collection, Sequence 프로토콜을 따르면 가능하다. 따라서 Array, Dictionary, Set, optioanl 등에서 사용이 가능
  • map을 사용하여도 기존의 컨테이너의 값은 변경되지 않고 새로운 컨테이너가 생성되어 map은 기존 데이터를 변형하는데 많이 사용된다.
  • map은 다른 함수의 형태로 입력을 받는다. 

map 메서드와  for-in 구문의 차이점은 코드의 재사용이나 컴파일러 최적화 성능 차이이다.

또, 다중 스레드 환경일 때 대상 컨테이너의 값이 스레드에서 변경되는 시점에 다른 스레드에서도 동시에 값이 변경되려고 할 때 예측하지 못한 결과가 발생하는 부작용을 방지한다.

  let item = ["가방", "책", "블로그", "지갑"]
   
  func addName(name: String) -> String {
  return "서근의 " + name
  }
   
  item.map(addName)

이것을 클로저로 바꾸면 아래와 같다.

  let item = ["가방", "책", "블로그", "지갑"]
   
  let first = item.map{ (name) in "서근의 " + name }
  print(first)
   
  let second = item.map({(name) in "서근의 " + name})
  print(second)
   
  let third = item.map {"서근의 " + $0}
  print(third)

정수를 문자열로 바꿀 수 있다.

  let number = [1, 2, 3, 4, 5]
   
  print(number.map{$0 + 1})  //[2, 3, 4, 5, 6]
   
  let newArray = number.map{"\($0)"} //Int타입을 String 타입으로 변환 가능
  print(newArray) //["1", "2", "3", "4", "5"]

map 메서드와 for-in 구문

for-in 구문과 map 메서드 사용을 비교해보자면 아래와 같다.

  let numbers: [Int] = [0, 1, 2, 3, 4]
   
  var doubledNumbers: [Int] = [Int]()
  var string: [String] = [String]()
   
  for number in numbers {
  doubledNumbers.append(number * 2)
  string.append("\(number)")
  }
  print(doubledNumbers) //[0, 2, 4, 6, 8]
  print(string) //[0, 2, 4, 6, 8]
   
   
  //map 메서드
  doubledNumbers = numbers.map({ (number: Int) -> Int in
  return number * 2 //[0, 2, 4, 6, 8]
  })
  string = numbers.map({ (number: Int) -> String in
  return "\(number)"  //["0", "1", "2", "3", "4"]
  })

map 메서드 동작 모식도

map 메서드를 사용하면 for-in 구문을 사용한 것보다 간단하고 편하게 연산을 실행할 수 있다. 또, map 메서드를 사용하면 for-in 구문을 사용하기 위해 빈 배열을 생성할 필요도, append 연산을 실행할 시간도 필요가 없어진다.

위 코드에서 사용된 map 메서드를 클로저 표현으로 요약할 수 있다.

  let numbers: [Int] = [0, 1, 2, 3, 4]
   
  //map 메서드
  doubledNumbers = numbers.map({ (number: Int) -> Int in
  return number * 2 //[0, 2, 4, 6, 8]
  })
  string = numbers.map({ (number: Int) -> String in
  return "\(number)"  //["0", "1", "2", "3", "4"]
  })
   
  //유형 추론으로 요약 가능
  doubledNumbers = numbers.map({ (number) in
  return number * 2 //[0, 2, 4, 6, 8]
  })
  string = numbers.map({ (number) in
  return "\(number)"  //["0", "1", "2", "3", "4"]
  })
   
  //매개변수 및 반환 타입 생략
  doubledNumbers = numbers.map({return $0 * 2})
  string = numbers.map({return "\($0)"})
  //반환 키워드 생략
  doubledNumbers = numbers.map({$0 * 2})
  string = numbers.map({"\($0)"})
   
  //후행 클로저로 요약 가능
  doubledNumbers = numbers.map { $0 * 2 }
  string = numbers.map { "\($0)"}

위에서 'map 메서드와  for-in 구문의 차이점은 코드의 재사용이나 컴파일러 최적화 성능 차이이다.'라고 언급했었는데, 코드의 재사용 측면에 대해 알아보자면 만약 같은 기능을 여러 번 사용해야 한다면 하나의 클로저를 여러 map 메서드에서 사용하는 것이 좋다.

  let evenNumbers: [Int] = [0, 2, 4, 6, 8, 10]
  let oddNumbers: [Int] = [0, 1, 3, 5, 7, 9]
  let multiplyTwo: (Int) -> Int = { $0 * 2 }
   
  let doubledEvenNumbers = evenNumbers.map(multiplyTwo)
  //[0, 4, 8, 12, 16, 20]
  let doubledOddNumbers = oddNumbers.map(multiplyTwo)
  //[0, 2, 6, 10, 14, 18]

다양한 컨테이너 타입에서의 map 활용

  let alphabetDictionary: [String: String] = ["a":"A", "b":"B"]
   
  var keys: [String] = alphabetDictionary.map { (tuple: (String, String)) -> String in
  return tuple.0
  }
  print(keys) //["b", "a"]
   
  keys = alphabetDictionary.map { $0.0 }
  print(keys) //["a", "b"]
   
  keys = alphabetDictionary.map({$0.1})
  print(keys) //["A", "B"]
   
  let value: [String] = alphabetDictionary.map{ $0.1 }
  print(value) //["A", "B"]
   
  var numberSet: Set<Int> = [1, 2, 3, 4]
  let resultSet = numberSet.map { $0 * 2 }
  print(resultSet) //[2, 8, 4, 6]
   
  let optionalInt: Int? = 2
  let resultInt = optionalInt.map { $0 * 3 }
  print(resultInt) //Optional(6) - error : 타입캐스팅에서 다루도록 함
   
  let range: CountableClosedRange = (0...5)
  let resultRange: [Int] = range.map { $0 * 5 }
  print(resultRange) //[0, 5, 10, 15, 20, 25]

위 코드에서 optional 쪽이 오류가 났는데 이 부분은 타입 캐스팅 부분에서 자세히 다뤄보겠습니다.

필터(filter)

  • filter는 내부 값을 걸러서 추출하는 역할을 한다.
  • map과 동일하게 새로운 컨테이너에 걸러진 값을 담아 반환한다.
  • map은 기존의 요소를 변경한 값을 반환했다면, filter는 기준을 가지고 기준에 맞는 값들을 반환해준다.
  • filter 함수의 매개변수로 전달되는 함수 반환 타입은 Bool 이다.
  • 새로운 컨테이너에 포함될 항목이라고 판단되면 true, 그게 아니라면 false 를 반환
  let number = [1, 2, 3, 4, 5]
  print(number.filter {$0 > 3}) //4, 5
   
  //필터 조건이 맞다면 map조건을 실행
  let filterAndMap = [1, 2, 3, 4, 5].filter{$0 > 3}.map{$0 * 10}
  print(filterAndMap) //40, 50

이런 식으로 filter를 사용하여 필요 없는 요소들을 삭제하고 필요한 요소들만 가지고 연산을 하는 것이 가능하다.

  let numbers: [Int] = [0, 1, 2, 3, 4, 5]
   
  var evenNumber: [Int] = numbers.filter { (number: Int) -> Bool in
  return number % 2 == 0
  }
  print(evenNumber) // [0, 2, 4]
   
  let oddNumbers: [Int] = numbers.filter { $0 % 2 == 1 }
  print(oddNumbers) // [1, 3, 5]

콘텐츠의 변형 후 필터링이 필요하다면 아래 코드와 같이 map을 사용 후 필터 메서드를 호출할 수 있다.

  let numbers: [Int] = [0, 1, 2, 3, 4, 5]
  let mappedNumber: [Int] = numbers.map{ $0 + 3 }
   
  let evenNumber: [Int] = mappedNumber.filter { (number: Int) -> Bool in
  return number % 2 == 0
  }
  print(evenNumber) //[4, 6, 8]
   
  //mappedNumber 프로퍼티가 굳이 필요하지 않다면 메서드를 체인처럼 연결해 사용 가능
  let oddNumbers: [Int] = numbers.map{$0 + 3}.filter { $0 % 2 == 1 }
  print(oddNumbers) //[3, 5, 7]

↪︎  map과 filter 메서드 연계 사용

map과 filter를 연계하여 사용하면 복잡한 연산을 쉽게 해결할 수 있다.

리듀스(reduce)

  • reduce는 줄이다 라는 뜻이지만, 결합 기능을 하는 메서드이다.
  • 컨테이너의 내부의 요소들을 하나로 합치는 기능을 하는 고차 함수이다.
  • 배열의 모든 값을 전달 인자로 전달받아 클로저의 연산 결과로 합해주게 된다.

Swift에서의 reduce 형태 (두 가지)

  • 첫 번째, 클로저가 각 요소를 전달받아 연산한 후 값을 다음 클로저 실행을 위해 반환하며 컨테이너를 순환하는 형태. initialResult 라는 이름의 매개변수로 전달되는 값을 통해 초깃값을 지정하고, nextPartialResult 매개변수로 클로저를 전달받음. 
  • 두 번째, 컨테이너를 순환하며 클로저가 실행되지만 클로저가 따로 결괏값을 반환하지 않는 형태. 대신 inout 매개변수를 사용하여 초깃값에 직접 연산을 실행함
  let number = [1, 2, 3, 4, 5]
   
  let sum1 = number.reduce(0) { (result:Int, element: Int) -> Int in return result + element }
  print(sum1) //15
   
  //추론으로 생략 가능
  let sum2 = number.reduce(0) { (result, element) in result + element }
  print(sum2) //15
   
  let sum3 = number.reduce(0) {$0 + $1}
  print(sum3) //15
   
  let sum4 = number.reduce(1, +)
  print(sum4) //16
   
  /*
  reduce 초기값이 0이기 때문에 0 + 1 부터 시작하여 마지막 값을 결괏값으로 보여준다.
  0 + 1
  1 + 2
  3 + 3
  6 + 4
  10 + 5
  결괏값 = 15
  */

만일 initial(초기값) 값이 1이라면 초기 항목은 {1 + 1} 이다. 클로저는 이전 결과와 다음 항목을 계속 호출하여 다음과 같은 과정을 거쳐 하나의 값을 얻게 된다. {1 + 1}, {2 + 2}, {4 + 3}, {7 + 4}, {11 + 5}이며, 결과는 16이 된다.

map, filter, reduce 활용

map, filter, reduce를 활용해서 특정 조건을 출력하는 코드를 만들어 보려고 한다. 먼저 Friend 구조체에 친구의 정보를 담을 저장 프로퍼티를 생성하고 Gender 배열을 정의해 성별을 담아둔다. 그리고 배열 friends 인스턴스를 생성한다.

  enum Gender: String {
  case male = "남자"
  case female = "여자"
  case unknow = "미상"
  }
   
  struct Friend {
  let name: String
  let gender: Gender
  let location: String
  var age: UInt
  }
   
  var friends: [Friend] = [Friend]()

친구들의 정보를 아래 코드처럼 정의해 주는데 현재 나이 + 1 을 해주고, 조건은 "미국에 거주하지 않는 20세보다 작거나 같은 남성을 찾는다."인 사람을 찾으려고 한다.

  friends.append(Friend(name: "서근", gender: .male, location: "러시아", age: 20))
  friends.append(Friend(name: "철수", gender: .male, location: "한국", age: 15))
  friends.append(Friend(name: "민지", gender: .female, location: "미국", age: 22))
  friends.append(Friend(name: "훈이", gender: .male, location: "대전", age: 13))
  friends.append(Friend(name: "영미", gender: .female, location: "서울", age: 31))
  friends.append(Friend(name: "찰스", gender: .male, location: "미국", age: 11))
  friends.append(Friend(name: "후산", gender: .male, location: "우즈베키스탄", age: 17))
  friends.append(Friend(name: "하산", gender: .female, location: "카자흐스탄", age: 20))

이제 map으로 현재 나이에 + 1을 더해 Friend 배열을 생성한다.

  var result: [Friend] = friends.map { 
  Friend(name: $0.name, gender: $0.gender, location: $0.location, age: $0.age + 1)
  }

그리고 filter 메서드로 미국에 거주하지 않고 20세보다 작거나 같은 남성을 걸러주고, reduce메서드로 필터링해준다.

  result = result.filter { $0.location != "미국" && $0.gender == .male && $0.age <= 20 }
   
  let string: String = result.reduce("미국에 거주하지 않는 20세보다 작거나 같은 남성을 찾는다.") { 
  $0 + "\n" + "이름:\($1.name) | 거주지:\($1.location) | 성별:\($1.gender.rawValue) | 나이:\($1.age)세"
  }
   
  print(string)
   
  //미국에 거주하지 않는 20세보다 작거나 같은 남성을 찾는다.
  //이름:철수 | 거주지:한국 | 성별:남자 | 나이:16세
  //이름:훈이 | 거주지:대전 | 성별:남자 | 나이:14세
  //이름:후산 | 거주지:우즈베키스탄 | 성별:남자 | 나이:18세

전체 코드로 보면 다음과 같다.

  enum Gender: String {
  case male = "남자"
  case female = "여자"
  case unknow = "미상"
  }
   
  struct Friend {
  let name: String
  let gender: Gender
  let location: String
  var age: UInt
  }
   
  var friends: [Friend] = [Friend]()
   
  friends.append(Friend(name: "서근", gender: .male, location: "러시아", age: 20))
  friends.append(Friend(name: "철수", gender: .male, location: "한국", age: 15))
  friends.append(Friend(name: "민지", gender: .female, location: "미국", age: 22))
  friends.append(Friend(name: "훈이", gender: .male, location: "대전", age: 13))
  friends.append(Friend(name: "영미", gender: .female, location: "서울", age: 31))
  friends.append(Friend(name: "찰스", gender: .male, location: "미국", age: 11))
  friends.append(Friend(name: "후산", gender: .male, location: "우즈베키스탄", age: 17))
  friends.append(Friend(name: "하산", gender: .female, location: "카자흐스탄", age: 20))
   
  var result: [Friend] = friends.map { Friend(name: $0.name, gender: $0.gender, location: $0.location, age: $0.age + 1)}
   
  result = result.filter { $0.location != "미국" && $0.gender == .male && $0.age <= 20}
   
  let string: String = result.reduce("미국에 거주하지 않는 20세보다 작거나 같은 남성을 찾는다.") { $0 + "\n" + "이름:\($1.name) | 거주지:\($1.location) | 성별:\($1.gender.rawValue) | 나이:\($1.age)세"}
   
  print(string)
  //미국에 거주하지 않는 20세보다 작거나 같은 남성을 찾는다.
  //이름:철수 | 거주지:한국 | 성별:남자 | 나이:16세
  //이름:훈이 | 거주지:대전 | 성별:남자 | 나이:14세
  //이름:후산 | 거주지:우즈베키스탄 | 성별:남자 | 나이:18세

'Xcode Study' 카테고리의 다른 글

Xcode의 StoryBoard가 xml 화면으로 나올 때  (0) 2023.11.24
UITableViewCell  (0) 2023.11.24
sqlite3 설치 및 데이터 생성  (0) 2023.10.26
서버 구축 vapor  (0) 2023.10.26
assert / guard  (0) 2023.10.19