순수 함수

순수 함수 (Pure Functions)

Scala가 함수형 코드를 작성하도록 도와주는 또 하나의 기능이 바로 **순수 함수(pure function)**를 쓸 수 있다는 점이에요. 같은 입력을 주면 항상 같은 결과를 주는 그런 함수죠. 그 정의부터 차근차근 살펴볼게요.

출처: Scala 3 Book

본문

Scala가 함수형 코드를 작성하는 데 도움을 주는 또 하나의 기능은 순수 함수를 작성할 수 있다는 점이에요. 순수 함수는 이렇게 정의할 수 있어요:

  • 함수 f는, 같은 입력 x가 주어졌을 때 항상 같은 출력 f(x)를 반환하면 순수하다

  • 함수의 출력은 오직 입력 변수와 그 구현에만 의존한다

  • 오직 출력만 계산할 뿐, 주변 세계(외부)를 수정하지 않는다

이 정의가 뜻하는 바는 다음과 같아요:

  • 입력 매개변수를 수정하지 않는다

  • 숨겨진 상태(hidden state)를 변경(mutate)하지 않는다

  • 어떤 '뒷문(back door)'도 없다: 외부 세계(콘솔, 웹 서비스, 데이터베이스, 파일 등)에서 데이터를 읽지 않고, 외부 세계로 데이터를 쓰지도 않는다

이 정의의 결과로, 같은 입력값으로 순수 함수를 호출할 때마다 항상 같은 결과를 얻게 돼요. 예를 들어 double 함수를 입력값 2로 무한 번 호출해도 항상 결과로 4를 얻어요.

순수 함수 예시

이 정의를 생각해 보면, scala.math._ 패키지의 이런 메서드들은 순수 함수랍니다:

  • abs

  • ceil

  • max

이런 String 메서드들도 순수 함수예요:

  • isEmpty

  • length

  • substring

Scala 컬렉션 클래스의 대부분 메서드도 순수 함수처럼 동작해요. drop, filter, map 등을 비롯해 아주 많죠.

Scala에서 *함수(function)*와 *메서드(method)*는 거의 완전히 바꿔 쓸 수 있어요. 그래서 업계에서 일반적으로 쓰는 '순수 함수'라는 용어로 함수와 메서드 둘 다를 설명할 수 있어요. 메서드를 함수처럼 어떻게 쓸 수 있는지 궁금하다면 Eta Expansion 토론을 참고하세요.

불순(impure) 함수 예시

반대로, 다음 함수들은 정의를 위반하기 때문에 *불순(impure)*해요.

  • println – 콘솔, 파일, 데이터베이스, 웹 서비스, 센서 등과 상호작용하는 메서드는 모두 불순해요

  • currentTimeMillis – 날짜와 시간 관련 메서드는 출력이 입력 매개변수가 아닌 다른 것에 의존하기 때문에 모두 불순해요

  • sys.error – 예외를 던지는 메서드는 단순히 결과를 반환하지 않기 때문에 불순해요

불순 함수는 보통 다음 중 하나 이상을 해요:

  • 숨겨진 상태에서 읽는다. 즉 함수에 입력 매개변수로 명시적으로 전달되지 않은 변수와 데이터에 접근한다

  • 숨겨진 상태에 쓴다

  • 주어진 매개변수를 변경하거나, 포함된 클래스의 필드 같은 숨겨진 변수를 변경한다

  • 외부 세계와 어떤 종류의 I/O를 수행한다

일반적으로 반환 타입이 Unit인 함수를 주의해야 해요. 그런 함수는 아무것도 반환하지 않으므로, 논리적으로 호출하는 유일한 이유는 어떤 부수 효과(side effect)를 얻기 위함이에요. 결과적으로 그런 함수의 사용은 대부분 불순합니다.

하지만 불순 함수도 필요해요…

물론 애플리케이션이 외부 세계를 읽고 쓸 수 없다면 그다지 쓸모가 없겠죠. 그래서 이런 권장 사항이 나왔어요:

애플리케이션의 핵심(core)은 순수 함수로 작성하고, 그 핵심 주위에 외부 세계와 상호작용하는 불순한 '래퍼(wrapper)'를 써라. 누군가 말했듯이, 순수한 케이크 위에 불순한 아이싱(icing) 한 겹을 올리는 것과 같아요.

외부 세계와의 불순한 상호작용을 더 순수하게 느껴지게 만드는 방법들이 있다는 점도 중요해요. 예를 들어 입출력을 다루기 위해 IO 모나드를 사용한다는 얘기도 들어볼 수 있을 거예요. 이런 주제는 이 문서의 범위를 벗어나니, 단순하게 생각하기 위해 FP 애플리케이션은 외부 세계와 상호작용하는 다른 함수들로 감싸진 순수 함수의 핵심을 가진다고 생각하면 도움이 돼요.

순수 함수 작성하기

참고: 이 섹션에서는 업계에서 일반적으로 쓰는 '순수 함수'라는 용어가 Scala 메서드를 가리키는 데 자주 사용돼요.

Scala에서 순수 함수를 작성하려면 Scala의 메서드 문법으로 그냥 작성하면 돼요(물론 함수 문법을 사용할 수도 있어요). 예를 들어 입력값을 두 배로 만드는 순수 함수는 이렇게 생겼어요:

def double(i: Int): Int = i * 2

재귀에 익숙하다면, 정수 리스트의 합을 계산하는 순수 함수는 이렇게 작성할 수 있어요:

def sum(xs: List[Int]): Int = xs match
  case Nil => 0
  case head :: tail => head + sum(tail)

이 코드를 이해할 수 있다면, 이 함수가 순수 함수 정의를 충족한다는 걸 알 수 있을 거예요.

핵심 포인트

이 섹션의 첫 번째 핵심 포인트는 순수 함수의 정의예요:

순수 함수는 선언된 입력과 그 구현에만 의존하여 출력을 만들어내는 함수다. 오직 출력만 계산할 뿐, 외부 세계에 의존하지도 않고 수정하지도 않는다.

두 번째 핵심 포인트는, 모든 실전 애플리케이션은 외부 세계와 상호작용한다는 점이에요. 따라서 함수형 프로그램을 단순화해서 생각하면, 외부 세계와 상호작용하는 다른 함수들로 감싸진 순수 함수의 핵심으로 구성된다고 볼 수 있어요.

더 알아보기