매개 변수와 인수 — Optional Arguments
매개 변수와 인수 — Optional Arguments (F#)
함수나 메서드에 값을 넘길 때 F#은 '매개 변수(parameter)'와 '인수(argument)'를 서로 다르게 부른답니다. 매개 변수는 값이 들어올 자리를 미리 이름으로 정해 둔 것이고, 인수는 그 자리에 실제로 채워 넣는 값이에요. 이 글에서는 튜플 형식과 커리 형식으로 인수를 넘기는 법, 이름을 붙여서 넘기는 명명된 인수, 그리고 그중에서도 특히 유용한 선택적 매개 변수(optional arguments)를 차근차근 살펴볼게요.
출처
- 원문: Parameters and Arguments - F# | Microsoft Learn
- 요청하신 URL(
/dotnet/fsharp/language-reference/values/optional-arguments)은 현재 404로 사라졌고, 선택적 매개 변수 내용은 위 페이지의 "Optional parameters" 절로 통합되어 있습니다. - 공식 문서 저작물 특성상 코드 예제와 시그니처는 원문 그대로 보존했습니다.
본문
이 문서는 함수, 메서드, 프로퍼티에 매개 변수를 정의하고 인수를 전달하는 언어 차원의 지원을 설명합니다. 참조로 전달하는 방법과, 가변 개수의 인수를 받을 수 있는 메서드를 정의하고 사용하는 방법까지 담고 있어요.
매개 변수는 값이 들어오길 기대하는 자리의 이름을 뜻하고, 인수는 각 매개 변수에 실제로 제공된 값을 뜻합니다.
매개 변수는 튜플 형식이나 커리 형식, 또는 둘을 섞은 형태로 지정할 수 있어요. 매개 변수 이름을 명시해 인수를 넘길 수도 있고, 메서드의 매개 변수는 선택적으로 만들어 기본값을 주는 것도 가능합니다.
매개 변수 패턴 (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)에서 보통 string 배열로 넘어오는 명령줄 인수에 관심이 없을 때도 와일드카드 패턴이 유용해요.
[<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
이런 멤버들은 사실 어떤 일이든 할 수 있지만, 이 구문은 실질적으로 최종 값을 반환하기 전에 프로퍼티 setter를 호출하는 간편 문법인 셈이에요.
선택적 매개 변수 (Optional parameters)
F#은 메서드의 선택적 매개 변수를 두 가지 서로 다른 형태로 지원하고, 각각 쓰임새가 달라요.
- Optional parameters (F# native)
- Optional parameters (C# interop)
F# 네이티브 선택적 매개 변수
메서드의 선택적 매개 변수는 매개 변수 이름 앞에 **물음표(?)**를 붙여 지정해요. 호출되는 쪽(callee) 입장에서 선택적 매개 변수는 F#의 option 타입으로 해석되기 때문에, Some과 None을 이용한 match 식으로 평소 option 타입을 다루는 방식 그대로 값을 확인할 수 있습니다. 선택적 매개 변수는 멤버에서만 허용되고, let 바인딩으로 만든 함수에서는 쓸 수 없어요.
이미 있는 선택적 값을 매개 변수 이름으로 메서드에 넘길 수도 있어요. ?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 기반 선택적 매개 변수를 쓸 때는 기본값을 정하는 데 defaultArg 대신 **defaultValueArg**를 사용하면 돼요.
C# interop 선택적 매개 변수
C#과의 상호 운용을 위해서는 [<Optional; DefaultParameterValue<(...)>] 특성을 F#에서 사용할 수 있어요. 그러면 호출자 쪽에서 인수를 선택적으로 볼 수 있게 됩니다. 이는 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는 관리 포인터(managed pointer) 타입이에요. 어떤 타입을 쓸지에 대한 기준은 다음과 같습니다.
- 포인터에서 읽기만 하면
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
매개 변수가 포인터이고 값이 변경 가능하므로, 함수 실행이 끝난 뒤에도 값의 변경 사항이 그대로 유지돼요.
.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#의 타입 안전한 포맷팅 메커니즘으로 서로 다른 개수의 인수를 처리하면서도 컴파일 타임 타입 검사를 보장합니다.
매개 변수 배열은 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