익명 레코드
익명 레코드 (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 익명 레코드를 IsByRefLike나 IsReadOnly로 선언하는 것은 불가능해요. IsByRefLike와 IsReadOnly 익명 레코드에 대한 오픈 언어 제안이 있어요.