선언형 & 함수형 프로그래밍

박형석·2021년 11월 25일
1

CS

목록 보기
5/10
post-thumbnail

프로그래밍 대세

최신 언어들은 대부분 다 반영하는 프로그래밍 패러다임이다. 사실 이렇다고 말하지만 swift만 제대로 써봤기 때문에... swift가 그렇다. 새로운 시각과 관점이 필요하기에 명령형에 익숙한 사람들에게 전환이 쉽지 않다고 한다. 그래도 앞으로 대세이기 때문에 꼭 알아두고 사용해보자.

RxSwift를 쓰지만 실제로 함수형 프로그램을 설계해보지 않았기 때문에 좋은 점은 알지만 그 안에 어떤 철학과 시각이 담겨있는지 깊이는 모른다. 점점 공부를 해가기로 하고 이번 포스팅에서는 함수형 프로그래밍의 간단히 소개하고 이해하는 시간을 가져보자.

선언형 프로그래밍

함수형을 이해하기 위해 함수형을 포함하는 선언형 프로그래밍에 대해서 간단히 알아보자.

선언형 프로그래밍이란?

명령형 프로그래밍은 어떻게 할지 표현하고, 선언형 프로그래밍은 무엇을 할 건지 표현한다.

아래는 선언형을 이해에 도움이 되는 여러 정의다.

  • 개발자 혹은 어떤 것이 무엇을 하는지에 더 관심을 가지는 것이다.
  • '기계의 작동 모델'보다 '개발자의 정신 모델'에 더 부합하는 방식으로 프로그래밍을 하는 것이다.
  • 선언적 문장으로 프로그래밍을 하는 것이다.
  • 제어 흐름, 알고리즘을 설명하지 않고 그 연산의 논리를 표현하는 패러다임이다.

명령형 vs 선언형

명령형 프로그래밍

func double(arr: [Int]) -> [Int] {
 var results = [Int]()
 for i in 0...<arr.count {
  results.appent(arr[i] * 2)
 }
 return results
}
  • double이라는 함수가 원하는 기능을 구현하는 방법과 그 단계를 명시적으로 표현하고 있다.
  • double은 상태의 일부를 변경시키고 있다. results라는 변수를 만들어 계속해서 수정하며 단계를 진행한다.
  • 코드의 가독성이 떨어진다. 코드만 보고 무슨 일이 일어나고 있는지 직관적으로 알 수 없다. (물론 이 경우는 짧아서 금방 알 수 있다만...)

선언형 프로그래밍

var results = arr.map { $0*2 }
  • 프로그래머가 무엇을 하고 싶은지 기술했다. 위 명령형 코드에서는 각 요소를 더블링하기 위해서 상태를 변경하는 구문을 이용해 목적을 달성했다. 하지만 이 코드는 map이라는 고차함수를 사용해서 '각각 요소를 더블링 해주세요'라고 선언, 최종 목적을 기술하고 있다. map이 어떻게 구현되는지 모른다. 신경쓸 이유도 없다. 그 사용 목적만을 알고 있을 뿐이다.
  • 어떤 상태도 변경하지 않는다. 모든 변경 사항은 map 안에 추상화되어 있다.
  • 직관적이고 읽기 쉽다.
  • context-independent 선언형 코드는 '어떤 목적을 성취하기 위해서 사용되는 단계 중에 하나'가 아니라 '궁극적인 목적 자체'와 관련이 있기 때문에 그 목적이 동일하다면 다른 프로그램에 적합하게 사용할 수 있다. 명령한 코드는 현재 컨텍스트에 의존적이기 때문에 이 작업을 수행하기 어렵다.

함수형 프로그래밍

함수형 프로그래밍이란?

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

도움이 되는 다른 정의

  • 순수 함수의 조합으로 프로그래밍하며 최종 Output이 발생할 수 있도록 순수 함수들을 엮어서 호출한다.
  • 문이 아닌 식이나 선언으로 수행되는 선언형 프로그래밍 패러다임을 따르고 있다.

기존 명령형 프로그래밍(절차적이나 객체 지향이나)에서는 프로그램에서 값이나 상태의 변화를 중요하게 여기지만 함수형 프로그래밍 패러다임은 함수 자체의 응용을 중요하게 여긴다. 기존 객체지향 프로그래밍에서는 객체들이 가지고 있는 속성의 변화와 그런 변화를 주고 받는 객체들의 행동으로 인해 프로그래밍이 진행되었다면, 함수형 프로그래밍에서는 그런 상태의 변화없이 '순수함수들의 조합'으로 프로그래밍 한다.

그렇기 때문에 함수형 프로그래밍을 지원하는 swift에서는 다양한 종류의 함수를 호출, 전달, 반환하는 등의 동작만으로도 프로그램을 구현할 수 있다.

이런 의미에서 지난 절차적 프로그래밍에서도 언급했던 것처럼, 함수형 프로그래밍의 함수명령형 프로그래밍에서의 함수는 의미가 조금 다르다. 명령형 함수를 이용하는 함수는 실행 시 함수가 전달받은 인자 외에도 포인터, 레퍼런스 값 등의 프로퍼티 값 또는 메모리 참조 값 등이 변경될 수 있고 함수 내부 처리에도 영향을 줄 수 있다. 하지만 함수형 프로그래밍의 함수는 '순수함수', 즉 순수하게 함수에 전달된 인자 값만 결과에 영향을 주기 때문에 상태 값을 가지지 않는다. 함수는 상호 간섭없이 배타적으로 실행된다. 그래야 함수마다 다른 값을 리턴하거나 side effect가 일어나거나 다른 스레드에 영향을 받거나 하는 일들이 일어나지 않기 때문이다. 그래서 함수형 프로그래밍은 병렬처리에 부작용이 없고 대규모 병렬처리에 큰 장점이 있다.

함수형 프로그래밍의 특징

1. 순수 함수

  • 동일한 입력에는 항상 같은 값을 반환한다.
  • 함수의 출력(return)은 오로지 그 함수에 입력된 값(input)에만 의존한다.
  • 함수의 실행은 프로그램의 실행에 영향을 미치지 않아야 한다.
    Self-contained되어야 한다. 즉, side effect가 없다.
    Side effect가 없다는 의미는 오로지 출력(return) 만 수행한다는 의미이다.

2. 불변성

  • Input의 Immutability을 유지해야 순수함수의 순수성 유지가 의미가 있다.
  • Input이 변할 수 있을까?

    shallow copy vs deep copy

    Input이 변경되지 않도록 순수함수에서는 input을 copy해서 사용하는데,
    call by value가 적용되는 원시 타입(primative type)이면 문제가 없지만,
    call by reference가 적용되는 참조 타입(reference type)이면 얘기가 좀 달라진다.
    객체처럼 참조타입 데이터는 해당 값이 아닌 '값이 저장된 메모리의 주소'가 저장된다.
    Shallow copy(얕은 복사)는 참조형 Type 데이터가 저장한 '메모리 주소 값'만 복사한 것을 의미한다.
    반대로 Deep copy는 새로운 메모리 공간을 확보해 완전히 복사하는 것을 의미한다.

  • 구조체를 Input에 넣었을 때와 클래스를 Input에 넣었을 때 불변성을 유지시키는 구조가 다르다. 구조체는 deep copy이기 때문에 함수 내에서 변경이 불가능하기 때문에 Input의 불변성을 고민하지 않아도 된다. 하지만 클래스는 swallow copy이기 때문에 함수 내에서 속성 변경이 가능하다. 이는 함수가 상태를 변경할 수 있는 여지를 주게 되므로 주의해야 한다.

3. 참조의 투명성

함수의 순수성, 전달인자의 불변성을 유지하면 다음과 같은 참조의 투명성을 기대할 수 있다.

  1. 자기 충족적이다(self-contained). 함수 외부에 의존하는 코드가 없고, 함수 사용자 입장에서는 유효한 매개변수만 전달하면 된다.

  2. 결정론적이다 (deterministic). 동일한 매개변수에 대해서는 항상 동일한 결과가 나온다.

  3. 예외 (Exception) 를 던지지 않는다. out of memory error 혹은 stack overflow error 는 발생할 수 있지만, 이러한 에러들은 버그로 취급되며, 함수의 사용자가 다룰 수 있는 것은 아니다.

  4. 다른 코드가 예기치 않게 실패하는 조건을 만들지 않는다. 예를 들어, 참조 투명성을 가진 함수는 매개 변수의 값을 변경하거나 함수 외부의 데이터를 변경하지 않는다.

  5. 데이터베이스, 파일 시스템, 네트워크 등의 외부 기기로 인해 동작이 멈추지 (hang) 않는다.

4. Avoid

함수형 프로그래밍에서 가장 피해야 하는 지점이다.

  1. 공유상태 피하기 (Avoid shared state)
  2. 상태변화 피하기 (Avoid mutating state)
  3. 부작용 피하기 (Avoid side effects)

함수형 프로그래밍 활용

1. 함수의 합성

  • 원하는 Output을 위해 둘 이상의 재사용 가능한 순수 함수를 조합하는 과정을 말한다.
  • 함수형 프로그램은 여러 작은 순수 함수들로 이루어져있기 때문에 이 함수들을 연쇄적으로 또는 병렬로 호출해서 더 큰 함수를 만드는 과정으로 전체 프로그램을 구축한다. 그리고 이 과정에서 함수를 엮기 때문에 고차원함수(Higher-Order Functions)를 활용해야 한다.

    고차원 함수는 함수를 인자로 받고 또 결과로 반환하는 함수를 이야기한다.

  • 이 때문에 함수형 프로그래밍에서 함수는 일급객체여야 한다.

    일급객체?
    - 전달인자(argument)로 전달할 수 있다.
    - 동적 프로퍼티 할당이 가능하다. 컴파일 단계가 아니라 런타임시에도 할당이 가능하다는 뜻이다.
    - 변수나 데이터 구조(자료구조) 안에 담을 수 있다
    - 반환 값으로 사용할 수 있다.
    - 할당할 때 사용된 이름과 관계없이 고유한 객체로 구별할 수 있다.

  • swift에서는 filter, map, flatMap, reduce 등의 기능을 사용할 수 있다.

2. Function Decoration (함수 데코?)

  • 순수함수를 재사용하다보면, 이미 만들어진 순수함수를 그대로 적용하기보다 일부 개선하거나, 조금 다른 방향으로 수정해야 하는 상황이 발생할 수 있다. 이 때 function decoration을 적용한다.

    데코레이터 사용법
    https://injun379.tistory.com/78

3. Partial Application & Curring

  • 순수 함수를 조합하다보면, 인자의 수가 서로 맞지 않는 순수함수를 엮어야 하는 상황이 있을 수 있다.
    이 상황을 Arity(the number of arguments taken by a function) Mismatch라 한다.

  • Arity Mismatch는 왜 일어날까?
    순수함수 중에는 2개 이상의 인자를 필요로 하는 함수가 있을 수 있음
    이 순수함수는 2개 이상의 순수함수에서 실행된 출력값을 받아와야 함
    하지만 함수는 무조건 1개의 return 값만 나옴
    이렇게 인자의 수가 서로 맞지 않는 상황을 Arity Mismatch라 한다.

  • Partial Application & Curring으로 Arity Mismatch을 해결
    Partial Application : 인자를 부분적으로 먼저 엮어준다.
    Curring : 인자를 하나씩만 받는 함수의 체인으로 만드는 방법이다.

// Partial Application
func add(_ a: Int, _ b: Int) -> (Int) -> Int {
    return { c in
        return a + b + c
    }
}
var addTenFive = add(10, 5)//10과 5를 미리 합쳐준다!
print(addTenFive(5)) // 미리 10과 5가 합쳐줬기 때문에 20이 나온다.

//Curring
func before(_ a: String) -> ((String)->String) {
  return { b in
      return a + b;
  }
}
var word = before("이렇게");
var finalWord = word("붙어요!");
print(finalWord); //이렇게붙어요!

함수형 프로그래밍의 장점

  1. 여러 가지 연산 처리 작업이 동시에 일어나는 프로그램을 만들기 쉽다.
  2. 멀티 코어 혹은 여러 개 연산 프로세서를 사용하는 시스템에서 효율적인 프로그램을 만들기 쉽다.
  3. 상태변화에 따른 부작용에서 자유로워지기 때문에 순수하게 기능 구현에 초점을 맞추어 설계할 수 있다.

함수형 프로그래밍의 단점

  1. 순수함수를 구현하기 위해 코드의 가독성이 좋지 않을 수 있다.
  2. 재귀적 코드 스타일은 무한루프에 빠질 수 있다.
  3. 순수함수를 쓰는건 쉬울 수 있지만, 그것들을 조합하는 것은 쉽지 않다.
profile
IOS Developer

0개의 댓글