메서드
메서드 (Methods)
함수 장에서 함수는 인자 튜플을 반환값으로 매핑하는 객체이며, 적절한 값을 반환할 수 없으면 예외를 던진다고 배웠어요. 같은 개념의 함수나 연산이 인자 타입에 따라 꽤 다르게 구현되는 일은 흔해요. 정수 두 개를 더하는 것과 부동소수점 두 개를 더하는 것은 아주 다르고, 그 둘과 정수와 부동소수점을 더하는 것도 또 다르죠. 구현이 달라도 이 연산들은 모두 "덧셈"이라는 일반 개념에 속해요. 그래서 Julia에서 이런 동작들은 모두 한 객체, 즉 + 함수에 속해요.
본문
같은 개념의 다양한 구현을 매끄럽게 쓰기 위해, 함수를 한꺼번에 정의할 필요는 없어요. 인자 타입과 개수의 특정 조합에 대한 구체적 동작을 제공하면서 조각조각 정의할 수 있죠. 함수에 대해 가능한 동작 하나의 정의를 *메서드(method)*라고 불러요. 지금까지는 모든 인자 타입에 적용되는 메서드를 하나만 가진 함수 예시만 봤는데, 메서드 정의의 시그니처에 인자의 개수뿐 아니라 타입도 표시하도록 주석을 달 수 있고, 메서드 정의도 여러 개 제공할 수 있어요. 함수를 특정 인자 튜플에 적용하면 그 인자들에 적용 가능한 가장 구체적인 메서드가 실행돼요. 따라서 함수의 전체 동작은 다양한 메서드 정의 동작들의 패치워크예요. 패치워크가 잘 설계되면, 메서드 구현이 꽤 달라도 함수의 겉으로 드러나는 동작은 매끄럽고 일관돼 보여요.
함수를 적용할 때 어느 메서드를 실행할지 고르는 것을 *디스패치(dispatch)*라 불러요. Julia는 디스패치 과정이 주어진 인자의 개수와 함수의 모든 인자 타입에 기반해 함수의 어떤 메서드를 호출할지 고르도록 허용해요. 이는 전통적인 객체지향 언어와 다른데, 거기서는 첫 번째 인자에만 기반해 디스패치하고, 그 첫 인자는 종종 특별한 인자 문법을 가지며 때로는 인자로 명시적으로 쓰지 않고 암묵적으로 생략되곤 하죠. [1] 함수의 첫 인자가 아니라 모든 인자를 사용해 어떤 메서드를 호출할지 고르는 것을 다중 디스패치(multiple dispatch)라고 불러요. 다중 디스패치는 수학 코드에서 특히 유용한데, 연산이 한 인자에 다른 인자보다 더 "속한다"고 인위적으로 여길 이유가 없으니까요. x + y의 덧셈 연산이 y보다 x에 더 속할까요? 수학 연산자의 구현은 일반적으로 모든 인자의 타입에 달려 있어요. 그러나 수학 연산을 넘어서도, 다중 디스패치는 프로그램을 구조화하고 조직하는 강력하고 편리한 패러다임으로 끝납니다.
참고: 이 장의 모든 예시는 같은 모듈 안에서 함수에 대한 메서드를 정의한다고 가정해요. 다른 모듈의 함수에 메서드를 추가하려면
import하거나 모듈 이름으로 정규화된 이름을 써야 해요. 네임스페이스 관리 절을 참고하세요.
메서드 정의 (Defining Methods)
지금까지 예시에서는 타입 제약이 없는 인자 타입을 가진 메서드 하나뿐인 함수만 정의했어요. 그런 함수는 전통적인 동적 타입 언어에서처럼 동작해요. 그렇지만 우리는 인식하지 못한 채 거의 계속 다중 디스패치와 메서드를 써 왔어요. 앞서 언급한 + 함수를 포함해 Julia의 모든 표준 함수와 연산자는 인자 타입과 개수의 다양한 조합에 대한 동작을 정의하는 많은 메서드를 갖고 있으니까요.
함수를 정의할 때, 복합 타입 절에서 소개한 :: 타입 단언 연산자를 사용해 적용 가능한 매개변수의 타입을 선택적으로 제약할 수 있어요.
julia> f(x::Float64, y::Float64) = 2x + y
f (generic function with 1 method)
이 함수 정의는 x와 y가 모두 Float64 타입의 값인 호출에만 적용돼요.
julia> f(2.0, 3.0)
7.0
다른 타입의 인자에 적용하면 MethodError가 발생해요.
julia> f(2.0, 3)
ERROR: MethodError: no method matching f(::Float64, ::Int64)
The function `f` exists, but no method is defined for this combination of argument types.
Closest candidates are:
f(::Float64, !Matched::Float64)
@ Main none:1
Stacktrace:
[...]
julia> f(Float32(2.0), 3.0)
ERROR: MethodError: no method matching f(::Float32, ::Float64)
The function `f` exists, but no method is defined for this combination of argument types.
Closest candidates are:
f(!Matched::Float64, ::Float64)
@ Main none:1
Stacktrace:
[...]
julia> f(2.0, "3.0")
ERROR: MethodError: no method matching f(::Float64, ::String)
The function `f` exists, but no method is defined for this combination of argument types.
Closest candidates are:
f(::Float64, !Matched::Float64)
@ Main none:1
Stacktrace:
[...]
julia> f("2.0", "3.0")
ERROR: MethodError: no method matching f(::String, ::String)
The function `f` exists, but no method is defined for this combination of argument types.
보다시피 인자는 정확히 Float64 타입이어야 해요. 정수나 32비트 부동소수점 같은 다른 숫자 타입은 자동으로 64비트 부동소수점으로 변환되지 않고, 문자열도 숫자로 파싱되지 않아요. Float64는 구체 타입이고 Julia에서 구체 타입은 서브클래스화할 수 없으므로, 그런 정의는 정확히 Float64 타입인 인자에만 적용될 수 있어요. 하지만 선언된 매개변수 타입이 추상적인 더 일반적인 메서드를 쓰는 게 유용할 때가 많아요.
julia> f(x::Number, y::Number) = 2x - y
f (generic function with 2 methods)
julia> f(2.0, 3)
1.0
이 메서드 정의는 Number의 인스턴스인 모든 인자 쌍에 적용돼요. 각각 숫자 값이기만 하면 같은 타입일 필요는 없죠. 서로 다른 숫자 타입을 다루는 문제는 2x - y 표현식의 산술 연산에 위임돼요.
여러 메서드를 가진 함수를 정의하려면, 인자의 개수와 타입을 다르게 하면서 함수를 여러 번 정의하면 돼요. 함수에 대한 첫 메서드 정의가 함수 객체를 만들고, 이후 메서드 정의는 기존 함수 객체에 새 메서드를 추가해요. 함수를 적용할 때 인자의 개수와 타입에 맞는 가장 구체적인 메서드 정의가 실행돼요. 따라서 위의 두 메서드 정의는 함께 추상 타입 Number의 인스턴스인 모든 쌍에 대한 f의 동작을 정의하는데, Float64 값의 쌍에 대해서는 다른 동작을 제공해요. 인자 하나가 64비트 부동소수점이지만 다른 하나는 아니라면, f(Float64,Float64) 메서드를 호출할 수 없고 더 일반적인 f(Number,Number) 메서드를 써야 해요.
julia> f(2.0, 3.0)
7.0
julia> f(2, 3.0)
1.0
julia> f(2.0, 3)
1.0
julia> f(2, 3)
1
2x + y 정의는 첫 번째 경우에만 쓰이고, 나머지 경우에는 2x - y 정의가 쓰여요. 함수 인자의 자동 캐스팅이나 변환은 절대 일어나지 않아요. Julia에서 모든 변환은 비마법적이고 완전히 명시적이에요. 다만 변환과 승격(Conversion and Promotion) 장에서, 충분히 발전된 기술을 영리하게 적용하면 마법과 구분할 수 없게 된다는 걸 보여줘요. [Clarke61]
숫자가 아닌 값이나, 인자가 2개보다 적거나 많은 경우에는 함수 f가 정의되지 않은 상태로 남고, 적용해도 여전히 MethodError가 발생해요.
julia> f("foo", 3)
ERROR: MethodError: no method matching f(::String, ::Int64)
The function `f` exists, but no method is defined for this combination of argument types.
Closest candidates are:
f(!Matched::Number, ::Number)
@ Main none:1
f(!Matched::Float64, !Matched::Float64)
@ Main none:1
Stacktrace:
[...]
julia> f()
ERROR: MethodError: no method matching f()
The function `f` exists, but no method is defined for this combination of argument types.
Closest candidates are:
f(!Matched::Float64, !Matched::Float64)
@ Main none:1
f(!Matched::Number, !Matched::Number)
@ Main none:1
Stacktrace:
[...]
대화형 세션에서 함수 객체 자체를 입력하면 그 함수에 어떤 메서드들이 있는지 쉽게 볼 수 있어요.
julia> f
f (generic function with 2 methods)
이 출력은 f가 메서드 두 개를 가진 함수 객체라는 걸 알려줘요. 그 메서드들의 시그니처가 무엇인지 보려면 methods 함수를 쓰면 돼요.
julia> methods(f)
# 2 methods for generic function "f" from Main:
[1] f(x::Float64, y::Float64)
@ none:1
[2] f(x::Number, y::Number)
@ none:1
이건 f가 메서드 두 개를 가지는데, 하나는 Float64 인자 두 개를, 다른 하나는 Number 타입 인자들을 받는다는 걸 보여줘요. 또 메서드가 정의된 파일과 줄 번호도 알려주는데, 이 메서드들은 REPL에서 정의됐으니 겉보기 줄 번호 none:1을 얻는 거예요.
::로 타입 선언이 없으면 메서드 매개변수의 타입은 기본적으로 Any예요. Julia의 모든 값은 추상 타입 Any의 인스턴스이므로 타입이 제약되지 않는다는 뜻이죠. 그래서 f에 대해 이런 포괄(catch-all) 메서드를 정의할 수 있어요.
julia> f(x,y) = println("Whoa there, Nelly.")
f (generic function with 3 methods)
julia> methods(f)
# 3 methods for generic function "f" from Main:
[1] f(x::Float64, y::Float64)
@ none:1
[2] f(x::Number, y::Number)
@ none:1
[3] f(x, y)
@ none:1
julia> f("foo", 1)
Whoa there, Nelly.
이 포괄 메서드는 매개변수 값의 쌍에 대한 다른 어떤 가능한 메서드 정의보다도 덜 구체적이라서, 다른 메서드 정의가 적용되지 않는 인자 쌍에 대해서만 호출돼요.
세 번째 메서드의 시그니처에는 x와 y 인자에 타입이 지정돼 있지 않다는 점을 주목하세요. 이건 f(x::Any, y::Any)를 짧게 줄여 쓴 거예요.
단순한 개념처럼 보여도, 값 타입에 대한 다중 디스패치는 아마 Julia 언어의 가장 강력하고 중심적인 단일 기능일 거예요. 핵심 연산들은 보통 수십 개의 메서드를 가져요.
julia> methods(+)
# 180 methods for generic function "+":
[1] +(x::Bool, z::Complex{Bool}) in Base at complex.jl:227
[2] +(x::Bool, y::Bool) in Base at bool.jl:89
[3] +(x::Bool) in Base at bool.jl:86
[4] +(x::Bool, y::T) where T<:AbstractFloat in Base at bool.jl:96
[5] +(x::Bool, z::Complex) in Base at complex.jl:234
[6] +(a::Float16, b::Float16) in Base at float.jl:373
[7] +(x::Float32, y::Float32) in Base at float.jl:375
[8] +(x::Float64, y::Float64) in Base at float.jl:376
[9] +(z::Complex{Bool}, x::Bool) in Base at complex.jl:228
[10] +(z::Complex{Bool}, x::Real) in Base at complex.jl:242
[11] +(x::Char, y::Integer) in Base at char.jl:40
[12] +(c::BigInt, x::BigFloat) in Base.MPFR at mpfr.jl:307
[13] +(a::BigInt, b::BigInt, c::BigInt, d::BigInt, e::BigInt) in Base.GMP at gmp.jl:392
[14] +(a::BigInt, b::BigInt, c::BigInt, d::BigInt) in Base.GMP at gmp.jl:391
[15] +(a::BigInt, b::BigInt, c::BigInt) in Base.GMP at gmp.jl:390
[16] +(x::BigInt, y::BigInt) in Base.GMP at gmp.jl:361
[17] +(x::BigInt, c::Union{UInt16, UInt32, UInt64, UInt8}) in Base.GMP at gmp.jl:398
...
[180] +(a, b, c, xs...) in Base at operators.jl:424
다중 디스패치는 유연한 매개변수 타입 시스템과 함께, Julia가 구현 세부 사항에서 분리된 고수준 알고리즘을 추상적으로 표현할 수 있게 해줘요.
메서드 특수화 (Method specializations)
같은 함수의 메서드를 여러 개 만들면 이것을 "특수화(specialization)"라고 부르기도 해요. 이 경우 함수에 메서드를 추가해 함수를 특수화하는 거예요. 각 새 메서드는 함수의 새 특수화이죠. 위에서 보았듯 이런 특수화들은 methods가 반환해요.
프로그래머의 개입 없이 일어나는 또 다른 종류의 특수화가 있어요. Julia의 컴파일러가 사용되는 특정 인자 타입에 대해 메서드를 자동으로 특수화할 수 있거든요. 이런 특수화는 새 Method를 만들지 않으므로 methods에 나열되지 않는데, @code_typed 같은 도구로 그런 특수화를 검사할 수 있어요.
예를 들어 메서드를 만들면:
mysum(x::Real, y::Real) = x + y
함수 mysum에 새 메서드를 하나 주었고(아마 유일한 메서드), 그 메서드는 Real 숫자 입력의 어떤 쌍이든 받아요. 하지만 그 다음에:
julia> mysum(1, 2)
3
julia> mysum(1.0, 2.0)
3.0
을 실행하면, Julia는 mysum을 두 번 컴파일해요. 한 번은 x::Int, y::Int용, 또 한 번은 x::Float64, y::Float64용이죠. 두 번 컴파일하는 이유는 성능이에요. +(mysum이 사용하는)에 대해 호출되는 메서드는 x와 y의 구체적인 타입에 따라 달라지는데, 서로 다른 특수화를 컴파일함으로써 Julia는 모든 메서드 조회를 미리 해둘 수 있어요. 그러면 프로그램이 실행 중에 메서드 조회를 신경 쓸 필요가 없어서 훨씬 빠르게 돌아가요. Julia의 자동 특수화 덕분에 일반적인 알고리즘을 쓰면서도, 컴파일러가 필요한 각 경우를 처리할 효율적이고 특수화된 코드를 생성할 거라고 기대할 수 있어요.
잠재적 특수화의 수가 실질적으로 무한해질 수 있는 경우, Julia는 이 기본 특수화를 피할 수도 있어요. 자세한 내용은 Julia가 특수화를 피하는 때를 알아두기를 참고하세요.
메서드 모호성 (Method Ambiguities)
어떤 인자 조합에 대해 적용 가능한 유일한 가장-구체적 메서드가 존재하지 않도록 함수 메서드 집합을 정의하는 것도 가능해요.
julia> g(x::Float64, y) = 2x + y
g (generic function with 1 method)
julia> g(x, y::Float64) = x + 2y
g (generic function with 2 methods)
julia> g(2.0, 3)
7.0
julia> g(2, 3.0)
8.0
julia> g(2.0, 3.0)
ERROR: MethodError: g(::Float64, ::Float64) is ambiguous.
Candidates:
g(x, y::Float64)
@ Main none:1
g(x::Float64, y)
@ Main none:1
Possible fix, define
g(::Float64, ::Float64)
Stacktrace:
[...]
여기서 g(2.0, 3.0) 호출은 g(::Float64, ::Any) 메서드나 g(::Any, ::Float64) 메서드 어느 쪽으로도 처리될 수 있어요. 메서드를 정의한 순서는 상관없고, 어느 쪽도 다른 쪽보다 더 구체적이지 않아요. 그런 경우 Julia는 메서드를 임의로 고르는 대신 MethodError를 발생시켜요. 교집합 경우에 적절한 메서드를 지정하면 메서드 모호성을 피할 수 있어요.
julia> g(x::Float64, y::Float64) = 2x + 2y
g (generic function with 3 methods)
julia> g(2.0, 3)
7.0
julia> g(2, 3.0)
8.0
julia> g(2.0, 3.0)
10.0
모호성을 해소하는 메서드를 먼저 정의하는 것을 권장해요. 그렇지 않으면 더 구체적인 메서드가 정의될 때까지 그 모호성이 (일시적으로라도) 존재하니까요.
더 복잡한 경우에는 메서드 모호성을 해결하는 일이 일정한 설계를 수반하기도 해요. 이 주제는 아래에서 더 다룰게요.
매개변수 메서드 (Parametric Methods)
메서드 정의는 시그니처를 한정하는 타입 매개변수를 선택적으로 가질 수 있어요.
julia> same_type(x::T, y::T) where {T} = true
same_type (generic function with 1 method)
julia> same_type(x,y) = false
same_type (generic function with 2 methods)
첫 번째 메서드는 두 인자가 그 타입이 무엇이든 같은 구체 타입일 때마다 적용되고, 두 번째 메서드는 나머지 모든 경우를 덮는 포괄 메서드 역할을 해요. 그래서 전체적으로 두 인자가 같은 타입인지 검사하는 불리언 함수가 정의돼요.
julia> same_type(1, 2)
true
julia> same_type(1, 2.0)
false
julia> same_type(1.0, 2.0)
true
julia> same_type("foo", 2.0)
false
julia> same_type("foo", "bar")
true
julia> same_type(Int32(1), Int64(2))
false
이런 정의는 타입 시그니처가 UnionAll 타입인 메서드에 해당해요(UnionAll 타입 참조).
이런 식으로 디스패치로 함수 동작을 정의하는 것은 Julia에서 아주 흔하고, 심지어 관용적(idiomatic)이에요. 메서드 타입 매개변수는 인자의 타입으로만 쓰여야 하는 건 아니에요. 함수 시그니처나 함수 본문에서 값이 쓰일 수 있는 어디든 쓸 수 있죠. 메서드 타입 매개변수 T가 메서드 시그니처에서 매개변수 타입 Vector{T}의 타입 매개변수로 쓰이는 예시를 볼게요.
julia> function myappend(v::Vector{T}, x::T) where {T}
return [v..., x]
end
myappend (generic function with 1 method)
이 예시의 타입 매개변수 T는 추가되는 원소 x가 벡터 v의 기존 eltype의 하위 타입임을 보장해요. where 키워드는 메서드 시그니처 정의 뒤에 그런 제약들의 목록을 도입해요. 이는 위에서 본 것처럼 한 줄 정의에서도 똑같이 동작하며, 아래처럼 반환 타입 선언이 있다면 그 앞에 와야 해요.
julia> (myappend(v::Vector{T}, x::T)::Vector) where {T} = [v..., x]
myappend (generic function with 1 method)
julia> myappend([1,2,3],4)
4-element Vector{Int64}:
1
2
3
4
julia> myappend([1,2,3],2.5)
ERROR: MethodError: no method matching myappend(::Vector{Int64}, ::Float64)
The function `myappend` exists, but no method is defined for this combination of argument types.
Closest candidates are:
myappend(::Vector{T}, !Matched::T) where T
@ Main none:1
Stacktrace:
[...]
julia> myappend([1.0,2.0,3.0],4.0)
4-element Vector{Float64}:
1.0
2.0
3.0
4.0
julia> myappend([1.0,2.0,3.0],4)
ERROR: MethodError: no method matching myappend(::Vector{Float64}, ::Int64)
The function `myappend` exists, but no method is defined for this combination of argument types.
Closest candidates are:
myappend(::Vector{T}, !Matched::T) where T
@ Main none:1
Stacktrace:
[...]
추가되는 원소의 타입이 추가될 벡터의 원소 타입과 맞지 않으면 MethodError가 발생해요. 다음 예시에서는 메서드의 타입 매개변수 T가 반환값으로 쓰여요.
julia> mytypeof(x::T) where {T} = T
mytypeof (generic function with 1 method)
julia> mytypeof(1)
Int64
julia> mytypeof(1.0)
Float64
타입 선언에서 타입 매개변수에 하위 타입 제약을 둘 수 있는 것처럼(매개변수 타입 참조), 메서드의 타입 매개변수도 제약할 수 있어요.
julia> same_type_numeric(x::T, y::T) where {T<:Number} = true
same_type_numeric (generic function with 1 method)
julia> same_type_numeric(x::Number, y::Number) = false
same_type_numeric (generic function with 2 methods)
julia> same_type_numeric(1, 2)
true
julia> same_type_numeric(1, 2.0)
false
julia> same_type_numeric(1.0, 2.0)
true
julia> same_type_numeric("foo", 2.0)
ERROR: MethodError: no method matching same_type_numeric(::String, ::Float64)
The function `same_type_numeric` exists, but no method is defined for this combination of argument types.
Closest candidates are:
same_type_numeric(!Matched::T, ::T) where T<:Number
@ Main none:1
same_type_numeric(!Matched::Number, ::Number)
@ Main none:1
Stacktrace:
[...]
julia> same_type_numeric("foo", "bar")
ERROR: MethodError: no method matching same_type_numeric(::String, ::String)
The function `same_type_numeric` exists, but no method is defined for this combination of argument types.
julia> same_type_numeric(Int32(1), Int64(2))
false
same_type_numeric 함수는 위에서 정의한 same_type 함수와 비슷하게 동작하지만, 숫자 쌍에 대해서만 정의돼요.
매개변수 메서드는 타입을 쓸 때 쓰는 where 표현식과 같은 문법을 허용해요(UnionAll 타입 참조). 매개변수가 하나뿐이면 감싸는 중괄호(where {T}에서)를 생략할 수 있지만, 명확성을 위해 보통 선호돼요. 여러 매개변수는 콤마로 구분할 수 있는데, 예를 들어 where {T, S<:Real}처럼요. 또는 where S<:Real where T처럼 중첩 where로 쓸 수도 있어요.
메서드 재정의 (Redefining Methods)
메서드를 재정의하거나 새 메서드를 추가할 때, 그 변경이 즉시 적용되지 않는다는 점을 깨달아야 해요. 이는 Julia가 흔한 JIT 트릭과 오버헤드 없이 코드를 정적으로 추론하고 컴파일해 빠르게 실행하게 하는 핵심이에요. 실제로 어떤 새 메서드 정의도 현재 런타임 환경(태스크, 스레드, 이전에 정의된 @generated 함수 포함)에는 보이지 않아요. 이게 무엇을 뜻하는지 예시부터 시작해 볼게요.
julia> function tryeval()
@eval newfun() = 1
newfun()
end
tryeval (generic function with 1 method)
julia> tryeval()
ERROR: MethodError: no method matching newfun()
The applicable method may be too new: running in world age xxxx1, while current world is xxxx2.
Closest candidates are:
newfun() at none:1 (method too new to be called from this world context.)
in tryeval() at none:1
...
julia> newfun()
1
이 예시에서 새 newfun 정의는 만들어졌지만 즉시 호출할 수 없어요. 새 전역은 tryeval 함수에는 즉시 보이므로 return newfun(괄호 없이)이라고 쓸 수 있어요. 하지만 너 자신이나 호출자, 그들이 호출하는 함수 등 어느 누구도 이 새 메서드 정의를 호출할 수 없어요!
다만 예외가 있어요. REPL에서의 이후 newfun 호출은 예상대로 동작해서, newfun의 새 정의를 보고 호출할 수 있어요.
하지만 이후의 tryeval 호출은 계속해서 newfun의 정의를 REPL의 이전 문장 시점의, 즉 그 tryeval 호출 전의 정의로 보게 돼요.
직접 시도해 보고 어떻게 작동하는지 확인해 봐도 좋아요.
이 동작의 구현은 "월드 에이지 카운터(world age counter)"이며, 월드 에이지 매뉴얼 장에서 자세히 설명해요.
매개변수 메서드를 이용한 설계 패턴 (Design Patterns with Parametric Methods)
복잡한 디스패치 로직이 성능이나 사용성에 꼭 필요한 건 아니지만, 때로는 어떤 알고리즘을 표현하는 가장 좋은 방법이 될 수 있어요. 이런 식으로 디스패치를 쓸 때 자주 등장하는 몇 가지 일반적인 설계 패턴을 소개할게요.
슈퍼타입에서 타입 매개변수 추출하기
원소 타입이 잘 정의된 AbstractArray의 임의 하위 타입의 원소 타입 T를 반환하는 올바른 코드 템플릿이에요.
abstract type AbstractArray{T, N} end
eltype(::Type{<:AbstractArray{T}}) where {T} = T
소위 삼각 디스패치(triangular dispatch)를 쓴 거예요. UnionAll 타입, 예를 들어 eltype(AbstractArray{T} where T <: Integer)는 위 메서드와 매치되지 않는다는 점에 주의하세요. Base의 eltype 구현은 그런 경우를 위해 Any에 폴백 메서드를 추가해요.
흔한 실수는 내성(introspection)으로 원소 타입을 얻으려는 거예요.
eltype_wrong(::Type{A}) where {A<:AbstractArray} = A.parameters[1]
하지만 이게 실패하는 경우를 만들기는 어렵지 않아요.
struct BitVector <: AbstractArray{Bool, 1}; end
여기서 매개변수가 없는 타입 BitVector를 만들었는데, 원소 타입은 여전히 완전히 지정되어 있고 T는 Bool이에요!
또 다른 실수는 supertype로 타입 계층을 올라가려는 거예요.
eltype_wrong(::Type{AbstractArray{T}}) where {T} = T
eltype_wrong(::Type{AbstractArray{T, N}}) where {T, N} = T
eltype_wrong(::Type{A}) where {A<:AbstractArray} = eltype_wrong(supertype(A))
선언된 타입에는 동작하지만, 슈퍼타입이 없는 타입에서는 실패해요.
julia> eltype_wrong(Union{Vector{Int}, Matrix{Int}})
ERROR: MethodError: no method matching supertype(::Type{VecOrMat{Int64}})
Closest candidates are:
supertype(::UnionAll)
@ Base operators.jl:44
supertype(::DataType)
@ Base operators.jl:43
다른 타입 매개변수로 비슷한 타입 만들기
일반 코드를 작성할 때, 타입의 레이아웃을 조금 바꿔 비슷한 객체를 만들어야 하는 필요가 자주 생겨요. 타입 매개변수의 변경도 필요로 하죠. 예를 들어 임의의 원소 타입을 가진 일종의 추상 배열이 있고, 특정 원소 타입으로 계산을 쓰고 싶을 수 있어요. 이 타입 변환을 계산하는 방법을 설명하는 메서드를 각 AbstractArray{T} 하위 타입에 대해 구현해야 해요. 한 하위 타입을 다른 매개변수를 가진 다른 하위 타입으로 바꾸는 일반적인 변환은 없어요.
AbstractArray의 하위 타입들은 보통 이것을 위해 메서드 두 개를 구현해요. 하나는 입력 배열을 특정 AbstractArray{T, N} 추상 타입의 하위 타입으로 변환하는 메서드, 다른 하나는 특정 원소 타입의 새 초기화되지 않은 배열을 만드는 메서드예요. 이들의 샘플 구현은 Julia Base에서 찾을 수 있어요. input과 output이 같은 타입임을 보장하는 기본 사용 예시를 볼게요.
input = convert(AbstractArray{Eltype}, input)
output = similar(input, Eltype)
이것의 확장으로, 알고리즘이 입력 배열의 복사본을 필요로 하는 경우 convert는 충분하지 않아요. 반환값이 원래 입력을 앨리어싱할 수 있으니까요. similar(출력 배열을 만드는 데)와 copyto!(입력 데이터로 채우는 데)를 결합하는 것은 입력 인자의 변경 가능한 복사본에 대한 요구를 표현하는 일반적인 방법이에요.
copy_with_eltype(input, Eltype) = copyto!(similar(input, Eltype), input)
반복 디스패치 (Iterated dispatch)
다단계 매개변수 인자 목록을 디스패치하려면, 종종 각 디스패치 수준을 별개의 함수로 분리하는 게 가장 좋아요. 이게 단일 디스패치와 접근 방식이 비슷해 보일 수도 있지만, 아래에서 보듯 여전히 더 유연해요.
예를 들어 배열의 원소 타입에 디스패치하려는 시도는 종종 모호한 상황에 부딪혀요. 대신 일반적인 코드는 먼저 컨테이너 타입에 디스패치한 다음, eltype에 기반해 더 구체적인 메서드로 재귀해 내려가요. 대부분의 경우 알고리즘은 이 계층적 접근에 편리하게 잘 맞지만, 다른 경우에는 이 엄격함을 수동으로 해결해야 해요. 이런 디스패치 분기는 예를 들어 두 행렬을 합하는 로직에서 관찰할 수 있어요.
# 첫 번째 디스패치는 원소별 합산을 위한 map 알고리즘을 고른다.
+(a::Matrix, b::Matrix) = map(+, a, b)
# 두 번째 디스패치는 각 원소를 처리하고 계산에 적절한
# 공통 원소 타입을 고른다.
+(a, b) = +(promote(a, b)...)
# 일단 원소들이 같은 타입이 되면 더할 수 있다.
# 예를 들어 프로세서가 노출하는 원시 연산을 통해.
+(a::Float64, b::Float64) = Core.add(a, b)
트레이트 기반 디스패치 (Trait-based dispatch)
위의 반복 디스패치의 자연스러운 확장은, 타입 계층이 정의한 집합과 독립적인 타입 집합에 디스패치할 수 있게 해주는 계층을 메서드 선택에 추가하는 거예요. 문제의 타입들을 Union으로 써서 그런 집합을 만들 수도 있지만, Union 타입은 생성 후 변경할 수 없으므로 그 집합은 확장 가능하지 않아요. 하지만 그런 확장 가능한 집합은 흔히 "Holy-trait"라 불리는 설계 패턴으로 프로그래밍할 수 있어요.
이 패턴은 함수 인자가 속할 수 있는 각 트레이트 집합에 대해 서로 다른 싱글턴 값(또는 타입)을 계산하는 일반 함수를 정의해 구현돼요. 이 함수가 순수(pure)하면 일반 디스패치에 비해 성능 영향이 없어요.
이전 절의 예시는 map과 promote의 구현 세부 사항을 얼버무렸는데, 둘 다 이런 트레이트를 기준으로 동작해요. 행렬을 순회할 때(map 구현에서처럼) 중요한 질문 하나는 데이터를 어떤 순서로 순회할지예요. AbstractArray 하위 타입들이 Base.IndexStyle 트레이트를 구현하면, map 같은 다른 함수들이 이 정보에 디스패치해 최고의 알고리즘을 고를 수 있어요(추상 배열 인터페이스 참조). 이는 각 하위 타입이 map의 커스텀 버전을 구현할 필요가 없다는 뜻이에요. 일반 정의와 트레이트 클래스가 시스템이 가장 빠른 버전을 고르게 해주니까요. 트레이트 기반 디스패치를 보여주는 장난감 map 구현을 볼게요.
map(f, a::AbstractArray, b::AbstractArray) = map(Base.IndexStyle(a, b), f, a, b)
# 일반 구현:
map(::Base.IndexCartesian, f, a::AbstractArray, b::AbstractArray) = ...
# 선형 인덱싱 구현 (더 빠름)
map(::Base.IndexLinear, f, a::AbstractArray, b::AbstractArray) = ...
이 트레이트 기반 접근은 스칼라 +가 쓰는 promote 메커니즘에도 존재해요. promote_type를 쓰는데, 피연산자 두 타입이 주어졌을 때 연산을 계산할 최적의 공통 타입을 반환해요. 이 덕분에 가능한 타입 인자의 모든 쌍에 대해 모든 함수를 구현하는 문제를, 각 타입에서 공통 타입으로의 변환 연산 하나를 구현하는 것 + 선호하는 쌍별 승격 규칙 표 하나로 줄일 수 있어요.
출력 타입 계산 (Output-type computation)
트레이트 기반 승격에 대한 논의는 다음 설계 패턴으로 자연스럽게 이어져요. 행렬 연산의 출력 원소 타입을 계산하는 거예요.
덧셈 같은 원시 연산을 구현할 때는 promote_type 함수로 원하는 출력 타입을 계산해요. (앞서처럼, +를 호출했을 때 promote 호출에서 이게 동작하는 걸 봤어요.)
행렬에 대한 더 복잡한 함수에서는, 더 복잡한 연산 시퀀스의 기대 반환 타입을 계산해야 할 수도 있어요. 이는 보통 다음 단계로 수행돼요.
- 알고리즘 커널이 수행하는 연산 집합을 표현하는 작은 함수
op를 쓴다. promote_op(op, argument_types...)로 결과 행렬의 원소 타입R을 계산한다. 여기서argument_types는 각 입력 배열에eltype을 적용해 계산한다.similar(R, dims)로 출력 행렬을 만든다. 여기서dims는 출력 배열의 원하는 차원이다.
더 구체적인 예로, 일반적인 정사각 행렬 곱셈 의사 코드는 이렇게 생겼을 거예요.
function matmul(a::AbstractMatrix, b::AbstractMatrix)
op = (ai, bi) -> ai * bi + ai * bi
## 이건 `one(eltype(a))`가 만들 수 있다고 가정하므로 부족하다:
# R = typeof(op(one(eltype(a)), one(eltype(b))))
## 이건 `a[1]`이 존재하고 배열의 모든 원소를 대표한다고 가정하므로 실패한다
# R = typeof(op(a[1], b[1]))
## 이건 `+`가 `promote_type`을 호출한다고 가정하므로 부정확하다
## 하지만 Bool 같은 일부 타입에는 그렇지 않다:
# R = promote_type(ai, bi)
# 이건 틀렸다. 타입 추론의 반환값에 의존하는 것은
# 매우 취약하며(최적화도 불가능하고):
# R = Base.return_types(op, (eltype(a), eltype(b)))
## 하지만 마침내, 이건 동작한다:
R = promote_op(op, eltype(a), eltype(b))
## 때로는 원하는 것보다 더 큰 타입을 줄 수도 있지만
## 항상 올바른 타입을 준다
output = similar(b, R, (size(a, 1), size(b, 2)))
if size(a, 2) > 0
for j in 1:size(b, 2)
for i in 1:size(a, 1)
## 여기서 `ab = zero(R)`을 쓰지 않는다.
## `R`이 `Any`일 수 있고 `zero(Any)`가 정의되지 않았으므로
## 또한 `ab::R`을 선언해 루프에서 `ab`의 타입을 상수로 만들어야 한다.
## typeof(a * b) != typeof(a * b + a * b) == R일 수 있으므로
ab::R = a[i, 1] * b[1, j]
for k in 2:size(a, 2)
ab += a[i, k] * b[k, j]
end
output[i, j] = ab
end
end
end
return output
end
변환과 커널 로직 분리 (Separate convert and kernel logic)
컴파일 시간과 테스트 복잡도를 크게 줄이는 한 가지 방법은 원하는 타입으로 변환하는 로직과 계산을 격리하는 거예요. 그러면 컴파일러가 더 큰 커널의 나머지 본문과 독립적으로 변환 로직을 특수화하고 인라인할 수 있어요.
더 큰 타입 클래스에서 알고리즘이 실제로 지원하는 한 특정 인자 타입으로 변환할 때 흔히 보이는 패턴이에요.
complexfunction(arg::Int) = ...
complexfunction(arg::Any) = complexfunction(convert(Int, arg))
matmul(a::T, b::T) = ...
matmul(a, b) = matmul(promote(a, b)...)
매개변수로 제약된 가변 인자 메서드 (Parametrically-constrained Varargs methods)
함수 매개변수는 또한 "가변 인자(varargs)" 함수(가변 인자 함수)에 공급될 수 있는 인자의 수를 제약하는 데도 쓸 수 있어요. Vararg{T,N} 표기법이 그런 제약을 나타내요. 예를 들어:
julia> bar(a,b,x::Vararg{Any,2}) = (a,b,x)
bar (generic function with 1 method)
julia> bar(1,2,3)
ERROR: MethodError: no method matching bar(::Int64, ::Int64, ::Int64)
The function `bar` exists, but no method is defined for this combination of argument types.
Closest candidates are:
bar(::Any, ::Any, ::Any, !Matched::Any)
@ Main none:1
Stacktrace:
[...]
julia> bar(1,2,3,4)
(1, 2, (3, 4))
julia> bar(1,2,3,4,5)
ERROR: MethodError: no method matching bar(::Int64, ::Int64, ::Int64, ::Int64, ::Int64)
The function `bar` exists, but no method is defined for this combination of argument types.
Closest candidates are:
bar(::Any, ::Any, ::Any, ::Any)
@ Main none:1
Stacktrace:
[...]
더 유용하게는, 가변 인자 메서드를 매개변수로 제약하는 것도 가능해요. 예를 들어:
function getindex(A::AbstractArray{T,N}, indices::Vararg{Number,N}) where {T,N}
는 indices의 수가 배열의 차원과 일치할 때만 호출돼요.
공급된 인자의 타입만 제약하면 될 때, Vararg{T}는 동등하게 T...로 쓸 수 있어요. 예를 들어 f(x::Int...) = x는 f(x::Vararg{Int}) = x의 약칭이에요.
선택적 인자와 키워드 인자에 대한 참고 (Note on Optional and keyword Arguments)
함수 장에서 간단히 말했듯, 선택적 인자(optional argument)는 여러 메서드 정의를 위한 문법으로 구현돼요. 예를 들어 이 정의:
f(a=1,b=2) = a+2b
는 다음 세 메서드로 변환돼요.
f(a,b) = a+2b
f(a) = f(a,2)
f() = f(1,2)
이는 f() 호출이 f(1,2) 호출과 동등하다는 뜻이에요. 이 경우 결과는 5인데, f(1,2)가 위의 f의 첫 메서드를 호출하기 때문이죠. 하지만 항상 그런 건 아니에요. 정수에 더 특수화된 네 번째 메서드를 정의하면:
f(a::Int,b::Int) = a-2b
f()와 f(1,2) 둘 다의 결과가 -3이 돼요. 다시 말해 선택적 인자는 그 함수의 특정 메서드가 아니라 함수에 묶여 있어요. 어떤 메서드가 호출될지는 선택적 인자의 타입에 달려 있어요. 선택적 인자가 전역 변수로 정의되면, 선택적 인자의 타입이 런타임에 바뀔 수도 있어요.
키워드 인자는 일반 위치 인자와 아주 다르게 동작해요. 특히 메서드 디스패치에 참여하지 않아요. 메서드는 위치 인자에만 기반해 디스패치되고, 키워드 인자는 매칭되는 메서드가 식별된 뒤에 처리돼요.
알려진 버그: 키워드 인자의 존재가 디스패치에 영향을 줌 오래된 버그(#9498) 때문에, 키워드 인자를 공급하는 호출은 키워드 인자를 받아들이는 메서드만 고려해요. 이로 인해 키워드를 받지 않는 더 구체적인 메서드보다 덜 구체적인 메서드가 선택될 수 있어요.
julia> f(x; y=10) = "generic";
julia> f(x::Int) = "int-specific";
julia> f(1)
"int-specific"
julia> f(1; y=2) # 버그: 더 구체적인 메서드를 우회
"generic"
관련해서, 키워드 인자 없이 메서드를 재정의해도 같은 위치 시그니처를 가진 이전 정의의 키워드 처리를 대체하지 못해요. 메서드가 하나만 표시되는데도 말이죠.
julia> g(x; y=1) = "with keywords";
julia> g(x) = "without keywords";
julia> g
g (generic function with 1 method)
julia> g(1)
"without keywords"
julia> g(1; y=2) # 버그: 덮어쓴 정의를 호출
"with keywords"
이 경고는 서술적이지 처방적이지 않아요. 코드는 이 동작에 의존하면 안 돼요. 특수화된 메서드가 키워드 인자 호출에도 적용되게 유지하려면, 키워드 인자를 반복하거나 kwargs...로 아무 키워드나 수집해 그 메서드 자신의 키워드 인터페이스를 주세요.
함수 같은 객체 (Function-like objects)
메서드는 타입과 연관되므로, 임의의 Julia 객체에 그 타입의 메서드를 추가해 "호출 가능(callable)"하게 만들 수 있어요. (그런 "호출 가능" 객체를 "펑터(functor)"라고 부르기도 해요.)
예를 들어 다항식의 계수를 저장하면서, 다항식을 평가하는 함수처럼 동작하는 타입을 정의할 수 있어요.
julia> struct Polynomial{R}
coeffs::Vector{R}
end
julia> function (p::Polynomial)(x)
v = p.coeffs[end]
for i = (length(p.coeffs)-1):-1:1
v = v*x + p.coeffs[i]
end
return v
end
julia> (p::Polynomial)() = p(5)
함수가 이름이 아니라 타입으로 지정된다는 점을 주목하세요. 일반 함수와 마찬가지로 간결한 문법 형태가 있어요. 함수 본문에서 p는 호출된 객체를 가리켜요. Polynomial은 이렇게 쓸 수 있어요.
julia> poly = Polynomial([1,10,100])
Polynomial{Int64}([1, 10, 100])
julia> poly(3)
931
julia> poly()
2551
이 메커니즘은 Julia에서 타입 생성자와 클로저(주변 환경을 참조하는 내부 함수)가 동작하는 방식의 핵심이기도 해요.
빈 제네릭 함수 (Empty generic functions)
때때로 메서드를 아직 추가하지 않고 제네릭 함수를 도입하는 게 유용해요. 인터페이스 정의와 구현을 분리하는 데 쓸 수 있죠. 문서화나 코드 가독성을 위해 그렇게 할 수도 있어요. 문법은 인자 튜플이 없는 빈 function 블록이에요.
function emptyfunc end
메서드 설계와 모호성 회피 (Method design and the avoidance of ambiguities)
Julia의 메서드 다형성은 가장 강력한 기능 중 하나지만, 이 힘을 활용하는 것은 설계 문제를 제기할 수 있어요. 특히 더 복잡한 메서드 계층에서는 모호성이 생기기 쉽죠.
위에서 이런 모호성을:
f(x, y::Int) = 1
f(x::Int, y) = 2
메서드를 정의해 해결할 수 있다고 지적했어요.
f(x::Int, y::Int) = 3
이건 흔히 옳은 전략이에요. 하지만 이 조언을 무작정 따르는 게 역효과를 낼 수 있는 상황도 있어요. 특히 제네릭 함수의 메서드가 많을수록 모호성의 가능성도 많아져요. 메서드 계층이 이 단순한 예시보다 복잡해지면, 대안 전략을 신중히 생각해 볼 가치가 있어요.
아래에서 특정 과제와 그런 문제를 해결할 몇 가지 대안을 논할게요.
튜플과 NTuple 인자 (Tuple and NTuple arguments)
Tuple(그리고 NTuple) 인자는 특별한 문제를 제기해요. 예를 들어,
f(x::NTuple{N,Int}) where {N} = 1
f(x::NTuple{N,Float64}) where {N} = 2
는 N == 0일 가능성 때문에 모호해요. Int 변형을 호출할지 Float64 변형을 호출할지 결정할 원소가 없으니까요. 모호성을 해결하려면 빈 튜플에 대한 메서드를 정의하는 접근이 하나예요.
f(x::Tuple{}) = 3
아니면 하나만 빼고 모든 메서드에서 튜플에 원소가 적어도 하나 있도록 강제할 수도 있어요.
f(x::NTuple{N,Int}) where {N} = 1 # 이것이 폴백
f(x::Tuple{Float64, Vararg{Float64}}) = 2 # 이것은 Float64 하나 이상을 요구
설계 직교화 (Orthogonalize your design)
두 개 이상의 인자에 디스패치하고 싶어질 때, "래퍼(wrapper)" 함수가 더 단순한 설계를 만들 수는 없는지 생각해 보세요. 예를 들어 여러 변형을 쓰는 대신:
f(x::A, y::A) = ...
f(x::A, y::B) = ...
f(x::B, y::A) = ...
f(x::B, y::B) = ...
이렇게 정의하는 걸 고려해 볼 수 있어요.
f(x::A, y::A) = ...
f(x, y) = f(g(x), g(y))
여기서 g는 인자를 타입 A로 변환해요. 이것은 별개의 개념을 별개의 메서드에 배정하는 직교 설계(orthogonal design)라는 더 일반적인 원리의 매우 구체적인 예시예요. 여기서 g는 폴백 정의가 필요할 가능성이 커요.
g(x::A) = x
관련 전략으로 promote를 활용해 x와 y를 공통 타입으로 끌어오는 게 있어요.
f(x::T, y::T) where {T} = ...
f(x, y) = f(promote(x, y)...)
이 설계의 한 가지 위험은, x와 y를 같은 타입으로 변환하는 적절한 승격 메서드가 없으면 두 번째 메서드가 자기 자신을 무한 재귀해 스택 오버플로를 일으킬 수 있다는 점이에요.
한 번에 한 인자씩 디스패치하기
여러 인자에 디스패치해야 하고, 폴백이 많아 모든 가능한 변형을 정의하는 게 비현실적이라면, (예를 들어) 첫 인자에 디스패치한 다음 내부 메서드를 호출하는 "이름 캐스케이드(name cascade)"를 도입해 보세요.
f(x::A, y) = _fA(x, y)
f(x::B, y) = _fB(x, y)
그러면 내부 메서드 _fA와 _fB는 x에 관해 서로 모호성을 걱정하지 않고 y에 디스패치할 수 있어요.
이 전략에는 큰 단점이 적어도 하나 있음을 알아두세요. 많은 경우 사용자가 내보낸 함수 f의 특수화를 더 정의해 f의 동작을 커스터마이즈할 수 없어요. 대신 내부 메서드 _fA·_fB의 특수화를 정의해야 하고, 이는 내보낸 메서드와 내부 메서드의 경계를 흐리게 해요.
추상 컨테이너와 원소 타입 (Abstract containers and element types)
가능하면 추상 컨테이너의 특정 원소 타입에 디스패치하는 메서드를 정의하는 것을 피하세요. 예를 들어,
-(A::AbstractArray{T}, b::Date) where {T<:Date}
는 이런 메서드를 정의하는 누구에게나:
-(A::MyArrayType{T}, b::T) where {T}
모호성을 만들어내요. 가장 좋은 접근은 이 메서드들 어느 쪽도 정의하지 않는 거예요. 대신 일반 메서드 -(A::AbstractArray, b)에 의존하고, 그 메서드가 (컨테이너 타입과 원소 타입 각각에 대해 올바르게 동작하는 similar, - 같은) 일반 호출로 구현되게 하세요. 이것은 메서드를 직교화하라는 조언의 더 복잡한 변형일 뿐이에요.
이 접근이 불가능할 때는 모호성 해결에 관해 다른 개발자들과 논의를 시작할 만해요. 어떤 메서드가 먼저 정의됐다고 해서 수정되거나 제거될 수 없는 건 아니니까요. 최후의 수단으로, 한 개발자가 모호성을 무식하게 해결하는 "밴드에이드(붕대)" 메서드를 정의할 수 있어요.
-(A::MyArrayType{T}, b::Date) where {T<:Date} = ...
기본 인자가 있는 복잡한 메서드 "캐스케이드"
기본값을 공급하는 메서드 "캐스케이드"를 정의할 때는, 잠재적인 기본값에 해당하는 인자를 빠뜨리지 않도록 조심하세요. 예를 들어 디지털 필터링 알고리즘을 작성 중이고, 패딩을 적용해 신호의 가장자리를 처리하는 메서드가 있다고 해 보죠.
function myfilter(A, kernel, ::Replicate)
Apadded = replicate_edges(A, size(kernel))
myfilter(Apadded, kernel) # 이제 "실제" 계산 수행
end
이것은 기본 패딩을 공급하는 메서드와 충돌해요.
myfilter(A, kernel) = myfilter(A, kernel, Replicate()) # 기본적으로 가장자리를 복제
이 두 메서드는 함께 A가 계속 커지면서 무한 재귀를 만들어내요.
더 나은 설계는 호출 계층을 이렇게 정의하는 거예요.
struct NoPad end # 패딩이 원치 않거나 이미 적용됐음을 나타냄
myfilter(A, kernel) = myfilter(A, kernel, Replicate()) # 기본 경계 조건
function myfilter(A, kernel, ::Replicate)
Apadded = replicate_edges(A, size(kernel))
myfilter(Apadded, kernel, NoPad()) # 새 경계 조건을 나타냄
end
# 다른 패딩 메서드는 여기에
function myfilter(A, kernel, ::NoPad)
# 여기가 핵심 계산의 "실제" 구현
end
NoPad는 다른 어떤 종류의 패딩과도 같은 인자 위치에 공급되므로, 디스패치 계층을 잘 정리된 상태로 유지하고 모호성의 가능성을 줄여줘요. 게다가 "공개" myfilter 인터페이스를 확장해서, 패딩을 명시적으로 제어하려는 사용자가 NoPad 변형을 직접 호출할 수 있게 해줘요.
로컬 스코프에서 메서드 정의 (Defining methods in local scope)
로컬 스코프 안에서 메서드를 정의할 수 있어요. 예를 들면:
julia> function f(x)
g(y::Int) = y + x
g(y) = y - x
g
end
f (generic function with 1 method)
julia> h = f(3);
julia> h(4)
7
julia> h(4.0)
1.0
하지만 로컬 메서드를 조건부로 정의하거나 제어 흐름에 따라 정의하는 것은 피해야 해요. 이렇게:
function f2(inc)
if inc
g(x) = x + 1
else
g(x) = x - 1
end
end
function f3()
function g end
return g
g() = 0
end
결국 어떤 함수가 정의될지 분명하지 않으니까요. 미래에는 이런 방식으로 로컬 메서드를 정의하는 게 오류가 될 수도 있어요.
이런 경우에는 대신 익명 함수를 쓰세요.
function f2(inc)
g = if inc
x -> x + 1
else
x -> x - 1
end
end
- 1 C++이나 Java에서, 예를 들어
obj.meth(arg1,arg2)같은 메서드 호출에서 객체obj는 메서드 호출을 "받고", 명시적인 메서드 인자가 아니라this키워드를 통해 메서드에 암묵적으로 전달돼요. 현재this객체가 메서드 호출의 수신자일 때는 완전히 생략할 수 있고,this를 수신 객체로 암묵화하며meth(arg1,arg2)처럼만 쓸 수 있어요. - Clarke61 Arthur C. Clarke, Profiles of the Future (1961): Clarke의 제3법칙.
더 알아보기
- 함수 (Functions) — 메서드의 기초가 되는 함수 개념
- 타입 (Types) — 디스패치가 의존하는 타입 시스템
- 다중 디스패치에 관한 더 깊은 내용은 Julia의 타입·구조체 문서