유연한 형식

유연한 형식 (Flexible Types)

유연한 형식(Flexible Type)은 이 문서에서 다루는 핵심 개념이에요. 특정 형식과 호환되는 형식을 파라미터·변수·값에 쓸 수 있게 해 주는 표기법인데, 여기서 "호환"은 클래스나 인터페이스의 객체지향 계층 구조에서 값이 놓인 위치로 결정돼요. 특히 형식 계층에서 더 높은 위치의 형식으로 자동 변환이 일어나지 않는 상황에서도, 계층 안의 어떤 형식이나 어떤 인터페이스 구현체든 내 기능이 동작하도록 만들고 싶을 때 유용하죠. 예시 코드와 함께 차근차근 살펴볼게요.

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

본문

개요

유연한 형식 표기(flexible type annotation)란, 어떤 파라미터·변수·값이 지정된 형식과 호환되는 형식을 가질 수 있음을 나타내요. 여기서 호환이란 클래스나 인터페이스로 이루어진 객체지향 계층 구조에서의 위치로 판단해요. 자동 변환이 일어나지 않는 경우에도, 계층 구조에 속한 어떤 형식이나 특정 인터페이스를 구현한 어떤 형식이든 내 기능이 동작하도록 만들고 싶을 때 특히 쓸모가 있죠.

문법 (Syntax)

#type

설명 (Remarks)

위 문법에서 type은 기본 형식(base type)이나 인터페이스를 나타내요.

유연한 형식은 사실 "허용되는 형식을 그 기본 형식 또는 인터페이스와 호환되는 형식으로 제한하는 제약(constraint)을 가진 제네릭 형식"과 똑같아요. 즉 아래 두 줄의 코드는 서로 동일해요.

#SomeType

'T when 'T :> SomeType

유연한 형식은 여러 상황에서 쓰이는데, 대표적으로 고계 함수(higher order function, 함수를 인자로 받는 함수)가 있어요. 고계 함수가 유연한 형식을 반환하도록 만들면 아주 편리하죠. 아래 예시를 보면, iterate2에서 시퀀스 인자에 유연한 형식을 쓴 덕분에 시퀀스·배열·리스트는 물론 그 밖의 어떤 열거 가능한(enumerable) 형식을 생성하는 함수와도 잘 동작해요.

두 함수를 비교해 볼게요. 하나는 시퀀스를 반환하고, 다른 하나는 유연한 형식을 반환해요.

let iterate1 (f : unit -> seq<int>) =
 for e in f() do printfn "%d" e
let iterate2 (f : unit -> #seq<int>) =
 for e in f() do printfn "%d" e

// 리스트를 받는 함수를 넘기려면 캐스트(변환)가 필요해요.
iterate1 (fun () -> [1] :> seq<int>)

// 반환 값에 유연한 형식을 쓴 버전에는
// 리스트를 받는 함수를 그냥 넘겨도 괜찮아요.
iterate2 (fun () -> [1])

또 다른 예시로는 라이브러리 함수인 Seq.concat을 살펴볼게요.

val concat: sequences:seq<#seq<'T>> -> seq<'T>

이 함수에는 아래와 같은 열거 가능한 시퀀스들을 자유롭게 넘길 수 있어요.

  • 리스트의 리스트
  • 리스트의 배열
  • 배열의 리스트
  • 시퀀스의 배열
  • 열거 가능한 시퀀스의 그 어떤 조합이든

아래 코드는 Seq.concat을 이용해 유연한 형식이 지원할 수 있는 여러 시나리오를 보여줘요.

let list1 = [1;2;3]
let list2 = [4;5;6]
let list3 = [7;8;9]

let concat1 = Seq.concat [ list1; list2; list3]
printfn "%A" concat1

let array1 = [|1;2;3|]
let array2 = [|4;5;6|]
let array3 = [|7;8;9|]

let concat2 = Seq.concat [ array1; array2; array3 ]
printfn "%A" concat2

let concat3 = Seq.concat [| list1; list2; list3 |]
printfn "%A" concat3

let concat4 = Seq.concat [| array1; array2; array3 |]
printfn "%A" concat4

let seq1 = { 1 .. 3 }
let seq2 = { 4 .. 6 }
let seq3 = { 7 .. 9 }

let concat5 = Seq.concat [| seq1; seq2; seq3 |]

printfn "%A" concat5

실행 결과는 다음과 같아요.

seq [1; 2; 3; 4; ...]
seq [1; 2; 3; 4; ...]
seq [1; 2; 3; 4; ...]
seq [1; 2; 3; 4; ...]
seq [1; 2; 3; 4; ...]

F#도 다른 객체지향 언어와 마찬가지로, 파생 형식(derived type)이나 인터페이스 구현체가 기본 형식이나 인터페이스 형식으로 자동 변환되는 상황이 있어요. 하지만 이런 자동 변환은 인자의 직접적인 위치에서만 일어나요. 함수 형식의 반환 형식이나 형식 인자(type argument)처럼 좀 더 복잡한 형식의 일부로 들어간 하위 위치에서는 자동 변환이 적용되지 않죠. 그래서 유연한 형식 표기는 주로 "적용 대상 형식이 좀 더 복잡한 형식의 일부로 쓰일 때" 유용해요.

더 알아보기 (Learn more)