매개 변수와 인수

매개 변수와 인수 (Parameters and Arguments)

함수나 메서드를 호출할 때 값을 어떻게 넘겨주는지, 그리고 정의할 때 그 값을 어떻게 받을지 정하는 문법을 다뤄요. F#에서는 매개 변수를 여러 형태로 선언할 수 있고, 값을 이름으로 넘기거나, 값을 생략할 수 있게 옵션으로 지정할 수도 있죠.

출처: F# 공식 문서 — Parameters and Arguments (Microsoft Learn)

본문

이 문서는 함수·메서드·속성에 매개 변수를 정의하고 인수를 전달하는 방법을 설명해요. 값의 참조 전달(by reference) 방법과, 가변 개수의 인수를 받을 수 있는 메서드를 정의하고 사용하는 방법도 함께 다룹니다.

여기서 용어를 하나 정리해 둘게요. 매개 변수(parameter)는 값이 제공되길 기대하는 자리 이름을 말하고, 인수(argument)는 각 매개 변수에 실제로 넣어주는 값을 뜻해요.

매개 변수는 튜플 형태나 커리(curried) 형태, 혹은 둘을 섞은 형태로 지정할 수 있어요. 인수는 명시적인 매개 변수 이름을 붙여서 넘겨줄 수도 있구요. 메서드의 매개 변수는 옵션으로 지정하고 기본값을 줄 수도 있습니다.

매개 변수 패턴 (Parameter patterns)

함수와 메서드에 넘겨지는 매개 변수는 기본적으로 공백으로 구분된 패턴이에요. 다시 말해, Match Expressions에서 다룬 패턴은 대부분 함수나 멤버의 매개 변수 목록에도 그대로 쓸 수 있다는 뜻이죠.

메서드는 보통 인수를 전달할 때 튜플 형태를 사용해요. 튜플 형태가 .NET 메서드에서 인수를 넘기는 방식과 똑같기 때문에, 다른 .NET 언어 시각에서도 결과가 더 명확해 보인답니다. 커리 형태let 바인딩으로 만든 함수에서 가장 자주 쓰이구요.

아래 의사 코드처럼 튜플 형태와 커리 형태의 예를 볼 수 있어요.

// Tuple form.
member this.SomeMethod(param1, param2) = ...
// Curried form.
let function1 param1 param2 = ...

일부 인수가 튜플이고 일부는 아닌, 두 형태를 섞은 방식도 가능해요.

let function2 param1 (param2a, param2b) param3 = ...

매개 변수 목록에는 다른 패턴들도 쓸 수 있어요. 다만 패턴이 모든 가능한 입력과 맞지 않으면, 실행 중에 불완전한 일치(incomplete match)가 발생할 수 있구요. 인수의 값이 매개 변수 목록에 지정된 패턴과 맞지 않으면 MatchFailureException 예외가 발생합니다. 컴파일러는 매개 변수 패턴이 불완전한 일치를 허용하는 경우 경고를 띄워요.

매개 변수 목록에서 흔히 유용한 또 하나의 패턴이 바로 와일드카드 패턴이에요. 들어온 인수를 그냥 무시하고 싶을 때 와일드카드 패턴을 씁니다. 아래 코드가 인수 목록에서 와일드카드 패턴을 쓰는 예시예요.

let makeList _ = [ for i in 1 .. 100 -> i * i ]
// The arguments 100 and 200 are ignored.
let list1 = makeList 100
let list2 = makeList 200

와일드카드 패턴은 넘겨받은 인수가 필요 없을 때 유용해요. 예를 들어 프로그램의 진입점(entry point)에서, 보통 문자열 배열로 넘어오는 명령줄 인수에 관심이 없을 때 아래처럼 쓸 수 있죠.

[<EntryPoint>]
let main _ =
    printfn "Entry point!"
    0

인수에 쓰이는 다른 패턴으로는 as 패턴과, 구분된 공용체(discriminated union)나 활성 패턴(active pattern)과 연관된 식별자 패턴이 있어요. 단일 케이스 구분된 공용체 패턴은 아래처럼 사용할 수 있습니다.

type Slice = Slice of int * int * string

let GetSubstring1 (Slice(p0, p1, text)) =
    printfn "Data begins at %d and ends at %d in string %s" p0 p1 text
    text[p0..p1]

let substring = GetSubstring1 (Slice(0, 4, "Et tu, Brute?"))
printfn "Substring: %s" substring

출력은 다음과 같아요.

Data begins at 0 and ends at 4 in string Et tu, Brute?
Et tu

활성 패턴도 매개 변수로 유용하게 쓸 수 있어요. 예를 들어 인수를 원하는 형식으로 변환할 때 아래처럼 활용할 수 있죠.

type Point = { x : float; y : float }

let (| Polar |) { x = x; y = y} =
    ( sqrt (x*x + y*y), System.Math.Atan (y/ x) )

let radius (Polar(r, _)) = r
let angle (Polar(_, theta)) = theta

as 패턴을 쓰면 일치한 값을 지역 변수로 저장해 둘 수도 있어요. 아래 코드 줄이 그 예시입니다.

let GetSubstring2 (Slice(p0, p1, text) as s) = s

가끔 쓰이는 또 다른 패턴은, 마지막 인수의 이름을 두지 않는 함수예요. 함수 본문으로 람다 표현식을 제공해서, 암시적으로 넘어온 인수에 즉시 패턴 일치를 수행하는 방식이죠. 다음 코드가 그 예입니다.

let isNil = function [] -> true | _::_ -> false

이 코드는 제네릭 리스트를 받아서, 리스트가 비어 있으면 true를, 그렇지 않으면 false를 돌려주는 함수를 정의해요. 이런 기법을 쓰면 코드를 오히려 읽기 어려워질 수 있다는 점을 알아두세요.

가끔은 불완전한 일치를 포함하는 패턴이 유용할 때도 있어요. 예를 들어 프로그램의 리스트가 항상 원소 세 개만 가진다는 사실을 알고 있다면, 매개 변수 목록에 아래 같은 패턴을 쓸 수 있죠.

let sum [a; b; c;] = a + b + c

불완전한 일치 패턴은 빠른 프로토타이핑이나 임시 용도로 쓰는 게 가장 좋아요. 이런 코드에는 컴파일러가 경고를 띄우구요. 이런 패턴은 모든 가능한 입력을 일반적으로 다룰 수 없기 때문에 컴포넌트 API에는 적합하지 않습니다.

명명된 인수 (Named arguments)

메서드 인수는 쉼표로 구분된 인수 목록에서 위치로 지정할 수도 있고, 이름 뒤에 등호와 값을 붙여서 명시적으로 전달할 수도 있어요. 이름으로 지정하면 선언할 때의 순서와 다르게 배치해도 상관없습니다.

명명된 인수를 쓰면 코드가 더 읽기 쉬워지고, 메서드 매개 변수의 순서가 바뀌는 것 같은 API 변경에도 더 유연하게 대처할 수 있어요.

명명된 인수는 메서드에서만 허용되며, let으로 바인딩한 함수, 함수 값, 람다 표현식에는 쓸 수 없습니다.

다음 코드가 명명된 인수의 사용 예시예요.

type SpeedingTicket() =
    member this.GetMPHOver(speed: int, limit: int) = speed - limit

let CalculateFine (ticket : SpeedingTicket) =
    let delta = ticket.GetMPHOver(limit = 55, speed = 70)
    if delta < 20 then 50.0 else 100.0

let ticket1 : SpeedingTicket = SpeedingTicket()
printfn "%f" (CalculateFine ticket1)

클래스 생성자를 호출할 때도 명명된 인수와 비슷한 문법으로 클래스 속성의 값을 설정할 수 있어요. 다음 예시가 그 문법을 보여줍니다.

type Account() =
    let mutable balance = 0.0
    let mutable number = 0
    let mutable firstName = ""
    let mutable lastName = ""
    member this.AccountNumber
       with get() = number
       and set(value) = number <- value
    member this.FirstName
       with get() = firstName
       and set(value) = firstName <- value
    member this.LastName
       with get() = lastName
       and set(value) = lastName <- value
    member this.Balance
       with get() = balance
       and set(value) = balance <- value
    member this.Deposit(amount: float) = this.Balance <- this.Balance + amount
    member this.Withdraw(amount: float) = this.Balance <- this.Balance - amount


let account1 = new Account(AccountNumber=8782108,
                           FirstName="Darren", LastName="Parker",
                           Balance=1543.33)

더 자세한 내용은 Constructors (F#) 문서를 참고하세요.

이 기법은 속성 설정자(setter)를 호출하기 위한 용도로, 객체를 반환하는 모든 메서드(예: 팩토리 메서드)에도 그대로 적용돼요.

type Widget() =
    member val Width = 1 with get,set
    member val Height = 1 with get,set

type WidgetFactory =
    static member MakeNewWidget() =
         new Widget()
    static member AdjustWidget(w: Widget) =
         w
let w = WidgetFactory.MakeNewWidget(Width=10)
w.Width // = 10
w.Height // = 1
WidgetFactory.AdjustWidget(w, Height=10)
w.Height // = 10

이런 멤버들은 사실 임의의 작업을 수행할 수 있지만, 이 문법은 본질적으로 최종 값을 반환하기 전에 속성 설정자를 호출하는 간단한 축약 형태라는 점을 알아두세요.

선택적 매개 변수 (Optional parameters)

F#은 메서드의 선택적 매개 변수로 두 가지 서로 다른 형태를 지원해요. 각각 쓰이는 목적이 다르죠.

  • 선택적 매개 변수 (F# 네이티브)
  • 선택적 매개 변수 (C# 상호 운용)

선택적 매개 변수 — F# 네이티브

메서드 매개 변수 이름 앞에 물음표를 붙이면 선택적 매개 변수로 지정할 수 있어요. 호출되는 쪽(callee) 관점에서 선택적 매개 변수는 F#의 option 타입으로 해석되기 때문에, SomeNone을 쓰는 match 표현식으로 평소 option 타입을 다룰 때처럼 조회할 수 있습니다. 선택적 매개 변수는 멤버에서만 허용되며, let 바인딩으로 만든 함수에는 쓸 수 없어요.

기존 option 값을 매개 변수 이름으로 넘겨줄 수도 있어요. 예를 들어 ?arg=None 또는 ?arg=Some(3) 또는 ?arg=arg처럼요. 다른 메서드로 선택적 인수를 넘겨주는 메서드를 만들 때 유용하답니다.

defaultArg 함수를 써서 그림자 처리(shadowing)로 선택적 인수의 기본값을 설정할 수도 있어요. defaultArg 함수는 첫 번째 인수로 선택적 매개 변수를, 두 번째 인수로 기본값을 받습니다. C# 스타일의 확장과 달리, 이 함수는 메서드 작성자가 호출자가 값을 넘겼는지 여부를 알 수 있게 해줘요.

다음 예시가 선택적 매개 변수의 사용을 보여줍니다.

type DuplexType =
    | Full
    | Half

type Connection(?rate0 : int, ?duplex0 : DuplexType, ?parity0 : bool) =
    let duplex = defaultArg duplex0 Full
    let parity = defaultArg parity0 false
    let mutable rate = match rate0 with
                        | Some rate1 -> rate1
                        | None -> match duplex with
                                  | Full -> 9600
                                  | Half -> 4800
    do printfn "Baud Rate: %d Duplex: %A Parity: %b" rate duplex parity

let conn1 = Connection(duplex0 = Full)
let conn2 = Connection(duplex0 = Half)
let conn3 = Connection(300, Half, true)
let conn4 = Connection(?duplex0 = None)
let conn5 = Connection(?duplex0 = Some(Full))

let optionalDuplexValue : option<DuplexType> = Some(Half)
let conn6 = Connection(?duplex0 = optionalDuplexValue)

출력은 다음과 같아요.

Baud Rate: 9600 Duplex: Full Parity: false
Baud Rate: 4800 Duplex: Half Parity: false
Baud Rate: 300 Duplex: Half Parity: true
Baud Rate: 9600 Duplex: Full Parity: false
Baud Rate: 9600 Duplex: Full Parity: false
Baud Rate: 4800 Duplex: Half Parity: false

선택적 매개 변수에 [<Struct>] 특성을 적용하면 Value Option 타입으로 지정할 수도 있어요.

type T() =
    static member M([<Struct>] ?p : string) =
        match p with
        | ValueSome s -> printfn "%s" s
        | ValueNone -> printfn "None"

위처럼 구조체 기반(struct-backed) 선택적 매개 변수를 쓸 때는 기본값을 설정할 때 defaultArg 대신 defaultValueArg 를 사용해요.

선택적 매개 변수 — C# 상호 운용

C# 상호 운용 목적으로는 F#에서 [<Optional; DefaultParameterValue<(...)>] 특성을 사용해서 호출자 쪽에서 인수를 선택적으로 보이게 할 수 있어요. 이는 C#에서 MyMethod(int i = 3)처럼 인수를 선택적으로 정의하는 것과 동일합니다. 이 형태는 C# 코드와의 상호 운용을 돕기 위해 F# 4.1에서 도입됐어요.

open System
open System.Runtime.InteropServices
type C =
    static member Foo([<Optional; DefaultParameterValue("Hello world")>] message) =
        printfn $"{message}"

새 객체를 기본 매개 변수 값으로 지정할 수도 있어요. 예를 들어 Foo 멤버가 대신 선택적 CancellationToken을 입력으로 받게 할 수 있죠.

open System.Threading
open System.Runtime.InteropServices
type C =
    static member Foo([<Optional; DefaultParameterValue(CancellationToken())>] ct: CancellationToken) =
        printfn $"{ct}"

DefaultParameterValue에 인수로 주는 값은 매개 변수의 타입과 일치해야 해요. 예를 들어 다음은 허용되지 않습니다.

type C =
    static member Wrong([<Optional; DefaultParameterValue("string")>] i:int) = ()

이 경우 컴파일러는 경고를 만들고 두 특성을 모두 무시해버려요. 그리고 기본값 null은 타입을 명시해야 한다는 점도 기억하세요. 그렇지 않으면 컴파일러가 잘못된 타입을 추론하게 됩니다. 즉 [<Optional; DefaultParameterValue(null:obj)>] o:obj처럼 쓰는 거예요.

참조 전달 (Pass by reference)

F# 값을 참조로 전달하려면 byref를 사용해요. byref는 관리 포인터 타입입니다. 어떤 타입을 쓸지에 대한 지침은 다음과 같아요.

  • 포인터에서 읽기만 하면 → inref<'T>
  • 포인터에 쓰기만 하면 → outref<'T>
  • 포인터에서 읽기도 쓰기도 하면 → byref<'T>
let example1 (x: inref<int>) = printfn $"It's %d{x}"

let example2 (x: outref<int>) = x <- x + 1

let example3 (x: byref<int>) =
    printfn $"It's %d{x}"
    x <- x + 1

let test () =
    // No need to make it mutable, since it's read-only
    let x = 1
    example1 &x

    // Needs to be mutable, since we write to it
    let mutable y = 2
    example2 &y
    example3 &y // Now 'y' is 3

매개 변수가 포인터이고 값이 변경 가능(mutable)하기 때문에, 값에 가해진 변경은 함수 실행이 끝난 뒤에도 그대로 유지돼요.

.NET 라이브러리 메서드의 out 매개 변수를 저장하려면 튜플을 반환 값으로 쓰거나, out 매개 변수를 byref 매개 변수처럼 다룰 수 있어요. 다음 코드가 두 방법을 모두 보여줍니다.

// TryParse has a second parameter that is an out parameter
// of type System.DateTime.
let (b, dt) = System.DateTime.TryParse("12-20-04 12:21:00")

printfn "%b %A" b dt

// The same call, using an address of operator.
let mutable dt2 = System.DateTime.Now
let b2 = System.DateTime.TryParse("12-20-04 12:21:00", &dt2)

printfn "%b %A" b2 dt2

매개 변수 배열 (Parameter arrays)

가끔은 서로 다른 타입의 임의 개수 매개 변수를 받는 함수를 정의해야 할 때가 있어요. 사용될 수 있는 모든 타입을 고려해서 가능한 오버로드 메서드를 전부 만드는 건 현실적이지 않죠. .NET 구현은 이런 메서드를 매개 변수 배열(parameter array) 기능으로 지원해요. 시그니처에 매개 변수 배열을 가진 메서드는 임의 개수의 매개 변수를 받을 수 있고, 그 매개 변수들은 배열에 담겨요. 배열 요소의 타입이 함수에 전달할 수 있는 매개 변수 타입을 결정하죠. 매개 변수 배열의 요소 타입을 System.Object로 정의하면 클라이언트 코드는 어떤 타입의 값이든 전달할 수 있습니다.

F#에서 매개 변수 배열은 메서드에서만 정의할 수 있어요. 독립 함수나 모듈에 정의된 함수에서는 사용할 수 없습니다.

다만 printfn 같은 함수는 포맷 지정자와 타입 추론을 이용해 여러 인수를 동적으로 처리하는 방식으로 비슷한 동작을 얻어요. printfn은 매개 변수 배열에 의존하지 않고, F#의 타입 안전(type-safe) 포맷 메커니즘으로 가변 개수의 인수를 처리하면서 컴파일 타임 타입 검사를 보장합니다.

ParamArray 특성을 써서 매개 변수 배열을 정의해요. ParamArray 특성은 마지막 매개 변수에만 적용할 수 있습니다.

다음 코드는 매개 변수 배열을 받는 .NET 메서드를 호출하는 예시와, F# 타입에서 매개 변수 배열을 받는 메서드를 정의하는 예시를 보여줘요.

open System

type X() =
    member this.F([<ParamArray>] args: Object[]) =
        for arg in args do
            printfn "%A" arg

[<EntryPoint>]
let main _ =
    // call a .NET method that takes a parameter array, passing values of various types
    Console.WriteLine("a {0} {1} {2} {3} {4}", 1, 10.0, "Hello world", 1u, true)

    let xobj = new X()
    // call an F# method that takes a parameter array, passing values of various types
    xobj.F("a", 1, 10.0, "Hello world", 1u, true)
    0

프로젝트에서 실행하면 앞선 코드의 출력은 다음과 같아요.

a 1 10 Hello world 1 True
"a"
1
10.0
"Hello world"
1u
true

더 알아보기