인터페이스(Interface, F#)
인터페이스(Interface, F#)
인터페이스는 다른 클래스가 구현해야 할, 서로 연관된 멤버들의 묶음을 정의해요. F#에서 하고 싶은 "이 타입들은 이런 동작을 보장한다"는 약속을 코드로 옮기는 방법이죠. 이 페이지에서는 인터페이스를 선언하는 문법부터, 클래스 타입이나 개체 식으로 구현하는 방법, 그리고 상속이나 기본 구현까지 차근차근 살펴볼게요.
본문
들어가기 전에
인터페이스는 추상 멤버만 담고 있는 타입이에요. 다시 말해 멤버의 "모양(시그니처)"만 정해 두고, 실제 몸통(구현)은 그것을 채택하는 쪽이 채우도록 하는 거죠. 같은 인터페이스를 구현하더라도 타입마다 구현 내용이 달라질 수 있어요.
Syntax(문법)
인터페이스 선언은 대략 이런 모양이에요.
// 인터페이스 선언:
[ attributes ]
type [accessibility-modifier] interface-name =
[ interface ] [ inherit base-interface-name ...]
abstract member1 : [ argument-types1 -> ] return-type1
abstract member2 : [ argument-types2 -> ] return-type2
...
[ end ]
// 클래스 타입 정의 안에서 구현하기:
interface interface-name with
member self-identifier.member1argument-list = method-body1
member self-identifier.member2argument-list = method-body2
// 개체 식(object expression)으로 구현하기:
[ attributes ]
let class-name (argument-list) =
{ new interface-name with
member self-identifier.member1argument-list = method-body1
member self-identifier.member2argument-list = method-body2
[ base-interface-definitions ]
}
member-list
Remarks(설명)
인터페이스 선언은 클래스 선언과 닮았지만, 멤버를 구현하지 않는다는 점이 달라요. 대신 모든 멤버가 abstract 키워드로 표시된 추상 멤버인 거죠. 당연히 추상 메서드에는 메서드 본문을 적지 않아요.
한 가지 알아 두면 좋은 점은, F# 인터페이스 자체에는 기본 메서드 구현을 정의할 수 없지만, C#에서 정의된 기본 구현과는 호환된다는 거예요. default 키워드를 쓰는 기본 구현은 인터페이스가 아닌 기본 클래스에서 상속받을 때만 지원돼요.
인터페이스의 기본 액세스 가능성(accessibility)은 public이에요.
각 메서드 파라미터에는 일반 F# 문법으로 이름을 붙일 수도 있어요.
type ISprintable =
abstract member Print: format: string -> unit
위 ISprintable 예시에서 Print 메서드는 string 타입에 format이라는 이름을 가진 파라미터 하나를 받아요.
인터페이스를 구현하는 방법은 두 가지예요. 개체 식을 쓰는 방법과 타입을 쓰는 방법이죠. 어느 쪽이든 인터페이스의 추상 메서드에 메서드 본문을 제공하게 돼요. 구현은 인터페이스를 구현하는 각 타입에 특화돼서, 서로 다른 타입의 인터페이스 메서드가 서로 다를 수 있어요.
정의의 시작과 끝을 표시하는 interface와 end 키워드는 가벼운 문법(lightweight syntax)을 쓸 때는 생략할 수 있어요. 이 키워드를 쓰지 않으면 컴파일러는 실제 사용한 구문을 보고 이 타입이 클래스인지 인터페이스인지 추론해요. 멤버를 정의하거나 다른 클래스 문법을 쓰면 그 타입은 클래스로 해석돼요.
.NET 코딩 스타일에서는 모든 인터페이스 이름을 대문자 I로 시작해요.
파라미터를 여러 개 지정하는 방법도 두 가지가 있어요. F# 스타일과 .NET 스타일이죠. 둘 다 .NET 사용자 입장에서는 같은 방식으로 컴파일되지만, F# 스타일은 F# 호출자가 F# 스타일의 파라미터 적용을 쓰도록 하고, .NET 스타일은 F# 호출자가 튜플 기반(tupled) 인자 적용을 쓰도록 강제해요.
type INumericFSharp =
abstract Add: x: int -> y: int -> int
type INumericDotNet =
abstract Add: x: int * y: int -> int
클래스 타입으로 인터페이스 구현하기
클래스 타입에서 하나 이상의 인터페이스를 구현하려면 interface 키워드, 인터페이스 이름, 그리고 with 키워드 다음에 인터페이스 멤버 정의를 나열하면 돼요. 아래 코드처럼 말이죠.
type IPrintable =
abstract member Print: unit -> unit
type SomeClass1(x: int, y: float) =
interface IPrintable with
member this.Print() = printfn "%d %f" x y
인터페이스 구현은 상속되기 때문에, 파생 클래스는 인터페이스를 다시 구현할 필요가 없어요.
빈 인터페이스(마커 인터페이스) 정의하기
멤버가 없는 빈 인터페이스는 마커 인터페이스(marker interface)라고도 불러요. 특정 동작을 요구하지 않고 어떤 타입들의 집합을 식별하고 싶을 때 써요. 이 인터페이스는 멤버가 없어서, 타입에 표시(tag)를 달아 분류하는 용도로 쓰여요.
빈 인터페이스는 interface end 문법으로 정의해요.
// 빈 인터페이스(마커 인터페이스) 정의하기
type IMarker =
interface end
// 레코드 타입에서 빈 인터페이스 구현하기
type MyRecord =
{ Name: string }
interface IMarker
// 클래스 타입에서 빈 인터페이스 구현하기
type MyClass(value: int) =
member _.Value = value
interface IMarker
위 예시에서 IMarker는 빈 인터페이스로 정의됐어요. MyRecord와 MyClass는 모두 이 인터페이스에 멤버가 없으므로 어떤 메서드 구현도 제공할 필요 없이 구현해요. 이렇게 하면 인터페이스 타입을 써서 여러 타입을 공통된 방식으로 식별하고 다룰 수 있어요.
인터페이스 메서드 호출하기
인터페이스 메서드는 인터페이스를 통해서만 호출할 수 있어요. 인터페이스를 구현한 타입의 개체로는 직접 호출할 수 없죠. 그래서 이런 메서드를 부르려면 :> 연산자나 upcast 연산자로 인터페이스 타입까지 업캐스트(upcast) 해야 할 때가 있어요.
SomeClass 타입의 개체가 있을 때 인터페이스 메서드를 호출하려면 아래 코드처럼 개체를 인터페이스 타입으로 업캐스트해야 해요.
let x1 = new SomeClass1(1, 2.0)
(x1 :> IPrintable).Print()
대안으로, 개체에 업캐스트해서 인터페이스 메서드를 호출하는 메서드를 직접 선언할 수도 있어요. 다음 예시를 볼게요.
type SomeClass2(x: int, y: float) =
member this.Print() = (this :> IPrintable).Print()
interface IPrintable with
member this.Print() = printfn "%d %f" x y
let x2 = new SomeClass2(1, 2.0)
x2.Print()
개체 식으로 인터페이스 구현하기
개체 식(object expression)은 인터페이스를 간단히 구현하는 방법이에요. 이름 있는 타입을 만들 필요 없이, 추가 메서드 없이 그냥 인터페이스 메서드를 지원하는 개체만 있으면 될 때 유용해요. 개체 식은 아래 코드처럼 생겼어요.
let makePrintable (x: int, y: float) =
{ new IPrintable with
member this.Print() = printfn "%d %f" x y }
let x3 = makePrintable (1, 2.0)
x3.Print()
인터페이스 상속
인터페이스는 하나 이상의 기본 인터페이스에서 상속받을 수 있어요.
type Interface1 =
abstract member Method1: int -> int
type Interface2 =
abstract member Method2: int -> int
type Interface3 =
inherit Interface1
inherit Interface2
abstract member Method3: int -> int
type MyClass() =
interface Interface3 with
member this.Method1(n) = 2 * n
member this.Method2(n) = n + 100
member this.Method3(n) = n / 10
기본 구현(default implementation)이 있는 인터페이스 구현하기
C#에서는 아래처럼 기본 구현을 가진 인터페이스를 정의할 수 있어요.
using System;
namespace CSharp
{
public interface MyDim
{
public int Z => 0;
}
}
이런 인터페이스는 F#에서 바로 사용할 수 있어요.
open CSharp
// 클래스를 통해 인터페이스 구현하기
type MyType() =
member _.M() = ()
interface MyDim
let md = MyType() :> MyDim
printfn $"DIM from C#: %d{md.Z}"
// 개체 식으로도 구현할 수 있어요
let md' = { new MyDim }
printfn $"DIM from C# but via Object Expression: %d{md'.Z}"
기본 구현은 다른 가상 멤버를 override하듯이 override 키워드로 재정의할 수 있어요.
인터페이스에서 기본 구현이 없는 멤버는 여전히 명시적으로 구현해야 해요.
서로 다른 제네릭 인스턴스화에서 같은 인터페이스 구현하기
F#은 같은 인터페이스를 서로 다른 제네릭 인스턴스화로 구현하는 것도 지원해요.
type IA<'T> =
abstract member Get : unit -> 'T
type MyClass() =
interface IA<int> with
member x.Get() = 1
interface IA<string> with
member x.Get() = "hello"
let mc = MyClass()
let iaInt = mc :> IA<int>
let iaString = mc :> IA<string>
iaInt.Get() // 1
iaString.Get() // "hello"