Tuples

Tuples

여러 값을 묶어서 한 번에 다루고 싶을 때, F#에서는 튜플(Tuple)이라는 아주 편리한 구조를 써요. 이름은 붙어 있지 않지만 순서가 있는 값들의 묶음으로, 담긴 값들의 타입이 서로 달라도 상관없답니다. 함수 하나에서 여러 값을 동시에 돌려주거나, 인자들을 한꺼번에 받아야 할 때 특히 유용해요.

출처: F# Tuples — Microsoft Learn

본문

튜플이란

튜플은 이름이 없지만 순서가 있는 값들의 모임이에요. 담기는 값들은 타입이 서로 달라도 되고요. 참조 타입(Reference Type)으로 쓸 수도 있고 구조체(Struct)로 쓸 수도 있어요.

문법

기본적인 튜플 문법은 이렇게 생겼어요.

(element, ... , element)
struct(element, ... ,element )

위 문법에서 각 element 자리에는 F#에서 유효한 어떤 표현식이든 올 수 있어요.

예시

튜플을 만드는 예시를 몇 가지 볼게요. 같은 타입끼리 묶을 수도, 서로 다른 타입을 섞어 묶을 수도 있어요.

(1, 2)

// Triple of strings.
("one", "two", "three")

// Tuple of generic types.
(a, b)

// Tuple that has mixed types.
("one", 1, 2.0)

// Tuple of integer expressions.
(a + 1, b + 1)

// Struct Tuple of floats
struct (1.025f, 1.5f)

마지막 예시처럼 struct 키워드를 붙이면 구조체 튜플이 돼요. 평범한 괄호로 만드는 건 참조 튜플이고요.

개별 값 꺼내기

튜플에 담긴 값을 하나씩 꺼내 쓰려면 패턴 매칭(Pattern Matching)을 이용해요. 값에 이름을 붙여주고 바로 변수처럼 쓸 수 있어요.

let print tuple1 =
   match tuple1 with
    | (a, b) -> printfn "Pair %A %A" a b

match 표현식 밖에서도 let 바인딩으로 튜플을 분해(deconstruct)할 수 있어요.

let (a, b) = (1, 2)

// Or as a struct
let struct (c, d) = struct (1, 2)

함수의 입력으로 받은 튜플도 패턴 매칭으로 풀어서 쓸 수 있어요. 그러면 요소 하나하나에 바로 접근할 수 있죠.

let getDistance ((x1,y1): float*float) ((x2,y2): float*float) =
    // Note the ability to work on individual elements
    (x1*x2 - y1*y2) 
    |> abs 
    |> sqrt

튜플에서 딱 하나의 값만 필요할 때는 와일드카드 문자(언더스코어 _)를 쓰면 돼요. 쓰지 않을 값에 굳이 이름을 만들 필요가 없어져서 깔끔해요.

let (a, _) = (1, 2)

참조 튜플에서 구조체 튜플로 요소를 복사해 옮기는 일도 아주 간단해요.

// Create a reference tuple
let (a, b) = (1, 2)

// Construct a struct tuple from it
let struct (c, d) = struct (a, b)

fstsnd라는 함수도 있는데, 각각 튜플의 첫 번째 요소와 두 번째 요소를 돌려줘요. 단 이 함수들은 참조 튜플에서만 쓸 수 있어요.

let c = fst (1, 2)
let d = snd (1, 2)

세 번째 요소를 돌려주는 내장 함수는 없어요. 하지만 아래처럼 세 요소짜리 튜플(트리플)을 패턴 매칭으로 받는 함수를 직접 아주 쉽게 만들 수 있답니다.

let third (_, _, c) = c

전반적으로는, 개별 요소에 접근할 때 패턴 매칭을 쓰는 쪽이 더 권장되는 방식이에요.

튜플 활용하기

튜플은 함수에서 여러 값을 한 번에 돌려주기에 참 편리해요. 아래 예시는 정수 나눗셈을 수행해서, 몫을 튜플의 첫 번째 요소로, 나머지를 두 번째 요소로 돌려주는 함수랍니다.

let divRem a b =
   let x = a / b
   let y = a % b
   (x, y)

함수의 인자로도 튜플을 쓸 수 있어요. 보통 함수 문법이 암시하는 커링(Currying)을 피하고 싶을 때 특히 유용하죠.

let sumNoCurry (a, b) = a + b

일반적인 함수 정의 문법인 let sum a b = a + b를 보면, 이건 첫 번째 인자를 부분 적용(Partial Application)한 함수를 만들 수 있게 해주는 형태예요.

let sum a b = a + b

let addTen = sum 10
let result = addTen 95
// Result is 105.

이렇게 인자를 나란히 놓는 보통 문법과 달리, 튜플을 매개변수로 쓰면 커링이 비활성화돼요. 이 부분에 대한 자세한 내용은 Functions 문서의 "Partial Application of Arguments" 절을 참고하세요.

튜플 타입의 이름

튜플 타입의 이름을 적을 때는 요소 사이를 * 기호로 구분해요. 예를 들어 (10, 10.0, "ten")처럼 int, float, string으로 이루어진 튜플이라면 타입 이름은 아래처럼 써요.

int * float * string

구조체 튜플 타입의 별칭(Alias)을 만들 때는 바깥 괄호가 필수라는 점을 기억하세요.

type TupleAlias = string * float
type StructTupleAlias = (struct (string * float))

C# 튜플과의 연동

C#에서의 튜플은 구조체이며, F#의 구조체 튜플과 동일한 개념이에요. 그러니 C#과 상호 운용(Interoperate)해야 한다면 반드시 구조체 튜플을 사용해야 해요.

사실 어렵지 않아요. 예를 들어 튜플을 C# 클래스에 넘기고, 그 결과(역시 튜플)를 받아와야 하는 상황을 생각해볼게요.

namespace CSharpTupleInterop
{
    public static class Example
    {
        public static (int, int) AddOneToXAndY((int x, int y) a) =>
            (a.x + 1, a.y + 1);
    }
}

그리고 F# 코드 쪽에서는 구조체 튜플을 매개변수로 넘기고, 결과 역시 구조체 튜플로 받아오면 돼요.

open TupleInterop

let struct (newX, newY) = Example.AddOneToXAndY(struct (1, 2))
// newX is now 2, and newY is now 3

참조 튜플과 구조체 튜플 사이의 변환

참조 튜플과 구조체 튜플은 내부 표현(underlying representation)이 완전히 달라서 암시적으로는 서로 변환되지 않아요. 그래서 아래와 같은 코드는 컴파일되지 않아요.

// Will not compile!
let (a, b) = struct (1, 2)

// Will not compile!
let struct (c, d) = (1, 2)

// Won't compile!
let f(t: struct(int*int)): int*int = t

이 경우에는 한쪽 튜플을 패턴 매칭으로 풀고, 그 구성 요소들로 다른 쪽 튜플을 새로 만들어야 해요. 예를 들면 이렇게요.

// Pattern match on the result.
let (a, b) = (1, 2)

// Construct a new tuple from the parts you pattern matched on.
let struct (c, d) = struct (a, b)

참조 튜플의 컴파일된 형태

이 절에서는 튜플이 컴파일되면 어떤 형태가 되는지 설명해요. 다만 여기 내용은 .NET Framework 3.5 이하를 대상으로 하지 않는다면 사실 읽지 않아도 되는 부분이에요.

튜플은 컴파일되면 여러 제네릭 타입 중 하나의 객체로 바뀌는데, 그 타입들은 전부 System.Tuple이라는 이름을 공유하고 arity(즉 타입 매개변수의 개수)에 따라 오버로드돼요. 튜플 타입이 이런 형태로 보이는 경우는, C#이나 Visual Basic 같은 다른 언어에서 바라볼 때, 또는 F# 구문을 모르는 도구를 쓸 때예요. Tuple 타입은 .NET Framework 4에서 도입됐는데, 그보다 이전 버전의 .NET Framework를 대상으로 한다면 컴파일러가 F# Core Library 2.0 버전의 System.Tuple을 사용해요. 이 라이브러리의 타입들은 .NET Framework의 2.0, 3.0, 3.5 버전을 대상으로 하는 애플리케이션에서만 쓰이는데, .NET Framework 2.0과 4.0 F# 구성 요소 사이의 바이너리 호환성을 보장하기 위해 타입 전달(Type Forwarding)이 사용되죠.

구조체 튜플의 컴파일된 형태

구조체 튜플(예: struct (x, y))은 참조 튜플과는 근본적으로 달라요. 컴파일되면 arity, 즉 타입 매개변수의 개수에 따라 오버로드된 ValueTuple 타입으로 바뀌고, C# 튜플 및 Visual Basic 튜플과 동일하며 양방향으로 상호 운용돼요.

더 알아보기