F# Tutorial (5) - Function
Tags: fsharp
Published: 2023-01-03 02:46:59

이번 글에선 F#의 함수에 대해 알아보겠습니다.

#### 함수의 type

다음의 간단한 함수를 봅시다.
```fsharp
let f x = x + 1
```
이 함수의 이름은 `f`이고, 인자를 `x`란 이름으로 1개 받아 거기에 +1한 값을 리턴합니다. `x`의 type은 명시되지 않았지만 `+ 1`을 통해 `x`는 `int`임을 알 수 있습니다. 따라서 이 함수는 `int`를 1개 받아 `int`를 리턴하는 함수이며 `f`의 type은 `int -> int`로 정의할 수 있습니다. 즉, F#에서는 `(인자의 type) -> (리턴 type)`의 형태로 함수의 type을 결정하게 됩니다.

#### 함수 값

F#에서는 함수를 정의할 때 값을 정의할 때와 마찬가지로 `let`을 사용합니다.
```fsharp
let a = 1
let f x = x + 1
```
저번에 이에 대해서 언급했을 때는 `a` 역시 인자가 0개고 `1`을 리턴하는 함수로 생각하는게 이해하기 편할 것이라고 했습니다. 하지만 이는 이해를 돕기 위한 서술일 뿐 엄밀히 말해서 틀린 표현입니다. F#에서는 모든 함수를 값으로 취급합니다. 즉, `a`는 `1`로 정의된 값이고, `f`는 `x -> x + 1`로 정의된 값입니다.
이 말은 곧 타 언어에서의 변수와 함수의 구분이 없다는 의미입니다. 타 언어에서 함수 내에 변수를 선언하거나 함수의 인자로 변수를 받듯이 F#에서는 함수 내에 함수를 선언하거나 함수를 인자로 받는 것이 자연스럽다는 의미입니다. 애초에 둘은 동일한 것이니까요.
```fsharp
let result x =
    let function (a, b) = a + b
    100 * function (x, 2)    
    
let circleArea x =
    let pi = 3.1415
    pi * x * x
    
let apply1 (transform: int -> int) y = transform y
let increment x = x + 1
let result2 = apply1 increment 10			// 11
```
함수의 리턴값은 가장 마지막에 실행된 식의 값으로 결정됩니다. 물론 `return` 키워드를 사용해 명시적으로 지정해도 상관없습니다.
저번에도 언급했듯, 둘 이상의 인자가 있으면 괄호 없이 순서대로 나열합니다.
여담으로 위의 코드에서 `circleArea` 함수의 경우 3.1415를 곱하는 연산이 포함되었기 때문에 컴파일러는 이 함수의 리턴값을 `float`으로 판단합니다. 물론 인자에는 여전히 정수가 들어갈 수 있죠.

#### Lambda 함수

이제는 굉장히 흔한 개념이 되어버린 익명 함수입니다. 말 그대로 이름이 정해지지 않은 함수로 F#에서는 `fun` 키워드를 사용하여 나타냅니다.
```fsharp
fun x -> x + 1
fun (a:int) (b:int) -> a * b
fun x y -> let swap (a, b) = (b, a) in swap (x, y)
```
물론 익명 함수라고 해서 다른 함수를 정의하지 말라는 법은 없죠. 위의 코드에서처럼 얼마든지 다른 함수를 정의해서 사용해도 됩니다.
마지막 익명 함수의 `in` 키워드는 `let` 바인딩이 유효한 구간을 명시적으로 지정할 때 쓰입니다.

이러한 익명 함수는 물론 다른 함수의 인자 혹은 리턴값으로 사용될 수 있습니다.
```fsharp
let list = List.map (fun i -> i + 1) [1; 2; 3]	// [2; 3; 4]
```
`map`에 대한 건 이 글의 뒷부분에 나옵니다. 이런 방식으로 익명 함수를 사용할 수 있다는 것을 알아두시면 되겠습니다.

#### 재귀 함수

재귀 함수라는 개념 자체는 딱히 F#을 비롯한 FP언어만의 전유물은 아닙니다. F#이라고 해서 그 의미가 달라지지도 않고요. 하지만 F#에는 재귀 함수에 대한 특별한 룰이 한 가지 존재합니다. 함수를 정의할 때 `let`을 썼었는데, 재귀 함수의 경우 `rec` 키워드를 `let` 다음에 이어서 써야 합니다.
```fsharp
let rec fib n = 
    match n with
    | 1 -> 1
    | 2 -> 1
    | _ -> fib (n - 1) + fib (n - 2)
```
(`match` 및 `|`에 대해서는 나중에 다룹니다.)
매우 간단하게 구현한 피보나치 수열을 계산하는 함수입니다.

Haskell의 경우 재귀 함수라고 해서 특별한 키워드를 붙이진 않습니다. 하지만 F#에서는 반드시 `rec` 키워드가 필요한데요, 이는 내부적인 구현의 차이 때문입니다.
Haskell에서는 함수를 메모리에 저장할 때 실질적인 계산은 전혀 수행하지 않습니다. 그냥 하나의 'thunk'를 메모리에 할당하며 이 thunk의 계산이 실제로 필요할 때 그제서야 수행하게 돼죠. 이러한 구현 방식에선 이 thunk가 다른 thunk를 가리키든, 자기 자신을 가리키든 아무런 문제가 되지 않습니다.
하지만 F#에서는 다릅니다. F#에서는 함수를 메모리에 저장할 때 이미 계산을 수행하며, 따라서 그 함수는 내부적인 로직이 전부 정해져 있어야 합니다. 만일 함수 내에서 자기 자신을 다시 호출한다면 이는 미완성된 로직이며 무한루프가 발생할 수 있습니다. 그 때문에 일반 함수와 재귀 함수를 반드시 구분해야 하고, 이를 위해 `rec`이라는 키워드가 필요합니다.
어떻게 보면 Haskell과 F#의 철학의 차이를 엿볼 수 있는 부분이 되겠습니다.

#### Currying

위에서 함수의 type을 얘기할 때 인자가 1개인 함수에 대해서만 다뤘습니다. 인자가 2개 이상인 함수에 대해서 다루기 위해서는 currying 이라는 개념을 알아야 합니다.
다음의 함수를 생각해 봅시다.
```fsharp
let add (a:int) (b:int) = a + b
```
위에서 정의한 `add` 함수는 `int` 2개를 받아서 그걸 더한 결과를 리턴합니다.
그러면 `add 5`와 같이 인자를 불완전하게 제공하면 이건 뭐가 될까요?

`add 5`는 `add`에서 첫 번째 인자만 제공한 형태이므로 제대로 계산하기 위해서는 인자가 1개 더 필요하고, 이 두 번째 인자에 5를 더한 값을 리턴할 것입니다. 바꿔 말해 `add 5`는 `int`형 인자를 1개 받아 거기에 5를 더한 값을 리턴하는 함수라는 이야기고, 따라서 다음과 같은 코드가 성립할 수 있습니다.
```fsharp
let add5 = add 5
let result = add5 1		// 6
```
아예 `add 5`를 별도의 함수로 지정해도 아무 문제가 없습니다. 이처럼 F#에서는 어떤 함수가 받은 인자가 필요한 인자보다 부족하다면, 이것을 나머지 인자를 요구하는 새 함수로 취급합니다. 이러한 개념을 currying이라고 부르며, F#은 Haskell과 함께 FP 언어 중에서도 currying 개념을 잘 지원하는 언어 중 하나입니다.

그럼 currying을 염두에 두고 `add` 함수를 다시 살펴봅시다.
```fsharp
let add (a:int) (b:int) = a + b
```
`add` 함수에 대해 이번엔 다르게 해석해 보죠. 먼저 `add a` 함수는 `int`를 1개 받아서 그 값과 `a`를 더한 값을 리턴합니다.
그러면 `add` 함수는 `int`를 1개 받아서 `add a` 함수를 리턴한다고 볼 수 있습니다.
즉 `add`함수는 `int`를 1개 받아서 `int -> int` 함수를 리턴하는 함수이므로 `add`의 type은 `int -> (int -> int)`라고 볼 수 있습니다.
F#은 currying 지원이 잘 되어있기 때문에 결국 괄호를 생략해도 무방하며 `int -> int -> int`로 표기할 수 있습니다.

즉, F#에서는 인자가 여러 개 있는 함수라면 자동적으로 currying이 된 것으로 취급하며 따라서 중간에서 얼마든지 끊어서 쓸 수 있다는 이야기입니다. 그리고 그러한 함수의 type은 `(인자1의 type) -> (인자2의 type) -> ... -> (리턴 type)`의 형태가 되겠죠.

만일 특별한 이유가 있어 currying을 막고 싶다면 어떻게 해야 할까요? F#에서는 그럴 경우 tuple을 사용하라고 합니다.
```fsharp
let add1 a b = a + b
let add2 (a, b) = a + b
```
이 두 함수는 결과적으로는 같은 일을 하지만 세부적으론 좀 다릅니다. `add1`의 type은 `int -> int -> int`이고, `add2`의 type은 `int * int -> int`입니다. 또한 `add1`은 currying이 가능하여 `add1 1`과 같은 식으로 쓸 수 있지만, `add2`는 currying이 불가능하여 이런 식으로 나눌 수 없습니다.

#### inline 함수

F#에도 inline 함수가 있으며 함수 선언에 `inline` 키워드를 붙여서 만듭니다. 
```fsharp
let inline increment x = x + 1
```
여기까지만 보면 타 언어에서도 흔히 볼 수 있는 inline 함수지만, FP 언어로써의 특성과 F#의 type inference로 인해 다소 특이한 사용법이 존재합니다.

```fsharp
let add1 a b = a + b
let inline add2 a b = a + b
```
다시 한번 `add` 함수를 가져오겠습니다. `add1` 함수의 경우 두 인자 `a`와 `b`의 type이 정해져있지 않지만 F#의 type inference로 인해 적절한 type으로 고정되게 됩니다.
```fsharp
let result1 = add1 1 2			// 3
let result2 = add1 "dog" "cat"		// error!
```
즉, `result1`을 실행하면서 F# compiler는 `add1`이 `int -> int -> int` 함수라는 것을 알게 되고, 따라서 `result2`를 실행할 땐 주어진 인자가 `int`가 아니므로 에러가 발생하게 됩니다.

```fsharp
let result3 = add2 1 2			// 3
let result4 = add2 "dog" "cat"		// "dogcat"
```
inline 함수는 좀 달라집니다. `result3`을 실행하면서 F# compiler는 `add2`의 type을 고려하지 않고 본문을 그대로 옮겨놓으며, 따라서 여기서의 `add2`는 `int -> int -> int`가 됩니다. `result 4`를 실행할 때에도 역시 본문을 그대로 옮겨놓으며, 따라서 여기서의 `add2`는 `string -> string -> string`이 됩니다. 즉, 어떠한 type을 가져다놔도 `add2` 함수 자체에서 에러가 나는 것이 아닌 이상 문제가 없습니다.

이를 통해 동일한 표현식의, 서로 다른 type에서 적용되어야 할 함수를 `inline` 키워드를 사용하여 마치 오버로딩과 유사하게 흉내낼 수 있습니다.

#### iter, map, filter, fold

이번엔 저번에 미처 다루지 못한 나머지 collection 내장 함수들 중 일부를 다루겠습니다.
여기 적힌 함수들은 foldBack을 제외하면 전부 list, 배열, sequence에서 사용할 수 있으며, 각각의 collection에서 특정 조건에 따라 함수를 사용할 수 있게 해주는 내장 함수입니다.

**1) iter**
`iter`는 collection의 각각의 원소들에 대해 주어진 함수를 실행할 수 있게 해줍니다.
```fsharp
let list1 = [1; 2; 3]
List.iter (fun x -> printf "%d " x) list1
List.iteri (fun i x -> printfn "Index %d: %d " i x) list1
```
위의 코드에서도 알 수 있듯이 collection의 각각의 원소에 대해 같이 인자로 넘겨주는 함수를 실행시킬 뿐 그 리턴값은 전혀 신경쓰지 않습니다. `iter`는 각 원소를 넘겨주며, 만일 index도 같이 필요하다면 `iteri`를 사용하시면 됩니다. 이 경우 index, 원소 순서대로 인자를 넘겨주게 됩니다.

만일 2개의 collection에 대해 사용하고자 한다면 `iter2`, `iteri2` 함수를 사용하면 됩니다.

**2) map**
`map`은 collection의 각각의 원소들에 대해 주어진 함수를 실행하여, 그 결과를 모아 새 collection으로 리턴해줍니다.
```fsharp
let list1 = [1; 2; 3]
let result1 = List.map (fun x -> x + 1) list1		// [2; 3; 4]
let result2 = List.mapi (fun i x -> x + i) list1	// [1; 3; 5]
```
`iter`와의 차이점을 눈치채셨나요? `iter`는 리턴값 없이 실행만 할 뿐이지만, `map`은 구체적인 리턴값이 존재해야 합니다. 그래야 그 결과를 모아 collection으로 만들어 줄 수 있거든요. `iter`와 `iteri`의 관계와 마찬가지로, index가 필요하다면 `mapi`를 사용하면 됩니다.

또한 `iter`와 마찬가지로 `map2`, `mapi2`, `map3`, `mapi3` 등 여러 개의 collection에 대한 함수들도 존재합니다.

**3) filter**
`filter`는 collection의 각각의 원소들에 대해 주어진 함수를 실행하여, 그 중 `true`가 나오는 것들만 모아 새 collection으로 리턴해줍니다.
```fsharp
let list1 = [1; 2; 3]
let evenList = List.filter (fun x -> x % 2 = 0) list1	// [2]
```
`filter`를 사용할 때 같이 제공되어야 할 함수는 반드시 `bool`을 리턴해야 합니다.

또한 `map`과 `filter`를 합친 느낌의 `choose`라는 내장 함수도 존재합니다. `choose`는 collection의 각각의 원소들에 대해 주어진 함수를 실행하여, 그 중 `Some(x)`가 나오는 것들의 `x`값만 모아 새 collection으로 리턴해줍니다.

**4) fold**
여태까지 살펴본 내장 함수들은 전부 각 원소들에 대해 독립적으로 수행했습니다. 하지만 때로는 이전 원소까지의 결과를 누적해서 적용해야 하는 경우도 있죠. 그럴 때 `fold`를 사용합니다.
`fold`는 인자로 넘겨줄 함수가 원소값을 받을 인자 외에 'accumulator'라는 인자를 하나 더 가져야 합니다. 이 accumulator는 그 이전 원소까지의 실행 결과를 담게 되죠.
```fsharp
let sumList list = List.fold (fun acc x -> acc + x) 0 list
let result1 = sumList [1; 2; 3; 4; 5]		// 15

let reverseList list = List.fold (fun acc x -> x::acc) [] list
let result2 = reverseList [1; 2; 3; 4; 5]	// [5; 4; 3; 2; 1]
```
위의 코드에서 보시다시피 accumulator에는 무엇이든 올 수 있으며, 그 type은 `fold`의 두 번째 인자인 '초기 accumulator 값'으로 정해집니다.

`fold`에도 파생된 다른 함수들이 존재합니다. 먼저, collection을 역순으로 탐색하는 `foldBack`이 있으며, 2개의 collection에 대해 동일한 작업을 수행하는 `fold2`와 `foldBack2` 함수가 존재합니다. 주의할 점은 `foldBack`과 `foldBack2` 함수의 경우 해당 collection의 마지막부터 역순으로 탐색하기 때문에, 무한대의 개념을 다루고 lazy evaluation을 수행하는 sequence에서는 지원되지 않는다는 것입니다.

또한 `fold`는 모든 중간 계산을 생략하고 제일 마지막에 계산된 값을 리턴하는데, 그 중간 과정을 모두 보고 싶다면 `scan`을 사용하면 됩니다. `scan`은 주어진 함수를 각 원소에 대해 순차적으로 실행하며 각각의 원소마다 나오는 중간 결과값(즉, accumulator 값)을 모은 collection을 리턴합니다.

---
혹시 지금까지의 예시 코드 중에 조건문과 반복문이 전혀 없다는 것을 눈치채셨나요? FP에서는 조건문과 반복문에 직접적으로 대응되는 `if` 및 `for`, `while` 등을 사용하는 것을 지양합니다. 직접적인 control flow를 사용하게 되면 함수가 간단하지 못하거든요. 물론 F#은 C#과의 호환성을 위해 해당 statement가 존재하지만, 가급적 사용하지 않는 것이 좋습니다.

그렇다고 해서 조건문과 반복문에 해당하는 기능이 아예 존재하지 않는다는 이야기는 아닙니다. 반복문의 경우 위에서 살펴본 재귀 함수, 혹은 방금 다뤘던 다양한 내장 함수를 사용하면 되며 이게 FP 스타일에 더 알맞습니다. 

조건문을 FP 스타일로 어떻게 쓰는지는 나중에 알아보겠습니다.

#### Etc.

지금까지는 매우 단편적인 함수들만 살펴봤습니다. 하지만 F#도 엄연히 자체적으로 실행이 가능한 언어인 만큼, 진입점이 존재합니다.
F#의 진입점은 다음과 같이 정의됩니다.
```fsharp
[<EntryPoint>]
let main args =
    // Do whatever wants.
    0
```
`main` 함수의 위에 `[<EntryPoint>]`라는 특성을 달아야 하며, `main` 함수의 type은 `string array -> int`로 고정됩니다.
리턴값은 다른 C 계열 언어가 그렇듯 `0`이면 정상 종료, 그 이외의 값이면 비정상 종료입니다.

---
지금까지 함수를 선언할 때 항상 `let`만을 사용했습니다. 물론 `let`이 메모리에 값을 대입시키는 거의 유일한 방법인 것은 맞으나, 때로는 굳이 그 결과를 저장하지 않고 수행'만' 필요한 경우도 있습니다. 이럴 때 `do`를 사용합니다.
```fsharp
do printf "Hello World"
```
`printf` 함수를 실행하기만 하고 그 리턴값을 취하지 않는다는 의미이며, 많은 경우 `do`를 생략할 수 있습니다.
주의할 점은, `do`를 사용할 경우 그 다음에 오는 함수의 리턴값은 반드시 `unit`이어야 한다는 점입니다.
<br/><br/>
다음 글에서는 F#만의 고유한 문법인 pipeline에 대해 다루겠습니다.