DEV Community

CloudHolic
CloudHolic

Posted on

F# Tutorial (6) - Pipeline

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

Composition

다음의 두 함수를 생각해봅시다.

let negate x = -1 * x
let square x = x * x
Enter fullscreen mode Exit fullscreen mode

그리고 이 두 함수를 순차적으로 적용해야 한다고 가정해봅시다. 그러면 다음과 같이 작성할 수 있습니다.

let temp = square 5     // 25
let result = negate temp    // -25
Enter fullscreen mode Exit fullscreen mode

이걸 한 문장으로 줄이면 다음과 같겠죠.

let result = negate (square 5)  // -25
Enter fullscreen mode Exit fullscreen mode

F#에서는 이걸 함수의 composition이라 부르고, 다음과 같이 정의합니다.
(함수의 이름이 연산자로만 구성되어 있을 경우, let으로 선언할 때 함수명 앞뒤로 괄호를 붙여 함수임을 명확히 합니다.)

let inline (>>) f g x = g (f x)
let inline (<<) f g x = f (g x)
Enter fullscreen mode Exit fullscreen mode

>>, << 연산자를 사용하며, 각각 정방향, 역방향 composition이라고 합니다.
f의 type이 'T1 -> 'T2, g의 type이 'T2 -> 'T3이면, f >> g의 type은 'T1 -> 'T3가 되겠죠?
>> 연산자 자체만 놓고 보자면 ('T1 -> 'T2) -> ('T2 -> 'T3) -> 'T1 -> 'T3가 될 겁니다.
물론 역방향 composition에 대해서도 같은 논리로 type을 구할 수 있겠고요.

Composition을 사용해서 위에서 정의한 negatesquare 함수를 합쳐보면 다음과 같이 쓸 수 있습니다.

let negateSquare = square >> negate
let result = negateSquare 5     // -25
Enter fullscreen mode Exit fullscreen mode

>>, << 연산자는 함수 2개를 받아서 함수를 리턴한다는 점을 기억하시면 되겠습니다.

Pipeline

Pipeline은 composition과 비슷하면서도 다릅니다. 다음의 두 함수를 생각해봅시다.

let oddNums values = List.filter (fun x -> x % 2 = 1) values
let squareNums values = List.map (fun x -> x * x) values
Enter fullscreen mode Exit fullscreen mode

두 함수 다 list를 인자로 받으며, oddNums 함수는 그 중 홀수인 원소만을 리턴하고, squareNums 함수는 각 원소를 제곱해서 리턴합니다. 이 두 함수를 합친 함수, 즉 주어진 list에서 홀수인 원소만을 찾아 그것을 제곱한 결과를 모은 list를 리턴하는 함수는 다음과 같이 쓸 수 있을 겁니다.

let combine1 values =
    let odds = List.filter (fun x -> x % 2 = 1) values
    let squares = List.map (fun x -> x * x) odds
    squares
Enter fullscreen mode Exit fullscreen mode

보시면 이 둘을 합친 함수 combine1에서는 인자로 values라는 list를 전달받고, 이 list는 odds로, 또 squares로 흘러갑니다. 그런데 사실 이전 결과값을 그대로 쓰는 것이 명확하다면, 이런 식으로 값의 이름을 계속 전달해주는 것은 불필요한 syntax가 되겠죠.
그래서 F#에서는 데이터를 자연스럽게 넘기기 위해 pipeline이라는 개념을 도입했습니다. 다음과 같이 말이죠.

let inline (|>) x f = f x
let inline (<|) f x = f x
Enter fullscreen mode Exit fullscreen mode

|>, <| 연산자를 사용하며, 각각 정방향, 역방향 pipeline이라고 부릅니다. f의 type을 'T, x의 type을 'T -> 'U라고 하면 |>의 type은 'T -> ('T -> 'U) -> 'U가 될 겁니다. 역방향도 같은 논리로 type을 구할 수 있겠죠.

역방향 pipeline의 경우 얼핏 보기엔 아무런 의미가 없어 보이지만 괄호 없이 연산의 우선순위를 바꿔 가독성을 높이는 데에 사용할 수 있습니다. 아주 간단한 예시를 보죠.

// 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]
Enter fullscreen mode Exit fullscreen mode

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

이제 위의 예시에서 사용했던 oddssquares를 합쳐볼까요? 다음과 같이 작성하면 됩니다.

let combine2 values =
    values
    |> List.filter (fun x -> x % 2 = 1)
    |> List.map (fun x -> x * x)
Enter fullscreen mode Exit fullscreen mode

처음에 인자로 받은 values에서 시작하여 그 값을 그대로 List.filter 함수의 마지막 인자로 넘깁니다. 함수의 리턴값은 그 함수가 마지막에 계산한 식의 리턴값이 되기 때문에 별도로 리턴할 필요는 없습니다.

만일 pipeline으로 데이터를 2개 전달하고 싶다면 어떻게 해야 할까요? F#에서는 이를 위해 ||>, <|| 연산을 제공합니다.

let inline (||>) (a, b) f = f a b
let inline (<||) f (a, b) = f a b
Enter fullscreen mode Exit fullscreen mode

위와 같이 tuple의 형태로 데이터를 넘기고, 이를 받는 함수에서는 튜플을 자동적으로 풀어서 계산을 수행합니다.

3개의 데이터를 전달하고 싶다면 같은 형태로 |||>, <||| 연산을 사용하면 됩니다.

let inline (|||>) (a, b, c) f = f a b c
let inline (<|||) f (a, b, c) = f a b c
Enter fullscreen mode Exit fullscreen mode

삼중 pipeline 연산자도 크게 다르지 않습니다. 인자가 1개 더 늘어났다는 점만 빼고 말이죠.

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



다음 글에서는 pattern matching에 대해 다루겠습니다.

Top comments (0)