[iOS] 함수형 프로그래밍이란?? (feat. Swift로 알아보는)

 

 

함수형 프로그래밍이 뭔데??

 

 함수형 프로그래밍은 순수 함수와 불변성을 강조하는 프로그래밍 패러다임이라고 한다.

 

순수 함수? 불변성?? 이 부분은 뒤에서 더 자세하게 알아보자.

여하튼 함수형 프로그래밍을 사용하게 되면

코드의 가독성이 높아지고, 유지보수를 용이하게 해준다고 한다.

 

그럼 자세하게 알아보자.

 

 

 

순수 함수

 

 순수 함수는 동일한 입력에 대해 항상 동일한 출력을 반환하며 부작용이 없는 것이라고 한다.

  • 받은 인자 외에 다른 외부의 상태에 영향을 끼치지 않고 리턴값 외에는 외부와 소통이 없다.
  • 순수함수로 함수형 프로그래밍을 할 경우 오류를 줄이고 안정성을 높인다.

 

func plus(a: Int, b: Int) -> Int {
	return a + b
}

 

 예를 들어 위와 같은 함수가 있다고 해보면, a = 1, b = 2가 들어온다면 언제나 결과 값은 3으로 같다.

 

 

그럼 순수 함수가 아닌 예는 무엇일까?

 

1. 동일한 인자를 주었을 때 상황에 따라 결과가 달라지는 함수

 -> c의 값이 바뀌는 경우 결과가 달라지게 되기에 순수 함수가 아니다.

var c = 20

func plus(a: Int, b: Int) -> Int {
	return a + b + c
}

 

2. 사이드 이펙트가 발생하는 경우

 -> 함수 밖의 변수인 c 값을 변화시킬 수 있다. (영향을 줄 수 있다)

var c = 20

func plus(a: Int, b: Int) -> Int {
	c = b
	return a + b
}

 

 

 

그래서 순수 함수는 왜 필요한데??

 

 순수 함수를 사용하면 코드의 예측 가능성안전성이 올라가게 된다.

또 외부 상태에 의존하지 않기 때문에 Unit Test에 용이하고,

테스트 코드의 작성도 간단해진다.

 

그리고 부작용이 없기에 다중 스레드 환경에서 병렬 처리가 가능하고

결합도가 낮아 코드 리팩토링에도 용이하다.

(추측하건대 외부 값을 건드리게 된다면, 다중 스레드 환경에서 Race Condition이 발생할 것 같다..)

(Race Condition이 무엇인지 모른다면.. 아래 글을 참조해보자)

 

 

[운영체제] 동시성(Concurrency)

1. 동시성이란?여러 작업을 동시에 실행되는 것처럼 보이게 하는 것을 말한다.  2. 프로그램 / 프로세스 / 스레드동시성에 대해 자세하게 알아보기 앞서 알아둬야하는 개념들이 있다.정의프로그

d0ngurrrrrrr.tistory.com

 

 

 

 

 

불변성

 

 불변성은 데이터의 변경 불가능성을 의미하며,

데이터가 한 번 생성되면 그 값을 변경할 수 없게 만드는 것을 의미한다.

 

// 불변성을 유지하는 코드
let count = 0

func plusOne(value: Int) -> Int {
     return value + 1
}

let newCount = plusOne(value: count)
print(count)    // 0
print(newCount) // 1

 

let value = 10
// value = 20 // 에러: 상수이기에 변경 불가


struct Point {
	let x: Int
    let y: Int
}

var myPoint = Point(x: 1, y: 10)
// myPoint.x = 10 // 에러: x는 상수이기에 변경 불가

 

 

 

불변성은 또 왜 필요한데??

 

 불변성을 유지하는 것 역시 코드의 안정성을 높이고, 예측 가능성을 높여준다. 순수 함수와 마찬가지로 다중 스레드 환경에서 불변성을 유지하면 동시성 문제를 방지할 수 있고, 코드의 유지보수성과 확장성을 향상시킬 수 있다.

 

 

 

1급 객체

 

 함수형 프로그래밍에서 함수는 1급 객체가 된다고 한다.

 

그래서.. 1급... 객체??? 가 뭔데?

 

1급 객체의 조건을 만족한다면 일급 객체라고 할 수 있다고 한다.

1. 객체를 변수상수저장과 할당이 가능해야 한다.

2. 객체를 반환 값으로 사용할 수 있어야 한다

3. 객체를 함수의 매개변수로 사용이 가능해야 한다.

 

 

위의 조건들을 예시로 표현해 보자면

 

 

 

 

1. 객체를 변수나 상수에 저장과 할당이 가능해야 한다.

func getAge() -> Int {
	return 20
}
var myAge = getAge()
print(myAge) // 20

//할당
var inputAge = getAge
print(inputAge) // (Function)

var yourAge = inputAge()
print(yourAge) // 20

 20을 리턴하는 메서드를 myAge 변수로 대입하면 출력값은 20이 나온다.

또 inputAge라는 변수에 getAge를 할당하고

이를 yourAge에 inputAge()로 실행시키면 같은 값이 나오게 된다.

 

 

2. 객체를 반환 값으로 사용할 수 있어야 한다.

func printName(_ name: String) -> String {
	return "내 이름은 \(name)"
}

func getName(nameValue: (String) -> String, name: String) -> String {
	return nameValue(name)
}
var result = getName(nameValue: printName, name: "그리드")
print(result) // 내 이름은 그리드

 위 코드를 보면 var result = getName에서 nameValue의 매개변수로 printName() 함수를 사용했다.

이처럼 함수 내에서 추가적인 로직을 만들고자 할 때,

함수를 인자로 받아서 사용할 수 있어야 한다.

(뭔가 테스트하기 좋은 코드인 거 같다..)

 


3.
객체를 함수의 매개변수로 사용이 가능해야 한다.

func likeFood(_ name: String) -> String {
	return "최애 음식, \(name)"
}

func hateFood(_ name: String) -> String {
	return "손서리치는 음식, \(name)"
}

func foodCase(foodName: String) {
	switch foodName {
    case "피자":
    	return likeFood(foodName)
    case "생선":
    	return hateFood(foodName)
    default:
    	return "나두 몰루.."
    }
}

var pizza = foodCase(foodName: "피자") // 최애 음식, 피자
var fish = foodCase(foodName: "생선") // 손서리치는 음식, 생선

 

추가적인 로직을 만들고자 할 때 사용이 가능하지만, case 별로 여러 함수를 사용할 수 있다.

 

 

고차 함수

 함수를 파라미터로 받거나 함수를 리턴하는 함수를 고차함수라고 한다.

Swift에는 대표적인 고차함수가 몇 개 있는데, map, filter, reduce이다.

이런 고차 함수는 함수형 프로그래밍의 핵심 개념을 구현한다.

즉, 함수를 인자로 받아서 컬렉션의 요소를 변환하거나 필터링하거나 결합할 수 있다.

 

또 이런 고차 함수들은 기존 컬렉션을 변경하지 않고(불변성),

새로운 컬렉션이나 값을 생성한다.

그리고 고차 함수 내부에서 사용되는 함수들은 일반적으로 부작용이 없는 순수 함수이다.

 

 

 이렇게 고차 함수의 특징을 보았을 때, 떠오르는 것이 있다.

바로 함수형 프로그래밍의 정의다.

앞서서 함수형 프로그래밍은 순수 함수와 불변성을 강조하는 프로그래밍 패러다임이라고 하였다.

그렇기에 고차 함수는 함수형 프로그래밍의 주요 원칙들을 잘 구현하고 있고, 그렇기에 밀접한 관계가 있다.

 

그래서 고차 함수가 왜 필요한데??

 

 고차 함수를 사용하게 되면 코드가 간결해져서 이해하기가 쉬워지고,

재사용성이 높아지며 함수형 프로그래밍의 장점을 최대한 발휘할 수 있다.

그리고 Swift에서는 map, filter, reduce와 같은 고차 함수를 사용하는 것을 적극 권장하고 있다고 한다.

 

 

 

선언적 프로그래밍

 

 함수형 프로그래밍은 선언적 프로그래밍 중 하나로 꼽힌다고 한다.

기존 명령형 프로그래밍이 어떻게(How) 할 것인가에 집중한다면,

선언적 프로그래밍은 무엇을(What) 할 것인가에 집중한다.

 

 

명령형 프로그래밍

var sum = 0
for i in 1...5 {
	sum += i
}
print(sum) // 15

 

 명령형 프로그래밍에서는 루프와 변수를 사용해 명시적으로 계산의 흐름을 제어하고 있다.

 

 

선언형 프로그래밍

let numbers = [1,2,3,4,5]
let sum = numbers.reduce(0, +)
print(sum) // 15

 

 반면 선언형 프로그래밍은 루프나 변수 조작 없이,

간단히 reduce 함수를 사용해 결과값을 얻어냈다.

 

 이 예시를 통해 선언형 프로그래밍을 사용하게 되면

어떻게 계산할지에 대한 세부 사항을 숨겨 추상화 수준을 높이고,

코드의 가독성과 유지보수성을 높일 수 있다는 것을 알 수 있다.

 

또 명령형은 대입(변수에 값을 대입하여 상태를 변경 ex. sum = sum + 1 )을 사용하지만

선언형은 대입을 사용하지 않는다는 것에서 차이가 난다.

 

 

 

함수형 프로그래밍의 장/단점

장점

  • 코드의 간결성: 고차 함수 등의 개념을 통해 코드를 간결하게 작성 가능
  • 디버깅 용이성: 함수는 입력에만 의존하고 외부 상태에 영향을 주지 않아 함수 간의 의존성이 낮아져 디버깅이 쉬워짐
  • 테스트 용이성: 불변성과 순수 함수의 특성으로 인해 함수형 코드는 단위 테스트 작성이 용이
  • 병렬 프로그래밍: 부작용이 적고 상태 변이가 없는 특성 때문에 병렬 및 분산 환경에서 작업하기가 쉬워짐
  • 재사용성: 높은 수준의 추상화와 모듈화를 제공하므로 코드를 재사용하기가 편리해짐. (각 함수는 독립적이어서, 작은 함수들을 결합하여 더 큰 기능을 구성 가능)

단점

  • 러닝커브: 명령형 프로그래밍과는 다른 개념을 도입하고 있어 학습 곡선이 높을 수 있음, 또 순수 함수를 조합하는 것은 어려울 수 있음
  • 성능 문제: 불변성을 유지하기 위해 데이터를 복사하는 경우가 있어 성능 문제가 발생할 수 있음. 그러나 최적화 기술과 언어의 발달로 이 문제는 감소. 재귀함수를 사용할 때 무한 루프에 빠져 스택 오버플로우의 문제 발생할 수 있음

 

 

 

참고 블로그