F의 기능 프로그래밍 개념 소개#

함수형 프로그래밍은 함수 및 변경 불가능 데이터 사용을 강조 하는 프로그래밍 스타일입니다. 형식화된 함수형 프로그래밍이란 함수형 프로그래밍이 F# 같은 정적 형식과 결합된 경우를 말합니다. 일반적으로 함수형 프로그래밍에서는 다음 개념을 강조합니다.

  • 기본 구문으로 사용하는 함수
  • 문 대신 식
  • 변수 대신 변경 불가능한 값
  • 명령형 프로그래밍 대신 선언적 프로그래밍

이 시리즈에서는 F#을 사용하는 함수형 프로그래밍의 개념과 패턴을 살펴봅니다. 이 과정에서 F#의 일부 기능에 대해서도 배우게 됩니다.

용어

다른 프로그래밍 패러다임처럼 함수형 프로그래밍에도 반드시 알아야 하는 용어 모음이 있습니다. 다음은 자주 보게 되는 대표적인 일반 용어입니다.

  • 함수 -함수는 입력을 제공하면 출력을 생성하는 구문입니다. 좀 더 공식적으로는 한 집합의 항목을 다른 집합으로 매핑 합니다. 이러한 형식은 다양한 방식으로 구체화되는 데, 데이터 집합에서 작동하는 함수를 사용할 때는 더욱 그렇습니다. 함수는 함수형 프로그래밍의 가장 기본적인(그리고 중요한) 개념입니다.
  • - 식은 값을 생성하는 코드의 구문입니다. F#에서는 이 값은 바인딩하거나 명시적으로 무시해야 합니다. 식은 함수 호출로 쉽게 바꿀 수 있습니다.
  • 순수성 - 순수성은 동일한 인수에서는 반환 값이 항상 동일하며, 계산에 어떠한 의도하지 않은 결과도 발생하지 않음을 의미하는 함수 속성입니다. 순수한 함수는 자체 인수에만 종속됩니다.
  • 참조 투명도 - 참조 투명도는 프로그램 동작에 영향을 주지 않고도 출력으로 대체될 수 있는 식 속성을 말합니다.
  • 불변성 - 불변성은 내부에서 값을 변경할 수 없음을 의미합니다. 내부에서 변경할 수 있는 변수와는 반대되는 개념입니다.

예제

다음은 이러한 핵심 개념을 보여주는 예제입니다.

함수

함수형 프로그래밍의 가장 일반적이고 기본적인 구문은 함수입니다. 다음은 1을 정수에 더하는 간단한 함수입니다.

let addOne x = x + 1

해당 형식 시그니처는 다음과 같습니다.

val addOne: x:int -> int

시그니처는 "addOne에서 x라는 int를 수락하여 int를 생성한다"는 뜻이라고 생각하면 됩니다. 더 공식적으로는 addOne정수 집합의 값을 정수 집합으로 매핑 하는 것입니다. -> 토큰은 이 매핑을 의미합니다. F#에서는 대부분의 경우 수행 중인 작업을 함수 시그니처를 통해 확인합니다.

시그니처가 중요한 이유는 무엇일까요? 형식화된 함수형 프로그래밍에서 함수 구현은 일반적으로 실제 형식 시그니처보다 중요하지 않습니다. 값 1을 정수에 추가하는 것은 addOne 런타임에 흥미롭지만 프로그램을 생성할 때는 이 함수를 실제로 int 사용하는 방법을 알려 줍니다. 또한 이 함수를 (형식 시그니처에 대해) 올바르게 사용하면 addOne 함수 본문 내에서 문제를 진단할 수 있게 됩니다. 이것은 형식화된 함수형 프로그래밍의 자극제입니다.

식은 값으로 계산되는 구문입니다. 작업을 수행하는 문과 달리 식은 값을 반환하는 작업이라고 생각하면 됩니다. 함수형 프로그래밍에서 거의 항상 문 대신 식이 사용됩니다.

이전 함수인 addOne을 생각해 보세요. addOne의 본문은 식입니다.

// 'x + 1' is an expression!
let addOne x = x + 1

addOne 함수의 결과 형식을 정의하는 이 식의 결과죠. 예를 들어 이 함수를 구성하는 식을 string 같은 다른 형식으로 변경할 수 있습니다.

let addOne x = x.ToString() + "1"

함수의 시그니처는 이제 다음과 같습니다.

val addOne: x:'a -> string

F#의 모든 형식은 ToString()을 호출할 수 있으며, 따라서 x 형식이 일반이 되며(이를 자동 일반화하고 합니다) 결과 형식은 string이 됩니다.

식은 함수의 본문에만 국한되지 않습니다. 다른 곳에서 사용할 수 있는 값을 생성하는 식을 이용할 수도 있습니다. 대표적인 식은 if입니다.

// Checks if 'x' is odd by using the mod operator
let isOdd x = x % 2 <> 0

let addOneIfOdd input =
    let result =
        if isOdd input then
            input + 1
        else
            input

    result

if 식은 result라는 값을 생성합니다. result를 완전히 누락하면 if 식이 addOneIfOdd 함수의 본문이 됩니다. 식의 핵심은 값을 생성한다는 사실입니다.

unit이라는 특수한 형식도 있는데, 반환할 내용이 없을 때 사용합니다. 예를 들어 다음과 같은 간단한 함수를 생각해 보세요.

let printString (str: string) =
    printfn $"String is: {str}"

시그니처는 다음과 같습니다.

val printString: str:string -> unit

unit 형식은 반환되는 실제 값이 없음을 의미합니다. 결과로 반환되는 값이 없어도 "작동해야 하는" 루틴이 있을 때 유용합니다.

동일한 if 구문이 문이며 대부분의 경우 변경 변수가 값을 생성하는 명령형 프로그래밍과는 완전히 다릅니다. 예를 들어 C#에서는 코드를 다음과 같이 작성해야 합니다.

bool IsOdd(int x) => x % 2 != 0;

int AddOneIfOdd(int input)
{
    var result = input;

    if (IsOdd(input))
    {
        result = input + 1;
    }

    return result;
}

C#과 다른 C 스타일 언어는 식 기반 조건부 프로그래밍을 허용하는 삼항 식을 지원한다는 점에 유의하세요.

함수형 프로그래밍에서는 문을 이용해 값을 변경하는 경우가 거의 없습니다. 일부 함수형 언어는 문과 변경을 지원하지만, 함수형 프로그래밍에서는 두 개념을 잘 사용하지 않습니다.

순수 함수

앞에서 설명한 것처럼 순수 함수는 다음을 수행하는 함수입니다.

  • 동일한 입력에 대해서는 항상 동일한 값으로 계산합니다.
  • 의도하지 않은 결과를 유발하지 않습니다.

여기서는 수학 함수를 떠올리면 도움이 됩니다. 수학에서 함수는 관련 인수에만 종속되며 의도하지 않은 결과를 유발하지 않습니다. 수학 함수 f(x) = x + 1에서 f(x) 값은 x 값에만 종속됩니다. 함수형 프로그래밍에서의 순수 함수도 마찬가지입니다.

순수 함수를 작성할 때 함수는 관련 인수에만 종속되고 의도하지 않은 결과를 유발하는 작업은 수행하지 않아야 합니다.

다음은 전역적이고 변경 가능한 상태에 종속되기 때문에 순수하지 않은 함수의 예제입니다.

let mutable value = 1

let addOneToValue x = x + value

addOneToValue 함수는 순수하지 않은 함수임이 확실합니다. value가 언제든 1이 아닌 다른 값으로 변경될 수 있기 때문입니다. 전역 값에 종속되는 이 패턴은 함수형 프로그래밍에서는 사용하지 않아야 합니다.

다음은 의도하지 않은 결과를 유발하기 때문에 순수하지 않은 함수의 또 다른 예입니다.

let addOneToValue x =
    printfn $"x is %d{x}"
    x + 1

이 함수는 전역 값에 종속되지 않지만 x 의 값을 프로그램의 출력에 씁니다. 함수를 사용하면 안 된다는 것이 아니라 함수가 순수하지 않다는 것을 의미합니다. 프로그램의 다른 부분이 출력 버퍼 같은 프로그램 외부 요소에 종속되는 경우, 이 함수를 호출하면 프로그램의 다른 부분에 영향을 줄 수 있습니다.

printfn 문을 제거하면 순수 함수가 됩니다.

let addOneToValue x = x + 1

이 함수는 문을 사용하여 이전 버전 printfn 보다 본질적으로 나은 것은 아니지만 이 함수가 수행하는 모든 함수가 값을 반환한다는 것을 보장합니다. 이 함수를 여러 번 호출해도 매번 값 생성이라는 동일한 결과가 발생합니다. 순수성을 바탕으로 제공되는 예측 가능성은 많은 함수형 프로그래머가 원하는 기능입니다.

불변성

마지막으로, 형식화된 함수형 프로그래밍의 가장 기본적인 개념은 불변성입니다. F#에서는 기본적으로 어떤 값도 변경할 수 없습니다. 따라서 사용자가 변경 가능하다고 명시적으로 표시하지 않는 한 내부에서 값을 변경할 수 없습니다.

실제로 변경 불가능한 값을 사용한다는 것은 프로그래밍 접근 방식을 "뭔가를 바꿔야 해"에서 "새 값을 생성해야 해"로 변경한다는 뜻입니다.

예를 들어 1을 값에 추가하면 기존 값이 변경되는 대신 새 값이 생성됩니다.

let value = 1
let secondValue = value + 1

F#에서 다음 코드는 value 함수를 변경하지 않습니다. 대신 같음 검사를 수행합니다.

let value = 1
value = value + 1 // Produces a 'bool' value!

변형을 전혀 지원하지 않는 함수형 프로그래밍 언어도 있습니다. F#에서는 변형이 지원되지만 값에 대한 기본 동작은 아닙니다.

이 개념은 데이터 구조에도 적용됩니다. 함수형 프로그래밍에서 집합(및 수많은 요소) 같은 변경 불가능한 데이터 구조는 최초 예상과는 다른 방식으로 구현됩니다. 개념적으로 집합에 항목을 추가하는 것과 같이 집합은 변경되지 않으며 추가 값이 있는 집합을 생성합니다. 작동 원리를 살펴보면, 대부분의 경우 이 작업은 값을 효과적으로 추적하여 적절한 데이터 표현을 도출하는 다른 데이터 구조를 통해 수행됩니다.

값과 데이터 구조를 사용하는 이러한 방식은 대단히 중요합니다. 항목을 변경하는 모든 작업을 항목의 새 버전을 생성하는 작업처럼 취급해야 하기 때문입니다. 이렇게 하면 프로그램에서 같음 및 작음과 비교 같은 요소의 일관성을 유지할 수 있습니다.

다음 단계

다음 섹션에서는 함수 관련 심화 학습을 통해 함수형 프로그래밍에서 함수를 사용하는 다양한 방법을 살펴봅니다.

F# 에서 함수를 사용하면 함수를 자세히 살펴보고 다양한 컨텍스트에서 함수를 사용하는 방법을 보여 줍니다.

추가 참고 자료

함수적 사고방식 시리즈에서도 F#을 이용한 함수형 프로그래밍 관련 자료를 얻을 수 있습니다. 함수형 프로그래밍의 기본 사항을 실용적이고 읽기 쉬운 방식으로 살펴보며, F#을 이용해 개념을 설명합니다.