궁금한 내용을 검색해보세요!
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
서근 개발노트
티스토리에 팔로잉
SWIFT/Grammar

Swift : 기초문법 [클로저 및 고차함수(map, filter, reduce)]

서근
QUOTE THE DAY

-
Written by SeogunSEOGUN

반응형

본 게시글은 yagom님의 Swift 프로그래밍 3판을 참고하여 작성되었습니다.

 

클로저

클로저에 대해서 간단하게 예제를 들어보면서 살펴보자.

calculator이라는 함수가 있고, 값을 더하거나 빼거나 곱하는 코드를 작성해보자!

func calculator(n1: Int, n2: Int) -> Int {
    return n1 + n2
}

var makeCalculation = calculator(n1: 2, n2: 5) //7

만약 calculator 함수에 대한 입력을 순서대로 전달하려면 어떻게 해야 할까?

func calculator(n1: Int, n2: Int) -> Int {
    return n1 + n2
}

func add(n1: Int, n2: Int) -> Int {
    return n1 + n2
}

add함수를 만들었고 이 함수를 Calculator함수의 input 매개변수로 넣어주면 된다. input으로 넣기 위해선 add 함수를 요약해야 한다. add함수를 보면 (Int, Int) -> Int 로 요약 가능하다. 이것을 calculator 인풋 매개변수로 넣어주면 끝

func calculator(n1: Int, n2: Int, operation: (Int, Int) -> Int) -> Int {
    return operation(n1, n2)
}

func add(no1: Int, no2: Int) -> Int {
    return no1 + no2
}

func multiply(no1: Int, no2: Int) -> Int {
    return no1 * no2
}

func divide(no1: Int, no2: Int) -> Int {
    return no1 / no2
}

calculator(n1: 4, n2: 2, operation: add) //6
calculator(n1: 10, n2: 2, operation: multiply) //20
calculator(n1: 20, n2: 2, operation: divide) //10

클로저를 만드는 법을 쉽게 보자면

그렇다면 위 예제처럼 add / multiply / divide 함수를 사용하지 않고 클로저를 사용하면 편하겠네..!

func add(no1: Int, no2: Int) -> Int {
    return no1 + no2
}

// 이 함수를

{ (no1: Int, no2: Int) -> Int in
    return no1 + no2
}

// 이렇게 만들고 아래와 같이 호출함
calculator(n1: 4, n2: 2, operation: { (no1: Int, no2: Int) -> Int in
    return no1 + no2
}) //6

두 줄의 코드로 줄일 수 있지만 여전히 복잡하다. 하지만 이 클로저를 또 한 번 줄일 수 있다. 바로 유형추론을 사용하는 것이다.

요약하는 단계

유형 추론을 사용하여 입력값을 지우거나, $ 기호를 사용하여 극단적으로 코드를 줄일 수 있다. 또 클로저 이름을 삭제하고 후행 클로저를 사용하여 줄일 수 도 있다.

TIP
 
 

클로저의 장단점
장점 - 코드를 극적으로 단순화하는 것.
단점 - 기독 성이 떨어짐

고차 함수

맵(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 메서드 연계 사용

 

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

리듀스(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세

 

 

읽어주셔서 감사합니다🤟

 

 

 


잘못된 내용이 있으면 언제든 피드백 부탁드립니다.


서근


위처럼 이미지 와 함께 댓글을 작성할 수 있습니다.