익명 레코드

익명 레코드 (Anonymous Records)

F#의 익명 레코드를 설명하는 공식 문서를 한국어로 옮겨 봤어요. 이름을 지어 선언할 필요 없이 필요한 값을 바로 묶어 쓰는 기능인데, F# 레코드를 자주 쓰는 분이라면 금방 익힐 수 있어요. 코드와 시그니처는 원문 그대로 보존했으니 눈으로 비교하며 읽으면 더 좋아요.

출처: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/anonymous-records

본문

익명 레코드(anonymous record)는 사용 전에 선언할 필요가 없는, 이름 붙은 값들의 단순한 묶음이라는 뜻이에요. 익명 레코드는 struct로도, 참조 타입(reference type)으로도 선언할 수 있는데, 기본값은 참조 타입이에요.

구문 (Syntax)

다음 예제들이 익명 레코드의 구문을 보여줘요. [item]처럼 대괄호로 감싼 부분은 선택 항목이에요.

// Construct an anonymous record
let value-name = [struct] {| Label1: Type1; Label2: Type2; ...|}

// Use an anonymous record as a type parameter
let value-name = Type-Name<[struct] {| Label1: Type1; Label2: Type2; ...|}>

// Define a parameter with an anonymous record as input
let function-name (arg-name: [struct] {| Label1: Type1; Label2: Type2; ...|}) ...

기본 사용법 (Basic usage)

익명 레코드는 인스턴스를 만들기 전에 선언할 필요가 없는 F# 레코드 타입이라고 생각하면 가장 이해하기 쉬워요.

예를 들어, 익명 레코드를 만들어 반환하는 함수를 이렇게 써 볼 수 있어요.

open System

let getCircleStats radius =
    let d = radius * 2.0
    let a = Math.PI * (radius ** 2.0)
    let c = 2.0 * Math.PI * radius

    {| Diameter = d; Area = a; Circumference = c |}

let r = 2.0
let stats = getCircleStats r
printfn "Circle with radius: %f has diameter %f, area %f, and circumference %f"
    r stats.Diameter stats.Area stats.Circumference

다음 예제는 앞의 예제를 확장해서, 익명 레코드를 입력으로 받는 printCircleStats 함수를 만든 거예요.

open System

let getCircleStats radius =
    let d = radius * 2.0
    let a = Math.PI * (radius ** 2.0)
    let c = 2.0 * Math.PI * radius

    {| Diameter = d; Area = a; Circumference = c |}

let printCircleStats r (stats: {| Area: float; Circumference: float; Diameter: float |}) =
    printfn "Circle with radius: %f has diameter %f, area %f, and circumference %f"
        r stats.Diameter stats.Area stats.Circumference

let r = 2.0
let stats = getCircleStats r
printCircleStats r stats

입력 타입과 "모양(shape)"이 같지 않은 익명 레코드로 printCircleStats를 호출하면 컴파일이 실패해요.

printCircleStats r {| Diameter = 2.0; Area = 4.0; MyCircumference = 12.566371 |}
// Two anonymous record types have mismatched sets of field names
// '["Area"; "Circumference"; "Diameter"]' and '["Area"; "Diameter"; "MyCircumference"]'

struct 익명 레코드 (Struct anonymous records)

익명 레코드는 선택 키워드인 struct를 붙여 struct로도 정의할 수 있어요. 아래 예제는 앞의 예제를 확장해 struct 익명 레코드를 만들고 소비해요.

open System

let getCircleStats radius =
    let d = radius * 2.0
    let a = Math.PI * (radius ** 2.0)
    let c = 2.0 * Math.PI * radius

    // Note that the keyword comes before the '{| |}' brace pair
    struct {| Area = a; Circumference = c; Diameter = d |}

// the 'struct' keyword also comes before the '{| |}' brace pair when declaring the parameter type
let printCircleStats r (stats: struct {| Area: float; Circumference: float; Diameter: float |}) =
    printfn "Circle with radius: %f has diameter %f, area %f, and circumference %f"
        r stats.Diameter stats.Area stats.Circumference

let r = 2.0
let stats = getCircleStats r
printCircleStats r stats

structness 유추 (Structness inference)

struct 익명 레코드는 "structness 유추"도 지원하기 때문에, 호출하는 자리에서 struct 키워드를 일일이 적지 않아도 돼요. 아래 예제에서는 printCircleStats를 호출할 때 struct 키워드를 생략하고 있어요.

let printCircleStats r (stats: struct {| Area: float; Circumference: float; Diameter: float |}) =
    printfn "Circle with radius: %f has diameter %f, area %f, and circumference %f"
        r stats.Diameter stats.Area stats.Circumference

printCircleStats r {| Area = 4.0; Circumference = 12.6; Diameter = 12.6 |}

반대로 입력 타입이 struct 익명 레코드가 아닌데 struct를 명시하는 패턴은 컴파일이 실패해요.

다른 타입 안에 익명 레코드 끼워 넣기 (Embedding anonymous records within other types)

판별 공용체(discriminated union)의 각 case를 레코드로 선언하면 유용할 때가 많아요. 그런데 레코드 안 데이터가 판별 공용체 자체와 같은 타입이면, 모든 타입을 서로 재귀(상호 재귀, mutually recursive) 형태로 정의해야 해요. 익명 레코드를 쓰면 이런 제약을 피할 수 있어요. 아래는 그런 타입과, 그 위에서 패턴 매칭하는 함수의 예시예요.

type FullName = { FirstName: string; LastName: string }

// Note that using a named record for Manager and Executive would require mutually recursive definitions.
type Employee =
    | Engineer of FullName
    | Manager of {| Name: FullName; Reports: Employee list |}
    | Executive of {| Name: FullName; Reports: Employee list; Assistant: Employee |}

let getFirstName e =
    match e with
    | Engineer fullName -> fullName.FirstName
    | Manager m -> m.Name.FirstName
    | Executive ex -> ex.Name.FirstName

복사·갱신 표현식 (Copy and update expressions)

익명 레코드는 복사·갱신 표현식(copy and update expression)으로 새 인스턴스를 만드는 걸 지원해요. 예를 들어 기존 익명 레코드의 데이터를 복사해 새 익명 레코드를 만들 수 있어요.

let data = {| X = 1; Y = 2 |}
let data' = {| data with Y = 3 |}

그런데 이름 붙은 레코드와 달리, 익명 레코드는 복사·갱신 표현식으로 완전히 다른 형태의 레코드까지 만들 수 있어요. 아래 예제는 앞 예제의 같은 익명 레코드를 새로운 익명 레코드로 확장한 거예요.

let data = {| X = 1; Y = 2 |}
let expandedData = {| data with Z = 3 |} // Gives {| X=1; Y=2; Z=3 |}

이름 붙은 레코드의 인스턴스로부터 익명 레코드를 만드는 것도 가능해요.

type R = { X: int }
let data = { X = 1 }
let data' = {| data with Y = 2 |} // Gives {| X=1; Y=2 |}

참조형 익명 레코드와 struct 익명 레코드 사이에서도 데이터를 복사할 수 있어요.

// Copy data from a reference record into a struct anonymous record
type R1 = { X: int }
let r1 = { X = 1 }

let data1 = struct {| r1 with Y = 1 |}

// Copy data from a struct record into a reference anonymous record
[<Struct>]
type R2 = { X: int }
let r2 = { X = 1 }

let data2 = {| r1 with Y = 1 |}

// Copy the reference anonymous record data into a struct anonymous record
let data3 = struct {| data2 with Z = r2.X |}

익명 레코드의 특징 (Properties of anonymous records)

익명 레코드가 어떻게 쓰일 수 있는지를 제대로 이해하려면 꼭 알아야 할 특징들이 몇 가지 있어요.

익명 레코드는 구조적 동등성과 비교를 사용해요

레코드 타입처럼 익명 레코드도 구조적으로 동등(equatable)하고 비교(comparable)가 가능해요. 단, 이는 레코드 타입과 마찬가지로 구성 타입이 모두 동등성과 비교를 지원할 때만 성립해요. 또 두 익명 레코드가 동등성이나 비교를 지원하려면 "모양"이 같아야 해요.

{| a = 1+1 |} = {| a = 2 |} // true
{| a = 1+1 |} > {| a = 1 |} // true

// error FS0001: Two anonymous record types have mismatched sets of field names '["a"]' and '["a"; "b"]'
{| a = 1 + 1 |} = {| a = 2;  b = 1|}

익명 레코드는 직렬화할 수 있어요

익명 레코드는 이름 붙은 레코드처럼 직렬화할 수 있어요. 아래는 Newtonsoft.Json을 쓴 예시예요.

open Newtonsoft.Json

let phillip' = {| name="Phillip"; age=28 |}
let philStr = JsonConvert.SerializeObject(phillip')

let phillip = JsonConvert.DeserializeObject<{|name: string; age: int|}>(philStr)
printfn $"Name: {phillip.name} Age: %d{phillip.age}"

익명 레코드는 네트워크로 가벼운 데이터를 보낼 때 유용한데, 직렬화/역직렬화할 타입의 도메인을 미리 정의할 필요가 없기 때문이에요.

익명 레코드는 C# 익명 타입과 상호 운용돼요

C# 익명 타입을 요구하는 .NET API를 써야 하는 상황이 있을 수 있어요. C# 익명 타입은 익명 레코드를 사용하면 아주 쉽게 상호 운용할 수 있어요. 아래 예제는 익명 타입을 요구하는 LINQ 오버로드를 익명 레코드로 호출하는 방법을 보여줘요.

open System.Linq

let names = [ "Ana"; "Felipe"; "Emilia"]
let nameGrouping = names.Select(fun n -> {| Name = n; FirstLetter = n[0] |})
for ng in nameGrouping do
    printfn $"{ng.Name} has first letter {ng.FirstLetter}"

.NET 전반에 익명 타입을 넘겨야 하는 API가 아주 많아요. 그때 유용한 도구가 바로 익명 레코드예요.

제약 사항 (Limitations)

익명 레코드는 사용하는 데 몇 가지 제약이 있어요. 어떤 것은 설계상 어쩔 수 없는 것이고, 어떤 것은 추후 개선될 여지가 있는 것이에요.

패턴 매칭의 제약

익명 레코드는 이름 붙은 레코드와 달리 패턴 매칭을 지원하지 않아요. 그 이유는 세 가지예요.

  • 익명 레코드는 구조적 하위 타입(structural subtyping)을 지원하지 않아 정확한 필드 일치가 필요하기 때문에, 이름 붙은 레코드와 달리 패턴이 익명 레코드의 모든 필드를 다 고려해야 해요.
  • (1) 때문에 패턴 매칭 표현식에 추가 패턴을 둘 수 없어요. 서로 다른 각각의 패턴은 서로 다른 익명 레코드 타입을 뜻하게 되거든요.
  • (2) 때문에 어떤 익명 레코드 패턴이든 "점(dot)" 표기법을 쓰는 것보다 장황해져요.

제한된 맥락에서 패턴 매칭을 허용하자는 오픈 언어 제안(open language suggestion)이 있어요.

가변성(mutability)의 제약

현재는 가변(mutable) 데이터를 가진 익명 레코드를 정의할 수 없어요. 가변 데이터를 허용하자는 오픈 언어 제안이 있고요.

struct 익명 레코드의 제약

struct 익명 레코드를 IsByRefLikeIsReadOnly로 선언하는 것은 불가능해요. IsByRefLikeIsReadOnly 익명 레코드에 대한 오픈 언어 제안이 있어요.

더 알아보기 (Learn more)