함수형 프로그래밍

hjkim·2021년 12월 28일
0

프로그래밍 패러다임

프로그래밍 패러다임은 프로그래머로 하여금 코드를 어떻게 작성할 지 결정하는 역할을 하는데, 최근의 프로그래밍 패러다임은 크게 두 가지로 구분된다.

  • 명령형 프로그래밍 : 어떻게 할 것인지 설명하는 방식
    (how to solve)
    - 절차지향 프로그래밍 : 수행되어야 할 순차적인 처리 과정을 포함하는 방식(C, C++)
    - 객체지향 프로그래밍 : 객체들의 집합으로 프로그램의 상호작용을 표현(C++, Java, C#)
  • 선언형 프로그래밍 : 무엇을 할 것인지 설명하는 방식
    (what to solve)
    - 함수형 프로그래밍 : 순수 함수를 조합하고 소프트웨어를 만드는 방식(클로저, 하스켈, 리스프)

함수형 프로그래밍

함수형 프로그래밍은 자료의 처리를 수학적 함수의 계산으로 취급하고 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임 중 하나이다. 명령형 프로그래밍을 기반으로 개발해오던 개발자들은 소프트웨어의 크기가 커지며 스파게티 코드를 유지보수하는 것이 힘들다는 것을 깨달았다. 이를 해결하고자 등장한 것이 함수형 프로그래밍이라는 프로그래밍 패러다임이다.

함수형 코드는 출력값이 함수의 input parameter에만 의존하므로 인수 x에 같은 값을 넣고 함수 f를 호출하면 항상 f(x)라는 결과가 나온다. 따라서 프로그램의 동작 예측이 쉽다. 또한 작은 문제를 해결하기 위한 함수들을 작성하고 이를 조합해 프로그래밍하므로 가독성도 높아진다. 에러가 발생하면 스파게티 코드를 전부 살펴보아야 했던 명령형 프로그래밍 방식과 달리 함수형 프로그래밍은 x라는 input 값에 대해 예측과 동일한 output이 나오는지 확인하면 되고 문제가 발생한 함수 부분만 살펴보면 되므로 에러 탐색 범위도 줄어드는 것이다.

순수 함수

순수 함수는 결과가 오로지 입력 매개변수에 의해서만 좌우되며 연산이 아무런 side effect를 일으키지 않는, 즉 반환 값 외의 다른 외부 영향이 없는 함수이다. 여기서 side effect란 다음과 같은 변화를 의미한다.

  • 변수의 값 변경
  • 자료구조를 제자리에서 수정
  • 객체의 필드 값 설정
  • 예외나 오류가 발생하며 실행 중단
  • 콘솔 또는 파일 I/O 발생

다음 코드의 예시는 순수 함수가 아닌 예시이다.

public int impureFunc(int value) {
	return Math.random() * value;
}

input parameter에 같은 3이라는 값을 넘겨주고 return 하는 값을 확인한다고 가정한다.

int a = impureFunc(3);
int b = impureFunc(3);

이때 impureFunc라는 값이 순수 함수라면 a와 b는 같은 값을 갖는다. 하지만 이 함수는 내부에서 Random number를 3에 곱해주고 있다. 이 경우 return 값도 랜덤하기 때문에 a와 b가 같다는 것을 보장할 수 없다. 따라서 impureFunc는 순수 함수가 아니다.

public int pureFunc(int value) {
	return value * value;
}

위와 같이 pureFunc의 경우에는 a와 b 모두 3*3 = 9 라는 값을 리턴하므로 순수함수라 할 수 있다. 이처럼 외부 영향 없이 주어진 input 값에 대해 항상 같은 값을 리턴하는 함수를 순수 함수라 한다.

함수형 프로그래밍의 특징

1. 불변성(Immutable)

불변성은 어떤 값의 상태를 변경하지 않는다는 뜻이다. 상태의 변경은 side effect를 발생시키므로 함수형 프로그래밍에서는 이를 제한한다.
call by value를 생각하면 이해가 쉽다. 함수가 실행되고 난 뒤 데이터의 원본 값들이 변하지 않는다.

2. 참조 투명성(Referential Transparency)

프로그램 변경 없이도 어떤 표현식을 값으로 대체할 수 있다는 뜻이다. 즉, f(x)는 y로 대체될 수 있다는 의미이다. 코드를 통해 살펴본다.

String name = "hjkim";

public void print() {
	System.out.println("Hello " + name);
}

위의 print() 함수는 System.out.println과 name 이라는 외부 값들을 참조하고 있어 참조에 투명하지 않다. name이 변경되면 print()의 값이 변경되기 때문이다.

public String print(String name) {
	return "Hello " + name;
}

public static void main(String[] args) {
	String helloString = print("hjkim");
    	System.out.println(helloString);
}

위와 같이 수정한다면 print() 함수가 항상 input 값에 대해 일관된 값을 반환하게 되므로 참조에 투명한 함수가 된다. 이렇듯 외부의 값을 참조하는 것이 아니라 내부의 input parameter 값이 변경된 것에 따라서만 return 값을 다르게 갖는 것을 참조가 투명하다고 한다.

3. 일급 함수

일급 함수가 되기 위해서는 3가지 조건을 만족해야 한다.

  • 함수를 함수의 매개변수로 넘길 수 있다.
  • 함수를 함수의 반환값으로 돌려줄 수 있다.
  • 함수를 변수나 자료구조에 담을 수 있다.

함수형 프로그래밍에서는 함수를 일급 객체로 취급하므로 함수를 파라미터로 넘기거나 자료구조에 담고, 반환값으로 돌려주는 등의 작업이 가능하다.

함수형 프로그램은 여러 작은 순수 함수들로 이루어져 있으므로 이 함수들을 연쇄적 또는 병렬로 호출해서 더 큰 함수를 만드는 과정으로 프로그램을 구축해야 한다. 함수형 프로그래밍에서의 함수들은 전부 일급 객체이므로 원하는 output을 얻기 위해 서로 엮어주어야(Function composition) 한다. 함수를 엮으므로 고차원 함수를 활용한다. 고차원 함수란 함수를 인자로 받고 함수를 결과로 반환하는 함수이다. 즉, 함수 역시 '값'으로 취급하는 것이다. 자바의 고차원 함수로는 map(), 더블 콜론 연산자(::)가 있다.

4. Arity Mismatch

순수 함수를 조합하다 보면 인자의 수가 서로 맞지 않는 순수 함수를 엮어야 하는 상황이 발생할 수 있다. 이러한 상황을 Arity Mismatch라 한다.
해결하는 방법으로는 대표적으로 2가지가 존재한다. 코드 예제는 자바스크립트 코드이나, 개념 이해에 도움이 되므로 참조하도록 한다.

  1. Partial Application : 인자를 부분적으로 먼저 엮는다.

10, 5, 5를 매개변수로 받아 더하는 함수가 있다고 가정한다. 이때 10과 5를 미리 합쳐준 후 15에 5를 더하도록 인자를 부분적으로 엮어주는 방식이다.

//Partial (예시는 Lodash _.partial이다.) [출처](https://marpple.github.io/partial.js/)
function add(a, b, c) {
  return a + b + c;
}
var addTenFive = _.partial(add, 10, 5); //10과 5를 미리 더한다.
console.log(addTenFive(5)); // 15 + 5
  1. Curring : 인자를 하나씩만 받는 함수의 체인으로 만든다.

a와 b 문자열을 합쳐줘야 한다고 할 때, a에 대한 함수를 따로 구현하고 b에 대한 함수를 따로 작성해주어 이 함수들 간의 체인을 만들어주는 방식이다.

//Curring
function before(a) {
  return function after(b) {
  	return a + b;
  }
}
var word = before("이렇게");
var finalWord = word("붙어요!");
console.log(finalWord); //이렇게붙어요!

5. Lazy Evaluation

일반적인 코드는 코드 실행 즉시 값을 평가(Eager Evaluation)하지만 함수형 코드에서는 값이 필요한 시점에 평가(Lazy Evaluation)된다. 값이 실제로 필요한 시점이 올 때까지 실행하지 않는다.

list.parallelStream().limit(5).sum()

위의 코드에서 list 내부에 10개의 데이터가 저장되어 있다고 가정한다. 이때 parallel stream에서는 내부적으로 데이터를 2개씩이든 3개씩이든 JVM이 판단하기에 연산에 최적인 데이터 개수를 갖도록 thread가 나뉜다. 이렇게 나눠 둔 상태에서 sum()이라는 종단 연산이 호출되면 limit(5)가 작동한다. 즉, limit(5)가 sum() 앞에 있어 즉시 sum() 직전에 수행되는 것이 아니라 sum()이 호출되는 순간 limit(5) 연산까지 수행되는 것이다. 이것을 Lazy Evaluation이라 한다.

일반적인 for 문에서는

for (int i=0; i<10; i++) {
	System.out.println(i);
}

i<10인지 아닌지 판단하는 evaluation이 즉각적으로 일어난다. 반면 앞서 살펴본 parallel stream 연산에서는 limit(5) 연산이 즉각적으로 수행되지 않고 sum()이 호출될 때가 되어서야 수행된다. 이를 Lazy Evaluation이라 한다.

함수형 프로그래밍 정리

함수형 프로그래밍에서는 발생할 수 있는 side effect들을 최대한 프로그램을 구성하는 함수들과 분리한다. 이렇게 구성하면 예측 가능한 코드가 되어 버그를 줄일 수 있다. 또한 기능 추가/수정 시 관련 없는 함수는 수정할 필요가 없으며 함수를 추가하는 방식으로 쉽게 확장이 가능하다. 클래스나 함수들이 하나의 일만 하므로 재사용성이 높아지고 테스트도 수월하게 진행할 수 있다.
순수 함수를 작성하여 함수형 프로그램으로 개발하는 것이 무조건 장점만 존재하는 것은 아니다. 순수함수를 구현하기 위해 순수함수 내부의 코드 가독성이 떨어질 수도 있고, 순수함수들을 조합하는 데 어려움(Arity Mismatch)이 발생할 수 있다.
그럼에도 명령형 프로그램의 단점인 side effect들을 줄여 코드의 유지보수성을 높일 수 있다는 점에서 함수형 프로그래밍이 권장되고 있다는 사실은 부정할 수 없어 보인다.


[참조] https://ko.wikipedia.org/wiki/%ED%95%A8%EC%88%98%ED%98%95_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D
[참조] https://mangkyu.tistory.com/111
[참조] https://www.itworld.co.kr/t/61023/%EA%B0%9C%EB%B0%9C%EC%9E%90/189028
[참조] https://medium.com/humanscape-tech/%ED%95%A8%EC%88%98%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%EC%97%90-%EA%B4%80%ED%95%B4-7f6172599fc
[참조] https://velog.io/@thms200/%ED%95%A8%EC%88%98%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D
[참조] https://stackoverflow.com/questions/22395311/difference-between-pure-and-impure-function

profile
피드백은 언제나 환영입니다! :)

0개의 댓글