이번 글에선 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)