[About iOS] FP - 함수형 프로그래밍

kio·2022년 7월 16일
0
post-thumbnail

이 글은 다른 글들보다 유독 제 생각이 많이 들어가있고, 저만의 해석이 있습니다.
기본적으로 쉽게 설명하기 위한 비유가 들어가있으며, 완전히 그 비유와는 부합하진 않을 수 있어 큰 틀만 봐주시면 감사하겠습니다.
잘못된 점이 있다면 지적해주시면 겸허히 받아드리겠습니다.

함수형 프로그래밍?

Functional Programming 즉 함수형 프로그래밍은 기존 객체지향프로그래밍에서 실행되던 객체의 통제하에 이루어지는 프로그래밍과는 다르게 각 함수가 무소속으로 진행된다는 점이 크게 다른 패러다임이다.

객체지향 프로그래밍의 단점

객체지향 프로그래밍은 각 상황에 맞게 함수를 사용하고, 변수를 바꾸면서 같은 로직의 묶음으로 이루어져 있다. 그 속에서 추상화, 다형성, 캡슐화, 상속성, 동적바인딩 등의 법칙을 지켜가며 프로그래밍하는 패러다임이다.
하지만 치명적이 단점이 있다. 코드의 오류를 찾기 힘들다는 것이다.

내 눈에 보이는 var score: Int?의 값이 시시각각 변하고 어떤 값이 바뀔지
여기에 정수값이 잘들어가는지 실제 사용해봐야 안다.

흠 그럼 이런생각이 들 수 있다.
"어떻게 변수를 바꾸지 않고 프로그래밍하지?", "나도 함수를 쓰는 데 그게 함수형 프로그래밍과 다른건가?"

이제부터 그것에 대해 쉽게 정리해보자

함수형 프로그래밍의 특징

우선 함수형 프로그래밍의 특징을 간단하게 나열해보자!
이게 왜 순수함수의 조건이자 특징이 되는지는 나중에 설명하겠다.

1. 순수함수

let num = 10
func multiply(_ int a) -> Int {
	return num * a
}

위 코드는 num값에 따라 인자가 같은게 들어와도 결과가 달라진다.

func multiply(_ int a) -> Int{
	return 10 * a
}

이렇게 같은 인자가 들어오면 항상 같은 값을 뱉어내는게 순수함수이다.

2. 1급 함수

즉 함수로 뭐든 되는 것이 1급 함수이다. ( 수능 1등급은 뭐든 대학 다가는 느낌 ? ㅎㅎ)

func multiply(_ a: Int) -> Int {
	return a * 10 
}

let funcOne = multiply

위 처럼 함수를 변수 할당하는 것도 되고,

func multiply(_ a: Int) -> (_ b: Int) -> Int {
    return { b in
        return b * a
    }
}
let funcOne = multiply(5)

리턴값이 함수도 될 수 있고, 인자도 함수일 수 있는
즉 함수로 뭐든 되는 것을 1급 함수라고 한다.
그리고 이러한 1급 함수는 고차함수 즉 함수가 함수를 그 함수가 함수를 리턴하는 문법을 실행시켜야한다.

3. 선언형의 함수

우선 코드부터 보자!

var list = [1,2,3,4,5,6]

func updateList(){
    for i in 0..<list.count {
        list[i] = list[i] + 1
    }
}

func updateList() -> [Int] {
    return list.map { $0 + 1 }
}
list = updateList()

이렇게 함수안에 if, for 등을 사용하지 않고 쓰는 것이 함수형 프로그래밍의 특징이다.

4. 비상태, 불변성

어떤 값을 변경하지 않고, 변경해야 된다면 그 복사본을 변경해서 원래 값을 항상 유지하는 것을 설명한다.

var num = 1

func add(_ a: Int) -> Int{
    num += 1
    return num
}

func add(_ a: Int) -> Int{
    let temp = num
    return temp
}

왜 이런 특징들이 있는 것일까?

함수형 프로그래밍(函數型 프로그래밍, 영어: functional programming)은 자료 처리를 수학적 함수의 계산으로 취급하고 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임의 하나이다.
출처 - 위키피디아

함수형 프로그래밍의 특징이나 monad, functor 등등 다 저 한문장 즉 함수형 프로그래밍의 정의로 인해 도출된 결과다 그럼 차근차근 보자

왜 저런 특징들을 가지나??

우선 제가 존경하는 얄코님의 이 영상을 보고 시작해보자
항상 비유를 찰떡으로 해주시는 얄코님의 비유를 통해 설명할려고 한다.
아 그리고 이런 생각은 이 블로그의 글을 보고 많은 참고를 했다.

위 네가지는 조건이 아닌 특징이다. " 저 4가지를 지키는 것이 순수 함수형 프로그래밍이다. " 라기 보단 "순수 함수형 프로그래밍을 하기 위해선 자연스럽게 저 4가지가 지켜진다." 에 가깝다.

이러한 함수형 프로그래밍의 시작은 이렇게 생각해보자
" 함수로만 모든 것을 다 구현한다. " ( 어디까지나 일단 설명을 위해서 .. )
그럼 어떻게 해야할까?

우리의 최종 목표는 x의 값으로 y라는 결과를 도출하는 것이다.
우리는 객체지향프로그래밍에서 이러한 과정을 반복했다.
그러다 보니 비슷한 로직과 설계원리에 따라 묶여도 그 자체가 너무 커져 유지보수가 힘들어졌다.

그럼 이걸 쪼개서 작은 단위로 관리해보자!! 그러면 아래와 같은 명제가 참이여야한다.
1. x -> z
2. z -> y
이 명제가 맞다면 우리는 범주이론을 통해 x -> y 를 도출할 수 있다고 할 수있다.
그럼 함수형 프로그래밍의 관리하기 쉽고 재사용성이 높은 범위에서 저러한 z를 계속 만들어 가야한다.
즉 x -> z -> a -> b -> y
뭐 이런식으로 말이다.
이것이 함수형 프로그래밍이다. 그렇다면 이게 왜 위와 같은 특징을 같게 되었을까?

왜 순수함수를 가지나?

이러한 로직에 만약 함수 z가 네트워크 환경에 영향을 받아 같은 인풋을 줬는데 다른 아웃풋이 나오게 되었다. 그럼 함수형 프로그래밍에선 위에 로직이 깨질 수 있다.
만약 z -> a 가 위와 같은 상황에 의해 거짓이 된다면 우리는 무조건 처리해야하는 로직인 x -> y를 처리할 수 없게 된다. 이는 치명적인 문제다.
그래서 위에 모든 함수는 반드시 순수함수여야한다.( 어디까지나 순수 함수형 프로그래밍에서.. )

왜 1급 함수여야하나?

1급 함수여야하는 가장 큰 이유는 커링 때문이다. 만약 z, x -> a라면 이 두가지의 조합의 따라 결과가 바뀔 수 있다. 이러면 z은 같더라도 x에 의해 바뀌면서 x -> y의 도달 할 수 있다는 가능성이 없어질 수도 있다. 이것을 커링을 통해 x -> z -> a로 치환할수 있다. 이는 함수가 1급 객체 즉 1급함수여야 가능해야 가능하다.

왜 선언형의 함수를 가져야 하는가?

함수형 언어의 목적은 위에서 말한 빠른 처리속도와 테스트에 있다.
즉 "적은 오류를 최대한 모듈화해서 짠다." 라는 생각을 가지고 있기 때문에
우리는 선언형 함수를 사용한다.

for문은 우리의 디버깅을 방해한다. 이 흐름이 어떻게 되고 함수 내부에서 어떤 값이 어떻게 바뀌는지 이런걸 신경쓸거면 우린 굳이 함수형을 쓸 필요가 없다.
그래서 우리는 repetitive 않고 recursive한 함수를 만들어야한다.
함수형 언어에서 재귀를 사용하는 함수도 마찬가지로 순수함수이기 때문에 재귀를 사용하는데 흐름을 파악하기 쉽고 에러도 적어진다.

하지만 repetitive함이 필요할수도 있다.
그래서 우리는 지금부터 map을 알아보겠다.

초콜릿공장의 예를 가져와서 사용해보자

func makeLiquid(_ source: [Chocolate]) -> [SolidChocolate] {
    var array = [SolidChocolate]()
    for element in source {
        array.append(SolidChocolate(weight: element.weight, madeIn: element.madeIn))
    }
    return array
}

func makeProduct(_ source: [SolidChocolate]) ->[Product]{
    var array = [Product]()
    for element in source {
        array.append(Product(price: 1500))
    }
    return array
}

자 여기서 오류를 찾으세요 하면 한 함수다 몇줄 안되지만 눈으로 디버깅해야한다. 이렇게 되면 빠르지도 않고 함수내에 오류도 찾기 힘들지 않겠는가?

그럼 다음 코드를 보자.

func makeLiquid(_ source: [Chocolate]) -> [SolidChocolate] {
    return source.map { element -> SolidChocolate in
        return SolidChocolate(weight: element.weight, madeIn: element.madeIn)
    }
}

func makeProduct(_ source: [SolidChocolate]) ->[Product]{
    return source.map { element -> Product in
        return Product(price: 1500)
    }
}

자 이 얼마나 읽기 쉬운가 사실 다 읽을 필요도 없다. 눈으로 디버깅이 쉬워지고 이게 뭘하려는 알 수 있다.

근데 정말 중요하고 어려운 개념은 여기서 등장한다.

위에서 범주론을 얘기했던것을 기억하는가?
우리는 높은 단계의 추상화를 이루는 함수형 프로그래밍과 범주론을 엮는다.

추상화란 무엇인가? 는 추후에 다루도록 하고,
이런 생각을 해볼 수 있다.
a -> b -> c 이렇게 흐르는데
만약 b에서 throw로 error를 뱉으면 즉 얘기되지 않은 인자를 주면 이 연결성이 깨지게 되나?
또, 이런 생각이 든다 결국 이것은 a -> c로 가는 데이터의 변경의 연속일 뿐이다.
우리는 터치를 받고, 그 터치를 데이터로 바꾸고, 이를 json으로 받고, 이것을 다시 원하는 형태로 바꿔 화면에 출력한다.
이는 결국 정보의 변경의 연속이다.

우리는 이를 포괄하는 즉 데이터의 제네릭한 부분과 에러도 나올 수 있음, 결국 데이터를 변경하는 일련의 과정 이 세가지를 하나로 요약할 수 있다.

그것이 monad이다.

이 monad는 나보다 이 영상 더 큰 도움이 될 수도 있다.

functor를 통해 오류와 데이터 변경을 하나로 묶을 수 있다.
또 모나드를 통해 functor의 형태로 뱉을 수도 있다.

난 아직 이해를 못한건지 모르겠지만, 이것이 참 Rx와 비슷하다고 생각했다.

 Observable<String>.of("매운맛","달콤한맛","똥맛")
            .flatMap{ result -> Observable<Int> in
                return Observable.create { emitter in
                    emitter.onNext(result.count)
                    return Disposables.create()
                }
            }
            .map { $0 * 10}
            .subscribe()
            .dispose()

코드에 의미는 없지만 뭔가 비슷하다고 생각된다.
stream이라는 특수형태 (error도 있고, 데이터도 있는)
거기다 flatmap은 stream to stream
map은 stream to data
라는 점이 상당히 비슷하다고 생각된다.

내가 제일 어려운건 무수한 고수들이 이것이 엄청 어렵다고하는 이유를 모르겠다.
나도 무조건 어려운텐데 난 너무 쉽게 이해했다. 그래서 자신이 없으니 후에 보강할려고 한다.

여기까지 함수형 프로그래밍의 특징을 설명한 블로그는 맞지만 그게 왜 생겨났는지에 대해 설명을 해보았다.
물론 인터넷을 보고 정리하고 스스로 유추한 내용이 많아 정답이 아닐 순 있지만
어느 정도 가이드 라인이 될 것 같다. ( 완전 다 틀렸거나 너무 비약이 심하면 알려주쇼 선생님 )

이 글을 쓰면서 추가로 생각할 점..
1. 난 아직 머리는 객체형 명령형 사고로 문제를 해결할려고 해서 함수형 사고를 하는 법을 깨우쳐야 겠다.

그럼 이상으로 함수형 프로그래밍 블로깅을 끝내겠다.

수고하셨습니다~~~ 꼭 잘못된거 있으면 알려주세요 ㅠㅠ

0개의 댓글