대리자

대리자 (Delegates)

대리자(delegate)는 함수 호출을 객체로 감싼 것이라고 볼 수 있어요. F#에서는 보통 함수를 1급 값으로 다루기 위해 함수 값을 쓰지만, .NET Framework에서 대리자가 널리 쓰이기 때문에 대리자를 요구하는 API와 연동할 때는 대리자가 꼭 필요하답니다. 또 다른 .NET Framework 언어에서 사용할 수 있도록 설계된 라이브러리를 만들 때도 대리자를 사용할 수 있어요.

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

본문

구문 (Syntax)

type delegate-typename = delegate of type1 -> type2

설명 (Remarks)

위 구문에서 type1은 인자 타입(들)을, type2는 반환 타입을 나타내요. type1로 표현되는 인자 타입은 자동으로 커링(currying)됩니다. 이 말은 곧, 대상 함수의 인자가 커링되어 있다면 이 타입에는 튜플 형태를 쓰고, 이미 튜플 형태인 인자라면 괄호로 감싼 튜플을 사용하라는 뜻이에요. 자동 커링은 괄호 한 겹을 제거해서 대상 메서드와 맞아떨어지는 튜플 인자를 남겨 줍니다. 각 경우에 사용할 구문은 아래 코드 예제를 참고하세요.

대리자는 F# 함수 값, 그리고 정적 메서드나 인스턴스 메서드에 연결할 수 있어요. F# 함수 값은 대리자 생성자의 인자로 바로 전달할 수 있고, 정적 메서드의 경우 클래스 이름과 메서드 이름으로 대리자를 만듭니다. 인스턴스 메서드라면 객체 인스턴스와 메서드를 하나의 인자로 제공하면 돼요. 두 경우 모두 멤버 접근 연산자(.)를 사용합니다. 대리자 타입의 Invoke 메서드는 내부에 캡슐화된 함수를 호출해요. 또한 괄호 없이 Invoke 메서드 이름을 참조하면 대리자를 함수 값처럼 전달할 수도 있습니다.

다음 코드는 클래스 안의 여러 메서드를 나타내는 대리자를 만드는 구문을 보여 줘요. 메서드가 정적 메서드인지 인스턴스 메서드인지, 그리고 인자가 튜플 형태인지 커링된 형태인지에 따라 대리자를 선언하고 할당하는 구문이 조금씩 달라집니다.

type Test1() =
  static member add(a : int, b : int) =
     a + b
  static member add2 (a : int) (b : int) =
     a + b

  member x.Add(a : int, b : int) =
     a + b
  member x.Add2 (a : int) (b : int) =
     a + b


// Delegate1 works with tuple arguments.
type Delegate1 = delegate of (int * int) -> int
// Delegate2 works with curried arguments.
type Delegate2 = delegate of int * int -> int

let InvokeDelegate1 (dlg: Delegate1) (a: int) (b: int) =
   dlg.Invoke(a, b)
let InvokeDelegate2 (dlg: Delegate2) (a: int) (b: int) =
   dlg.Invoke(a, b)

// For static methods, use the class name, the dot operator, and the
// name of the static method.
let del1 = Delegate1(Test1.add)
let del2 = Delegate2(Test1.add2)

let testObject = Test1()

// For instance methods, use the instance value name, the dot operator, and the instance method name.
let del3 = Delegate1(testObject.Add)
let del4 = Delegate2(testObject.Add2)

for (a, b) in [ (100, 200); (10, 20) ] do
  printfn "%d + %d = %d" a b (InvokeDelegate1 del1 a b)
  printfn "%d + %d = %d" a b (InvokeDelegate2 del2 a b)
  printfn "%d + %d = %d" a b (InvokeDelegate1 del3 a b)
  printfn "%d + %d = %d" a b (InvokeDelegate2 del4 a b)

다음 코드는 대리자를 다루는 여러 가지 방법을 보여 줍니다.

type Delegate1 = delegate of int * char -> string

let replicate n c = String.replicate n (string c)

// An F# function value constructed from an unapplied let-bound function
let function1 = replicate

// A delegate object constructed from an F# function value
let delObject = Delegate1(function1)

// An F# function value constructed from an unapplied .NET member
let functionValue = delObject.Invoke

List.map (fun c -> functionValue(5,c)) ['a'; 'b'; 'c']
|> List.iter (printfn "%s")

// Or if you want to get back the same curried signature
let replicate' n c =  delObject.Invoke(n,c)

// You can pass a lambda expression as an argument to a function expecting a compatible delegate type
// System.Array.ConvertAll takes an array and a converter delegate that transforms an element from
// one type to another according to a specified function.
let stringArray = System.Array.ConvertAll([|'a';'b'|], fun c -> replicate' 3 c)
printfn "%A" stringArray

위 코드 예제의 실행 결과는 다음과 같습니다.

aaaaa
bbbbb
ccccc
[|"aaa"; "bbb"|]

대리자 매개변수에는 이름을 붙일 수도 있어요.

// http://www.pinvoke.net/default.aspx/user32/WinEventDelegate.html
type WinEventDelegate = delegate of hWinEventHook:nativeint * eventType:uint32 * hWnd:nativeint * idObject:int * idChild:int * dwEventThread:uint32 * dwmsEventTime:uint32 -> unit

대리자 매개변수 이름은 선택 사항이며 Invoke 메서드에 그대로 표시됩니다. 구현 쪽의 매개변수 이름과 반드시 일치할 필요는 없어요. 이 이름은 커링된 형태에서만 쓸 수 있고 튜플 형태에서는 쓸 수 없습니다.

type D1 = delegate of item1: int * item2: string -> unit
let a = D1(fun a b -> printf "%s" b)
a.Invoke(item2 = "a", item1 = 1) // Calling with named arguments

type D2 = delegate of int * item2: string -> unit // Omitting one name
let b = D2(fun a b -> printf "%s" b)
b.Invoke(1, item2 = "a")

위 코드 예제의 실행 결과는 다음과 같습니다.

aa

더 알아보기 (Learn more)