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에 대해 다루겠습니다.

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (0)