F# Tutorial (6) - Pipeline
Tags: fsharp
Published: 2023-01-03 02:49:36

이번 글에선 F#만의 독특한 문법인 pipeline에 대해 알아보겠습니다.

#### Composition

다음의 두 함수를 생각해봅시다.
```fsharp
let negate x = -1 * x
let square x = x * x
```
그리고 이 두 함수를 순차적으로 적용해야 한다고 가정해봅시다. 그러면 다음과 같이 작성할 수 있습니다.
```fsharp
let temp = square 5		// 25
let result = negate temp	// -25
```
이걸 한 문장으로 줄이면 다음과 같겠죠.
```fsharp
let result = negate (square 5)	// -25
```
F#에서는 이걸 함수의 composition이라 부르고, 다음과 같이 정의합니다.
(함수의 이름이 연산자로만 구성되어 있을 경우, `let`으로 선언할 때 함수명 앞뒤로 괄호를 붙여 함수임을 명확히 합니다.)
```fsharp
let inline (>>) f g x = g (f x)
let inline (<<) f g x = f (g x)
```
`>>`, `<<` 연산자를 사용하며, 각각 정방향, 역방향 composition이라고 합니다.
`f`의 type이 `'T1 -> 'T2`, `g`의 type이 `'T2 -> 'T3`이면, `f >> g`의 type은 `'T1 -> 'T3`가 되겠죠?
`>>` 연산자 자체만 놓고 보자면 `('T1 -> 'T2) -> ('T2 -> 'T3) -> 'T1 -> 'T3`가 될 겁니다.
물론 역방향 composition에 대해서도 같은 논리로 type을 구할 수 있겠고요.

Composition을 사용해서 위에서 정의한 `negate`와 `square` 함수를 합쳐보면 다음과 같이 쓸 수 있습니다.
```fsharp
let negateSquare = square >> negate
let result = negateSquare 5		// -25
```
`>>`, `<<` 연산자는 함수 2개를 받아서 함수를 리턴한다는 점을 기억하시면 되겠습니다.

#### Pipeline

Pipeline은 composition과 비슷하면서도 다릅니다. 다음의 두 함수를 생각해봅시다.
```fsharp
let oddNums values = List.filter (fun x -> x % 2 = 1) values
let squareNums values = List.map (fun x -> x * x) values
```
두 함수 다 list를 인자로 받으며, `oddNums` 함수는 그 중 홀수인 원소만을 리턴하고, `squareNums` 함수는 각 원소를 제곱해서 리턴합니다. 이 두 함수를 합친 함수, 즉 주어진 list에서 홀수인 원소만을 찾아 그것을 제곱한 결과를 모은 list를 리턴하는 함수는 다음과 같이 쓸 수 있을 겁니다.
```fsharp
let combine1 values =
    let odds = List.filter (fun x -> x % 2 = 1) values
    let squares = List.map (fun x -> x * x) odds
    squares
```
보시면 이 둘을 합친 함수 `combine1`에서는 인자로 `values`라는 list를 전달받고, 이 list는 `odds`로, 또 `squares`로 흘러갑니다. 그런데 사실 이전 결과값을 그대로 쓰는 것이 명확하다면, 이런 식으로 값의 이름을 계속 전달해주는 것은 불필요한 syntax가 되겠죠.
그래서 F#에서는 데이터를 자연스럽게 넘기기 위해 pipeline이라는 개념을 도입했습니다. 다음과 같이 말이죠.
```fsharp
let inline (|>) x f = f x
let inline (<|) f x = f x
```
`|>`, `<|` 연산자를 사용하며, 각각 정방향, 역방향 pipeline이라고 부릅니다. `f`의 type을 `'T`, `x`의 type을 `'T -> 'U`라고 하면 `|>`의 type은 `'T -> ('T -> 'U) -> 'U`가 될 겁니다. 역방향도 같은 논리로 type을 구할 수 있겠죠.

역방향 pipeline의 경우 얼핏 보기엔 아무런 의미가 없어 보이지만 괄호 없이 연산의 우선순위를 바꿔 가독성을 높이는 데에 사용할 수 있습니다. 아주 간단한 예시를 보죠.
```fsharp
// Will be [2; 4; 6; 8; 10]
let result = [1..10] |> List.filter (fun x -> x % 2 = 0)

// Will be [4; 16; 36; 64; 100]
let result2 = List.filter (fun x -> x % 2 = 0) <| List.map (fun x -> x * x) [1..10]
```
`result2`를 역방향 pipeline 없이 사용하려면 `List.filter (fun x -> x % 2 = 0) (List.map (fun x -> x * x) [1..10])`과 같이 써야 합니다. 이는 너무 길고 괄호가 중첩되어서 들어가 복잡하기까지 하죠.

이제 위의 예시에서 사용했던 `odds`와 `squares`를 합쳐볼까요? 다음과 같이 작성하면 됩니다.
```fsharp
let combine2 values =
    values
    |> List.filter (fun x -> x % 2 = 1)
    |> List.map (fun x -> x * x)
```
처음에 인자로 받은 `values`에서 시작하여 그 값을 그대로 `List.filter` 함수의 마지막 인자로 넘깁니다. 함수의 리턴값은 그 함수가 마지막에 계산한 식의 리턴값이 되기 때문에 별도로 리턴할 필요는 없습니다.

만일 pipeline으로 데이터를 2개 전달하고 싶다면 어떻게 해야 할까요? F#에서는 이를 위해 `||>`, `<||` 연산을 제공합니다.
```fsharp
let inline (||>) (a, b) f = f a b
let inline (<||) f (a, b) = f a b
```
위와 같이 tuple의 형태로 데이터를 넘기고, 이를 받는 함수에서는 튜플을 자동적으로 풀어서 계산을 수행합니다.

3개의 데이터를 전달하고 싶다면 같은 형태로 `|||>`, `<|||` 연산을 사용하면 됩니다.
```fsharp
let inline (|||>) (a, b, c) f = f a b c
let inline (<|||) f (a, b, c) = f a b c
```
삼중 pipeline 연산자도 크게 다르지 않습니다. 인자가 1개 더 늘어났다는 점만 빼고 말이죠.

F#에서 제공하는 pipeline 연산자는 여기까지입니다. 만일 4개 이상의 데이터를 pipeline으로 전달해야 한다면 직접 구현하셔야 합니다.

<br/><br/>다음 글에서는 pattern matching에 대해 다루겠습니다.