함수형 프로그래밍이 뭐길래?

leeokmin
DelightRoom
Published in
8 min readApr 6, 2021

(feat. Generics)

스위프트Swift를 다루는 iOS 개발자라면 함수형 프로그래밍 패러다임이라는 개념을 들어보셨을 것입니다. 이 패러다임은 프로그램이 상태의 변화 없이 데이터 처리를 수학적 함수 계산으로 취급하고자 하는 패러다임입니다. 사실 함수형 프로그래밍이 무엇인가는 이미 많은 글들에서 다루었습니다.

저 또한 스위프트 언어를 사용하는 iOS 개발자로서 함수형 프로그래밍 패러다임을 적용하기 위해 노력하고 있는데요. 이 글에서는 무엇인가에 대해서는 살포시 넘어가고 함수형 프로그래밍을 어떻게 사용하는지를 알아봅니다.

간단한 문제

처음은 간단한 문제로 시작합니다.

정수로 이루어진 배열이 주어졌을 때, 각 요소들을 1씩 증가한 새로운 배열을 만들어 반환하는 함수를 작성하라.

이 문제를 해결하는 함수를 작성한다면 아래와 같을 것입니다.

func increment(array: [Int]) -> [Int] {
var result: [Int] = []
for x in array {
result.append(x + 1)
}
return result
}

새로운 result 배열을 만들고 입력 배열의 각 요소들을 1씩 증가해 result 배열에 추가 합니다. 그리고 마지막에 이 배열을 반환하면 됩니다. 간단한 문제죠.

두번째 문제입니다.

정수로 이루어진 배열이 주어졌을 때, 각 요소들을 2씩 증가한 새로운 배열을 만들어 반환하는 함수를 작성하라.

어떻게 풀면 좋을까요? 위와 같은 방법으로 함수를 하나 더 추가하는 것으로 괜찮을까요? 물론 코드가 실행된다는 관점에서는 문제가 없습니다.

하지만 귀찮죠. 핵심입니다. 이런 유형의 문제가 늘어날 때마다 새로운 코드를 작성한다? 상당히 비효율적이다라는 생각이 듭니다.

다행히도 함수형 프로그래밍이 이를 수월하게 만들어 줍니다. 먼저 처음의 코드를 고쳐야겠습니다. 함수형 프로그래밍을 차용해 구현하면 아래와 같습니다.

func compute(array: [Int], transform: (Int) -> Int) -> [Int] {
var result: [Int] = []
for x in array {
result.append(transform(x))
}
return result
}
func increment2(array: [Int]) -> [Int] {
return compute(array: array) { element in element + 1 }
}

compute함수가 생소해 보입니다. 두 번째 인자 transform 이 어색함에 한 역할을 하는 것 같네요. transform 인자는 함수를 인수로 받습니다. 다만 Int타입의 인수를 받아 Int타입을 반환하는 함수만 인수가 될 수 있다는 제약조건이 있습니다. 이런 조건이 있는 함수를 (Int) -> Int 타입 함수라고 표현할 수 있는 것입니다.

increment2 내부에서 compute 를 호출하는 부분도 조금 달라 보입니다. 이를 풀어서 작성하면 다음과 같습니다.

func add(element: Int) -> Int {
return element + 1
}
func increment3(array: [Int]) -> [Int] {
return compute(array: array, transform: add)
}

add 함수는 Int 타입의 인자를 받아 1을 더하고 그 값을 반환합니다. 새로 정의한 increment3 함수 내부에서 이 함수를 인수로 사용하는 것을 확인해 보세요. increment2 함수는 위의 예시를 좀 더 간소하게 작성한 것입니다.

increment2는 아래처럼 더 간략하게 작성할 수도 있습니다.

func increment2(array: [Int]) -> [Int] {
return compute(array: array) { $0 + 1 }
}

인자의 이름을 정의하지 않고 $0 이라고 표현했습니다. 이는 함수의 첫 번째 인자임을 뜻합니다. 첫 번째 인자에 1을 더하라는 의미가 변함 없이 남아 있습니다.(인자의 수가 여러 개라면$1, $2... 와 같이 표현할 수 있습니다.)compute 함수의 첫번째 인자 타입을 정의해 두었으므로 $0Int타입입니다. 실제 프로젝트에는 주로 이렇게 코드를 작성합니다. 익숙해지면 편리하기 때문에 이렇게 작성하는 것을 추천 드립니다.

다시 처음의 물음으로 돌아옵시다.

정수로 이루어진 배열이 주어졌을 때, 각 요소들을 2씩 증가한 새로운 배열을 만들어 반환하는 함수를 작성하라.

이제 이 문제를 해결하는 함수는 아래와 같이 작성할 수 있습니다.

func incrementTwo(array: [Int]) -> [Int] {
return compute(array: array) { $0 + 2 }
}

제네릭Generics을 활용하자

compute 함수를 구현해 필요한 함수를 인수로 적용하고 호출했습니다. 이를 이용해 배열을 이용한 다양한 계산을 할 수 있습니다. 이제 세 번째 문제입니다.

정수로 이루어진 배열이 주어졌을 때, 각 요소들이 짝수일 때는 true, 홀수일 때는 false값을 가지는 새로운 배열을 만들어 반환하는 함수를 작성하라.

어떻게 작성해야 할까요? 반환하는 배열은 [Bool] 타입 이어야 합니다. 이미 작성한 compute 함수를 이용해 아래와 같이 작성할 수 있을 것 같습니다.

func isEven(array: [Int]) -> [Bool] {
return compute(array: array) { $0 % 2 == 0 }
}

안타깝지만 이 코드는 에러가 발생합니다. 두 번째 인자가 (Int) -> Int 타입이기 때문이죠. { $0 % 2 == 0 }(Int) -> Bool 타입이어서 에러가 발생하는 것입니다.

문제를 해결하기 위해 제네릭이라는 기능을 사용합니다. 제네릭은 타입 매개변수를 이용해 여러 타입에 유연할 수 있는 함수나 타입을 만들 수 있습니다. 매우 유용한 기능 입니다. 아래의 genericCompute 함수는 compute 함수를 제네릭을 이용해 다시 구현한 것입니다.

func genericCompute<T>(array: [Int], transform: (Int) -> T) -> [T] {
var result: [T] = []
for x in array {
result.append(transform(x))
}
return result
}

<> 안의 T 가 타입 매개변수 입니다. T 를 무엇으로 하느냐에 따라 다양한 타입의 함수에 유연하게 대응할 수 있습니다. genericCompute 함수를 이용해 지금까지 구현한 함수를 다음과 같이 표현할 수 있습니다.

func increment(array: [Int]) -> [Int] {
return genericCompute(array: array) { $0 + 1 }
}
func incrementTwo(array: [Int]) -> [Int] {
return genericCompute(array: array) { $0 + 2 }
}
func isEven(array: [Int]) -> [Bool] {
return genericCompute(array: array) { $0 % 2 == 0 }
}

조금 더 생각해보면 이 함수가 [Int] , 즉 정수 배열에서만 동작해야할 이유가 없습니다. 이것도 제네릭을 이용해 변경합니다.

func map<Element, T>(array: [Element], transform: (Element) -> T) -> [T] {
var result: [T] = []
for x in array {
result.append(transform(x))
}
return result
}

사실은,,

새로운 함수명 map 을 보니 뭔가 익숙한 느낌인가요? 맞습니다. 우리는 지금까지 스위프트 표준 라이브러리Swift Standard Library에 있는 map 함수를 구현한 것입니다. 사실 더 정확하게는 아래와 같은 형식으로 되어 있습니다.

extension Array {
func map<T>(_ transform: (Element) -> T) -> [T] {
var result: [T] = []
for x in self {
result.append(transform(x))
}
return result
}
}

ArrayElement 를 이미 타입 매개변수로 가지고 있어, T 만 새로 추가했습니다. 실제로 사용할 때는 다음과 같이 사용할 수 있습니다.

let array = [1, 2, 3, 4]array.map { $0 + 1 }
array.map { $0 + 2 }
array.map { $0 % 2 == 0 }

위에서 이야기 했습니다만 map 은 이미 스위프트 표준 라이브러리에 구현되어 있습니다. 그래서 실제로 구현할 필요는 없습니다. 중요한 것은 map 도 함수형 프로그래밍 방법으로 구현되어 있다는 것 그리고 우리는 이미 함수형 프로그래밍 패러다임을 사용하고 있다는 것이죠.

여기까지 map 을 직접 구현하면서 함수형 프로그래밍 패러다임을 활용하는 방법과 제네릭 사용을 살펴봤습니다. 다음에는 더 재밌는 스위프트 관련 글을 준비하겠습니다. 끝까지 읽어 주셔서 고맙습니다!

Smis, Swift man in Seoul

함수형 프로그래밍 패러다임을 스터디하고 실제 코드에 적용할 수 있는 기회를 제공해준 딜라이트룸에 무한 감사를 전하며 이 글을 마칩니다.

iOS 개발자로서 성장할 수 있는 환경을 원한다면! 알라미팀 구경하기

참조

  • Chris Eidhof. ‘Functional Swift.’ Apple Books.
  • 야곰 ‘스위프트 프로그래밍 3판’ 한빛미디어

--

--