본문 바로가기
개린이 이야기

함수형 프로그래밍에 관하여

by iOS 개린이 2022. 10. 9.

Swift는 함수형 프로그래밍을 지향한다. 

따라서, Swift에서 사용되는 함수형 프로그래밍 관점에서함수형 프로그래밍을 알아보자.

함수형 프로그래밍

-프로그램이 상태의 변화 없이 데이터 처리를 수학적 함수 계산으로 취급하고자 하는 프로그래밍 패러다임이다.

-기존 객체지향 프로그래밍이나 명령형 프로그래밍은 프로그램에서 값이나 상태의 변화를 중요하게 여기지만 함수형 프로그래밍은 함수 자체의 응용을 중요하게 여긴다.

-부작용(side-effects)이 없는 디자인을 추구하며, 이는 테스트와 디버깅을 쉽게 하며 코드의 안정성을 높이는 데에 중요한 역할을 한다.

 

 

함수형 프로그래밍을 배워야 하는 이유

1. 동시성 문제

다중 프로세서는 항상 동시성 문제를 가져올 수 있다.

데이터의 상태를 변경하는 객체 지향 프로그래밍 방식은 동시성 문제를 해결하는 데 문제가 많이 발생한다.

하지만 함수형 프로그래밍은 함수가 독립적이고, 외부 상태를 변경하지 않는 불변성을 가지고 있기 때문에 대규모 병렬처리나, 비동기 처리에 더 효율적이다.

 

2. 데이터 관리에 따른 부담

데이터 관리는 프로그래밍의 주요한 측면 중 하나이며, 그 중에서도 대용량 데이터를 효과적으로 처리하는 것은 굉장히 중요한 주제이다.

대용량 데이터를 다루는 작업이 점점 많아지고 있기 때문에

대용량 데이터를 처리할 수 있는 효율적인 데이터 구조와 데이터 연산이 필요하다.

 

객체지향 프로그래밍에서는 동적할당 및 참조 등으로 인한 부담이 있을 수 있다.

객체는 참조에 의해 전달되며, 복사와 달리 메모리의 특정 부분을 가리키는 포인터를 전달한다.

이것은 데이터의 원본이 수정될 가능성이 생기기 때문에 데이터의 불변성을 보장하는 데 문제가 발생할 수 있다.

또한 대용량 데이터를 객체로 변환하는데 부담이 커질 수 있다.

 

그에 반해 함수형 프로그래밍의 불변성은 데이터 안정성을 보장하며,

순수 함수는 데이터 처리 로직을 예측 가능하고,

고차함수는 다양한 데이터 연산을 효과적으로 수행할 수 있도록 만들어준다.

 

3. 함수형 프로그래밍은 모듈화되어 있다.

모듈화란 큰 프로그램을 작고 독립적인 하위 단위로 분리하는 것을 말한다.

이런 하위단위가 별도로 개발, 테스트, 유지보수, 재사용될 수 있어 프로그램의 전체 복잡성을 크게 줄일 수 있다.

 

함수형 프로그래밍에서 함수는 외부의 데이터를 변경하지 않는 불변성, 순수함수 특성으로 인해 함수를 독립적인 단위로 만들 수 있다. 함수가 독립적인 단위라는 것은 외부의 어떤 요소에도 의존하지 않기 때문에 내부적으로 수정이 일어나도 외부에 아무 문제를 일으키지 않는 것이다. 이런 의존성 감소는 모듈화를 용이하게 한다.

 

예를 들어, Swift의 고차함수는 배열이나 딕셔너리 등 컬렉션 타입에 대한 다양한 연산을 수행하는데 재사용할 수 있는 독립적인 모듈이다.

입력을 받고, 결과를 출력하는데 있어 독립적으로 움직일 수 있기 때문에 다양한 문제에 대해 재사용하고 조합할 수 있다.

 

함수형 프로그래밍의 특징

불변성(Immutability)

-불변성은 함수형 프로그래밍에서 프로그램의 예측 가능성과 안정성을 높이는 데 큰 역할을 한다.

 

-불변성은 데이터의 상태를 변경하지 않는다는 뜻으로, 동일한 입력에 대해 동일한 출력을 보장할 수 있다.

이는 디버깅을 용이하게 하며, 생각하지 못한 상태 변경으로 인한 오류를 방지할 수 있다.(side-effects 방지)

 

-또한 불변성은 동시성 프로그래밍에서 동기화 문제를 방지하는데 도움이 된다.

다중 쓰레드 간에 데이터를 공유할 때, 데이터가 변경될 수 있다면 동기화 문제가 발생할 수 있는데, 

데이터의 불변성으로 안전하게 공유할 수 있다.

 

예시 코드

func increaseNumbers(in array: inout [Int]) {
   for i in 0..<array.count {
       array[i] += 1
   }
}

var numbers = [1, 2, 3]
increaseNumbers(in: &numbers) //[2, 3, 4]

 

위 코드에서는 increaseNumbers() 함수는 인자로 들어온 배열의 요소를 직접 수정하여 numbers 배열의 값을 변경한다. 

만약 다른 부분에서 'numbers'에 동시에 접근하려고 한다면, 예기치 못한 결과를 가져올 수 있겠죠?

 

불변성을 유지

func increasedNumbers(from array: [Int]) -> [Int] {
    return array.map { $0 + 1 }
}

let numbers = [1, 2, 3]
let increased = increasedNumbers(from: numbers) //[2, 3, 4]

 

위 코드는 numbers의 불변성을 유지하면서, 원하는 결과값을 얻을 수 있다.

 

 

 

일급 함수(First-class functions)

-일급 함수란 함수를 일급 객체로 설정한다는 것으로 다음과 같은 의미가 있다.

1. 함수를 변수에 할당할 수 있다.

2. 함수를 다른 함수의 인자로 전달할 수 있다.

3. 함수를 다른 함수의 반환값으로 사용할 수 있다.

 

이러한 특성 덕분에 함수를 값(value)처럼 사용할 수 있고, 이를 통해 코드를 좀 더 유연하게 짤 수 있다.

 

-일급객체의 특성으로 함수를 다른 함수의 인자로 전달하거나, 함수의 반환값으로 사용하는 것이 가능하다.

이 특성을 활용하여 고차 함수를 작성하는 것이 가능하다. 

 

 

고차 함수

-고차함수는 다른 함수를 인자로 받거나, 함수를 반환하는 함수를 의미한다.

 

-https://iosjiho.tistory.com/105 - 고차함수에 대해 자세하게 설명한 글.

위 글은 고차함수 map, filter, reduce, flatMap, compactMap 에 대한

정의, 실제 구현부의 코드를 뜯어 해석해보고, 실제활용은 어떻게 하는지, 어떤 상황에 적용하는 것이 좋은지 상세하게 정리한 글이다.

 

Swift에서 고차함수 사용의 이점

1. 코드의 간결성

불필요한 반복적인 로직을 제거하여, 코드를 간결하게 만들어 준다.

 

2. 가독성 향상

각 고차함수의 이름마다 특정 작업을 수행하므로, 코드의 의도를 파악하는데 도움을 준다.

 

3. 재사용성

고차함수는 더 작은, 재사용 가능한 함수로 분리하는 데 도움이 된다.

 

4. 부작용 최소화

고차함수를 사용하면, 새로운 배열을 만들기 위해 외부 변수를 새로 선언하거나, 변경하는 경우를 줄일 수 있다.

따라서 데이터의 불변성을 유지하여, side-effects를 최소화하는 것에 도움이 된다.

 

 

순수함수

-순수함수란 동일한 입력이 주어지면 항상 동일한 출력을 반환하는 함수를 말한다.

출력을 도출해내는데 외부 값을 사용하지 않고, 입력만을 사용하기 때문에 Side-Effects가 발생하지 않는다.

 

-Swift에서 순수함수를 사용한 예로는 고차함수가 있다.

고차함수는 주어진 입력에 대해 항상 동일한 출력을 생성하고, 외부 상태를 변경하지 않는다.

 

-Swift에서 사용하는 순수함수의 예

let array = [1, 2, 3, 4, 5]
let containsThree = array.contains(3) //true

 

'contains' 함수는 입력 배열과 요소에 대해 항상 동일한 결과를 반환하고, 배열의 상태를 변경하지 않는다.

 

 

순수함수 조건

1. output은 input에 의해서만 결정된다.

2. 함수의 수행 과정에서 외부에 있는 값을 사용하지 않는다. (변경이 불가능한 상수는 사용이 가능)

3. 외부의 값을 변경하지 않는다.

 

이렇게 외부의 값이나 상황에 영향을 주지 않기 때문에 side-effect가 발생하지 않는다. (예상하지 못한 오류가 발생할 확률이 적어짐)

 

순수함수 예시

let boyAge : Int = 5
func addAge(girlAge : Int) -> Int {
   
    return boyAge + girlAge
}

 

위코드에서 반환 값으로 외부에 있는 'boyAge' 를 사용했기 때문에 순수함수가 아니라고 생각할 수 있다.

하지만 'boyAge' 가 상수로 정의되어 있기 때문에 변경이 불가능하다.

따라서 'boyAge' 의 불변성으로 인해 순수함수의 조건인" 특정 input에 대해서 항상 동일한 output 도출" 에 타당하다.

 

이렇게 순수함수를 사용하면 코드의 복잡성을 줄이고, 예측 가능성을 높일 수 있다.

 

 

 

함수형 프로그래밍의 한계(개인적인 견해)

-Swift의 고차함수를 학습하면서, 'reduce' 라는 고차함수에 대해서 알게 되었다.

리듀스는 두 가지 형태를 가지고 있는데, 같은 기능을 수행하는 함수가 왜 두 개씩이나 존재해야 할까? 라는 생각에 이유에 대해서 알아보았다.

 

두 형태의 차이는 'inout' 키워드의 사용 여부에 있다.

'inout' 파라미터를 사용하지 않는 리듀스

이 리듀스는 내부적으로 새로운 값을 생성하여 할당하므로, 외부 값에 변경을 주지 않는 '순수함수' 스타일에 가깝다.

하지만 이 방식은 대용량 데이터를 다루는 경우, 비효율적일 수 있다.

왜냐하면 값 타입을 사용하므로, 값이 수정될 때마다 메모리에 새로운 복사본을 생성할 수 있기 때문이다. 

 

'inout' 파라미터를 사용하는 리듀스

반면에 두 번째 리듀스는 참조 타입을 사용하므로, 참조된 값을 직접 수정하고, 메모리에  매번 새로운 값을 할당할 필요가 없다.

 

이런 점에서 Swift에서는 함수형 프로그래밍의 한계를 보완하기 위해 두 가지 형태의 리듀스 함수를 제공하는 것으로 생각된다. 따라서 우리는 함수형 프로그래밍을 사용하면서, 메모리 효율성을 고려하여 최적의 함수를 만드는 것이 중요하다.

 

 

 

 

참고:

야곰님의 스위프트 프로그래밍 3판 

[Swift] 함수형 프로그래밍이란? (tistory.com)