Swift로 해보는 함수형 프로그래밍 실습

Youth·2024년 11월 24일
0

TIL

목록 보기
21/21

안녕하세요 오랜만에 인사드리는 킴스캐슬입니다
어떤 글을 쓸까 고민을 하다가 최근에 관심이 있는 함수형 프로그래밍에 대해서 적어보려 합니다

함수형프로그래밍의 기초 개념 정리

요즘 함수형프로그래밍이라는걸 알게되었는데 함수를 자유자재로 다루는 코드를보면서 좀 멋있다고 생각하기도했고 클로저를 잘쓰고싶은 마음이 드는요즘이어서 그런가 이 패러다임을 배워보고 싶었습니다. 하지만 이걸 왜 써야하는지를 이해하지못하고 사용하는건 의미가 없을거같아서 함수형 프로그래밍(FP라고하겠습니다)이 왜 등장했고 왜 필요했고 본격적으로 다루기전에 FP에서 함수를 다루는 기본개념을 공부하고 정리해봤습니다!

FP가 재조명 받는 이유

  • High Memory
  • Performance / Responsibility
  • Concurrency

→ 패러다임이 기존의 재사용의 관점에서 동시성으로 이동하고있음

동시성프로그램의 경우에 데이터에 동시에 접근하는경우 문제가 발생함

→ 이문제를 해결하려면 데이터를 조작하지 않으면된다
→ 한번만들어진 데이터는 변경하지 않고 변경된 데이터가 필요할때는 새로운데이터를 만들어낸다
→ 기존의 데이터가 변경되지 않으니까 같은 데이터를 사용하는, 동시에 수행되는 다른프로그램에 서로 영향을 주지 않는다

💡 “외부에영향을 주지않는다 = side effect가 없다”라고 표현한다.
side effect가 없도록 프로그래밍을 함으로써 동시성 문제를 해결할수있다라는 공감대가 형성됨, 이러한 관점이 기존의 함수형프로그래밍과 닮았다.


Pure Function

순수함수란?

→ 특정 input에 대해서 항상 동일한 output을 반환하는 함수

  • output을 만드는데 input만을 사용한다는 의미이므로, 함수외부의 값을 사용하지않아 side-effect가 없다는 뜻이됩니다
var name = "FP"
var greeting = ""
func makeGreeting() {
	greeting = "Hello, \(name)"
}

위와 같은 경우 함수 makeGreeting()이 외부변수인 name과 greeting에 의해 영향을 받는다. 즉, side effect가 존재한다는 뜻이됩니다

이를 순수함수(pure function)으로 바꾸면

func greeting(_ name: String) -> String {
	return "Hello, \(name)"
}

이렇게 바꿀수있다. 함수의 수행결과는 오로지 input인 name에 의해서만 결정되고, 외부에 영향을 전혀받지 않는다. 어떠한 상황에서든 특정입력밧에 대해 항상 동일한 결과를 얻을 수 있습니다

그렇다면 여기서 한가지 의문점이 생기는데 “외부변수를 사용한다고 해서 무조건 순수함수가 될수 없는걸까?”

let greet = "Hello"
func greeting(_ name: String) -> String {
	return "\(greet), \(name)"
}

위 예시를 보면 분명히 함수의 output에 외부변수인 greet에 의해 영향을 받는거같지만 greet은 let으로 정의되어있어 변경이 불가능한 immutable data입니다. 그리고 외부변수가 immutable인 경우 이함수도 특정 input에 대해 항상 동일한 output을 내기 때문에 순수함수입니다

그렇다면 “input이 있거나 immutable한 외부변수만을 사용한다고 해서 무조건 순수함수일까?”

func add(_ a: Int) -> Int {
	return Int(arc4random()) + a
}

위와같은 예시를보면 input이 외부변수에 영향을 받지않지만 내부에서 랜덤값을 만들어서 return해주고있습니다. 즉, input이 동일하더라도 매번 다른 결과를 얻기때문에 순수함수가 될 수 없다고 할 수 있죠

직접 순수함수로 변경하는 대표적인 예제를 한가지 살펴보면

var sum = 0
func solution(_ nums: [Int]) -> Int {
    for i in nums {
        sum += i
    }
    return sum
}

위 예제에서 solution이라는 함수를 pure function으로 변경하려면 sum이라는 외부변수(var로 선언되어있어서 immutable하지 않다)를 없애야합니다

하지만 이 sum을 let으로 바꿔서 immutable하게 바꾸자니 값이 변하기때문에 변수로 선언해야해서 그렇게 할수가 없죠. 그래서 가장 쉬운방법은 sum을 함수내부에 넣는겁니다

func solution(_ nums: [Int]) -> Int {
    var sum = 0
    for i in nums {
        sum += i
    }
    return sum
}

Higher-Order Function

함수형 프로그래밍에선 함수를 1급객체로 취급합니다

1급객체란?

→ 프로그래밍 언어에서 함수의 파라미터로 전달되거나 리턴값으로 사용될 수 잇는 객체

  • 예시 : filter, map, reduce etc…

filter를 예로들면

func filter(_ isIncluded: (Element) throws -> Bool) rethrows -> [Element]

filter함수는 파라미터의 값으로 element를 input으로하고 bool 값을 return하는 함수를 받는다 → 이 함수는 함수를 파라미터로 받아들이는 고차함수라고 할 수 있습니다

함수를 반환하는 함수

  1. 첫번째 예시

    1. 함수를 새로 만들어서 그함수를 반환하는 경우
    // 이 함수는 a라는 input을 받아서 int를 input으로 받아서 input를 output으로하는 함수를 리턴하는 함수이다
    // multiply라는 함수에 a를 넣어줘야 함수가 리턴되고 그 리턴된 함수에 input으로 b를 넣어줘야 결과값이 리턴된다
    func multiply(_ a: Int) -> (Int) -> Int {
        func multi(_ b:Int) -> Int {
            return a * b
        }
        return multi
    }
    
    // a에 10을 input으로 넣어줘서 multiply(10)은 retun된 함수를 의미하고
    // 함수의 실행을 한번더하면 multiply(10)()인데 input으로 b의 값을 넣어줘야하므로 multiply(10)(20)이고
    // 두개를 곱한 값을 리턴하는 함수를 리턴해서 실행했으니까 결과값인 200이 나온다
    let area = multiply(10)(20) //200
  1. 두번째 예시

    1. 클로저를 반환하는 경우
    // 애초에 multiply라는 함수는 함수를 리턴하는 함수인데 리턴하는 함수가 int를 받아서 int를 리턴하는 함수면
    // 함수를 이름붙여서 만들지 않고 클로저로 만들어서 리턴하는것도 하나의 방식일수 있음
    // closure인데 b를 input으로 밭아서 int를 return해주는 방식으로 구현
    func multiply(_ a: Int) -> (Int) -> Int {
        return { b in
            return a * b
        }
    }
    
    // x10에는 클로저가 담겨져있음(b를 받아서 a랑곱해서 리턴하는 클로저)
    let x10 = multiply(10)
    
    // 클로저는 결국 함수의 실행이므로 괄호한에 input인 b를 넣어주면된다
    let area2 = x10(20)

함수의 합성(Composition)

함수의 반환값이 다른함수의 입력값으로 사용되는것

  • 함수의 반환값과 이것을 입력으로 받아들이는 값은 타입이 서로 같아야합니다
    func f1(_ i: Int) -> Int {
        return i * 2
    }
    
    func f2(_ i: Int) -> String {
        return "\(i)"
    }
    
    let result = f2(f1(100))

커링(Currying)

여러개의 파라미터를 받는 함수를 하나의 파라미터를 받는 여러개의 함수로 쪼개는 것을 커링이라고합니다

아래와 같이 파라미터를 두 개 받는 함수가 있을때 이것을

func f(_ a: Int, _ b: Int) -> Int

아래와 같이 파라미터 하나를 받는 함수 두개로 쪼개는 방식입니다

func multiply(_ a: Int) -> (Int) -> Int {
    return { b in 
        return a * b
    }
}
let area = multiply(10)(20) //200

→ 뜻을 해석해보면 a라는 int를 받아서 int를 받아서 int를 리턴해주는 함수를 리턴해주는 함수라고 해석하면됩니다

→ 함수의 합성을 원활하게 하기 위해서 커링을 사용한다!

예를들어서 n의 배수만을 모아 합을 구하는 함수는 보통 아래와같이 이렇게 구현합니다

func filterSum(_ ns: [Int], _ n: Int) -> Int {
    return ns.filter({ $0 % n == 0 }).reduce(0, +)
}

어쨋든 이 함수도 input 파라미터가 두개이므로 이걸 하나씩 쪼개는 currying을하면

func filterSum2(_ n: Int) -> ([Int]) -> Int {
    return { z in
        return z.filter({ $0%n == 0 }).reduce(0, +)
    }
}

🔥공부하다가 헷갈렸던 함수해석하는방법

func sum(_ num: Int) -> (Int) -> (Int) -> Int {
    return { y in
        return { z in
            return num + y + z
        }
    }
}

위와같은함수를 해석할때 어디가 인풋이고 어디가 아웃풋인지를 해석하기가 까다로워서 이런 함수가 보여도 쫄지않게 해석하는 방법을 보면

  1. 우선 sum이라는 함수는 num을 받아서 함수(1)를 리턴한다라고 해석
    1. 근데 그 함수(1)을 클로저로 표현한 상태(y라는 int를 받아서 함수(2)를 리턴)
  2. 근데 그 함수(1)는 int를 받아서 함수(2)를 리턴하는 함수
    1. 근데 그 함수(2)는 클로저로 표현한 상태(z라는 int를 받아서 int를 리턴)
  3. 근데 그 함수(2)는 int를 받아서 int를 리턴하는함수

fizzbuzz를 함수로 만들기

우선 오늘 실습해볼 기본코드는 fizzbuzz입니다
이거는 뭐 코딩을 시작할때 한번쯤은 해봤을법한 조건들을 모아놓은 코드인데 이걸 함수형 프로그래밍을 사용하면 어떻게되는지 해봅시다!

<기본코드>

var i = 1
while i <= 100 {
    if i % 3 == 0 && i % 5 == 0 {
        print("fizzbuzz")
    }
    else if i % 3 == 0 {
        print("fizz")
    }
    else if i % 5 == 0 {
        print("buzz")
    }
    else {
        print(i)
    }

    i += 1
}

fizzbuzz의 기본 코드는 이렇게되는데 그냥 간단히 설명을하면 1부터 100까지의 숫자들중

  • 3으로 나누어떨어지는 숫자는 fizz를 출력하고
  • 5로나누어떨어지는 숫자는 buzz를 출력하고
  • 3과 5로 동시에 나누어떨어지는 숫자는 fizzbuzz를 출력하고
  • 이 둘중 어느걸로도 나누어떨어지지 않으면 그냥 그 숫자를 그대로 출력한다

우선 기본코드에서 fizz부분을 함수로 바꿔보면

func fizz1(_ input: Int) -> String {
    if input % 3 == 0 {
        return "fizz"
    }
    return ""

이렇게 쓸수있고 buzz함수도 바꿔보면

func buzz1(_ input: Int) -> String {
    if input % 5 == 0 {
        return "buzz"
    }
    return ""
}

이렇게 바꿀수있고 그렇게 해서 <기본코드>에 while문 안쪽 부분을 하나의 함수로 만들수있습니다

func fizzbuzz1(_ i: Int) -> String {
    let f = fizz1(i)
    let b = buzz1(i)
    let result = f + b
    if result.isEmpty {
        return "\(i)"
    }
    return result
}

이렇게 만든 함수를 반복문 안에 넣으면

var i = 1

while i <= 100 {
    let r = fizzbuss(i)
    print(r)
    i += 1
}

이렇게 쓸수있는데 이제부터 생각을 좀 해보면 i는 외부변수이기때문에 side effect를 발생시킬 수 있는 위험이 존재합니다

그 상태에서 이번엔 while문을 함수로 만들면

func loop(min: Int, max: Int, do f: (Int) -> Void) {
    var i = min
    while i <= max {
        f(i)
        i += 1
    }
}

이렇게 만들 수 있고 외부변수를 함수 안에다가 넣음으로써 순수함수가 되고 side effect가 발생하지 않습니다
그래서 실행을 하면 이렇게 실행하면됩니다

loop(min: 1, max: 100) { i in
    let result = fizzbuzz1(i)
    print(result)
}

fizzbuzz함수를 간략화 해보기

자 우선 기본적인 함수로의 구현은 끝났으니 이제 우리가 만들었던 함수를 간소화하는 과정을 거쳐야합니다
만들었던 함수를 차근차근 간소화 시켜보면

우선

func fizz1(_ input: Int) -> String {
    if input % 3 == 0 {
        return "fizz"
    }
    return ""
}

이 함수를 간소화 시켜야하는데 클로저를 통해 간소화를 해보면

let fizz2 = { i in i % 3 == 0 ? "fizz" : ""} 

이렇게 간소화를 할수있습니다. 그리고 이때 변수에 타입을 생략할수있죠.
왜냐면 애초에 클로저에 리턴타입이 String이고 input을 3이라는 int로 나누었기때문에 input이 int고 output이 string이라고 자동으로 알기때문에 타입선언을 할 필요가 없습니다

그러면 뭐 buzz함수도 똑같겠죠?

func buzz1(_ input: Int) -> String {
    if input % 5 == 0 {
        return "buzz"
    }
    return ""
}

이 함수를 fizz함수처럼 클로저로 만들면

let buzz2 = { i in i % 5 == 0 ? "buzz" : ""}

이렇게 만들 수 있습니다

다음이 이제 fizzbuzz함수를 간소화 하는 방법인데 개인적으로는 이 방법을 알게되면서 살짝

아... 이렇게도 표현할수있구나...

라고 생각하게 되었습니다 ㅎㅎ...

func fizzbuzz1(_ i: Int) -> String {
    let f = fizz2(i)
    let b = buzz2(i)
    let result = f + b
    if result.isEmpty {
        return "\(i)"
    }
    return result
}

우리가 fizzbuzz함수를 이렇게 구현했는데 우선 저기 함수의 결과값을 리턴하는 부분을 내부함수로 만들 수가 있습니다

결국 보면 return이라는 input을 받아서 이게 비어있는지 없는지를 확인해서 비어있지 않으면 자기자신을 리턴하고 아니면 i라는 String을 반환합니다

근데 애초에 함수의 input을 return이라는 녀석만 받기에는 이게 output이 i가 될수도있기때문에 이함수에 외부변수가 사용되게됩니다. 즉, side effect가 발생하게 되니까 얘도 input으로 받아버려야하는거죠

    func ff(_ a: String, _ result: String) -> String {
        return result.isEmpty ? a : result
    }

이런느낌으로 여기에 a에 i를 string으로 변환하는 값이 들어가게되는것입니다

그리고 이거 자체가 우선 함수니까 이걸 당연히 클로저로 바꿀수도있겠죠
string두개를 받아서 string하나를 리턴하는거니까 ㅎㅎ

let ff: (String, String) -> String = { a,b in b.isEmpty ? a : b}

이런식으로 표현할 수도 있겠네요

그러면 애초에 이 클로저를 실행한 값을 전체함수의 리턴값으로 보내버리면되니까

func fizzbuzz2(_ i: Int) -> String {
    let result = fizz2(i) + buzz2(i)
    return { a,b in b.isEmpty ? a : b}("\(i)", result)
}

이렇게 표현되고 애초에 result를 변수로 뺄 필요도 없으니

func fizzbuzz2(_ i: Int) -> String {
    return { a,b in b.isEmpty ? a : b }("\(i)" ,fizz2(i) + buzz2(i))
}

로 쓸수있습니다
물론 이것도 당연히 클로저로 만들 수 있겠죠

let fizzbuzz3: (Int) -> String = { i in { a,b in b.isEmpty ? a : b }("\(i)" ,fizz2(i) + buzz2(i)) }

마지막으로 loop함수를 간소화시켜보면

func loop(min: Int, max: Int, do f: (Int) -> Void) {
    var i = min
    while i <= max {
        f(i)
        i += 1
    }
}

기존에 loop함수에서 min과 mix를 array로만들어서 for each해버리면 되겠다는 생각이 우선들고 기본적으로 for each에는 int -> Void 함수를 넣을 수 있는데 여기서 좀 띵했던 부분이 그 함수가 int를 string으로 그리고 void를 리턴해도 된다는 사실이었습니다... 물론 당연하긴하지만 좀 그동안 유연하게 생각하지 못했던 부분이기도 해서 신기했던 부분이었습니다 ㅎㅎ

그래서 최종적으로 바꾸면

func loop(min: Int, max: Int, do f: (Int) -> Void) {
    // min max를 array로 만든다
    Array(min...max).forEach(f)
}

loop(min: 1 , max: 100) { print(fizzbuzz3($0)) }

으로 바꿀 수있습니다


오늘은 함수형 프로그래밍에 대해서 알아봤는데
막상 적용을 하기에는 적응이 되어있지않을 상황에서 떠올리는것 자체가 쉽지는 않아보이긴 합니다 ㅠㅠ
그래서 최대한 이건 함수형 프로그래밍을 이용해볼 수 있겠는데? 하는 부분이있으면 적극적으로 시도해봐야할 것 같습니다:)

profile
AppleDeveloperAcademy@POSTECH 1기 수료, SOPT 32기 iOS파트 수료

0개의 댓글