이번 글에선 F#만의 독특한 문법인 pipeline에 대해 알아보겠습니다.
Composition
다음의 두 함수를 생각해봅시다.
let negate x = -1 * x
let square x = x * x
그리고 이 두 함수를 순차적으로 적용해야 한다고 가정해봅시다. 그러면 다음과 같이 작성할 수 있습니다.
let temp = square 5 // 25
let result = negate temp // -25
이걸 한 문장으로 줄이면 다음과 같겠죠.
let result = negate (square 5) // -25
F#에서는 이걸 함수의 composition이라 부르고, 다음과 같이 정의합니다.
(함수의 이름이 연산자로만 구성되어 있을 경우, let으로 선언할 때 함수명 앞뒤로 괄호를 붙여 함수임을 명확히 합니다.)
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 함수를 합쳐보면 다음과 같이 쓸 수 있습니다.
let negateSquare = square >> negate
let result = negateSquare 5 // -25
>>, << 연산자는 함수 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
두 함수 다 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
보시면 이 둘을 합친 함수 combine1에서는 인자로 values라는 list를 전달받고, 이 list는 odds로, 또 squares로 흘러갑니다. 그런데 사실 이전 결과값을 그대로 쓰는 것이 명확하다면, 이런 식으로 값의 이름을 계속 전달해주는 것은 불필요한 syntax가 되겠죠.
그래서 F#에서는 데이터를 자연스럽게 넘기기 위해 pipeline이라는 개념을 도입했습니다. 다음과 같이 말이죠.
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의 경우 얼핏 보기엔 아무런 의미가 없어 보이지만 괄호 없이 연산의 우선순위를 바꿔 가독성을 높이는 데에 사용할 수 있습니다. 아주 간단한 예시를 보죠.
// 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를 합쳐볼까요? 다음과 같이 작성하면 됩니다.
let combine2 values =
values
|> List.filter (fun x -> x % 2 = 1)
|> List.map (fun x -> x * x)
처음에 인자로 받은 values에서 시작하여 그 값을 그대로 List.filter 함수의 마지막 인자로 넘깁니다. 함수의 리턴값은 그 함수가 마지막에 계산한 식의 리턴값이 되기 때문에 별도로 리턴할 필요는 없습니다.
만일 pipeline으로 데이터를 2개 전달하고 싶다면 어떻게 해야 할까요? F#에서는 이를 위해 ||>, <|| 연산을 제공합니다.
let inline (||>) (a, b) f = f a b
let inline (<||) f (a, b) = f a b
위와 같이 tuple의 형태로 데이터를 넘기고, 이를 받는 함수에서는 튜플을 자동적으로 풀어서 계산을 수행합니다.
3개의 데이터를 전달하고 싶다면 같은 형태로 |||>, <||| 연산을 사용하면 됩니다.
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으로 전달해야 한다면 직접 구현하셔야 합니다.
다음 글에서는 pattern matching에 대해 다루겠습니다.
Top comments (0)