이 강의를 따라오면서 이미 함수 정의를 아주 많이 봐 왔을 거예요. 그런데 보통은 REPL에 나오는 출력에 대한 설명 없이 지나갔죠:
julia> f(x) = x^2
f (generic function with 1 method)
왜 "generic function with 1 method" 일까요?
인자 타입을 좀 더 구체적으로 지정하고 싶다고 해 봐요.
julia> f(x::Integer) = x^2
f (generic function with 2 methods)
julia> f(x::Real) = x^2
f (generic function with 3 methods)
아니면 인자의 개수를 바꿀 수도 있어요:
julia> f(x, y) = x * y
f (generic function with 4 methods)
같은 함수를 새로 정의한 것을 "메서드"라고 부르는 것 같아요. 메서드는 예전 정의를 덮어쓰는 대신 목록에 추가돼요.
메서드 목록은 이렇게 쉽게 볼 수 있어요:
julia> methods(f) # output edited for brevity
# 4 methods for generic function "f" from Main:
[1] f(x, y)
[2] f(x::Integer)
[3] f(x::Real)
[4] f(x)
함수 f를 호출하면, Julia는 어떤 메서드를 사용해야 할지 알아서 판단해요:
julia> f(2.5) # Float64
6.25
julia> f(2) # Int64
4
julia> f(2, 3) # Int64, Int64
6
매뉴얼에 따르면, 함수는 인자로 이루어진 튜플을 반환값으로 대응시키는 객체예요.
우리는 보통 f(2, 3)의 인자를 서로 별개의 두 항목으로 생각하지만, 컴파일러는 이 둘을 하나의 튜플로 봐요. 인자가 하나뿐일 때도 마찬가지예요(다만 이 점을 분명히 하려면 쉼표를 붙여야 하죠):
julia> typeof( (2, 3) )
Tuple{Int64, Int64}
julia> typeof( (2,) )
Tuple{Int64}
함수를 블랙박스로 보면, 튜플이 들어가고 반환값이 나온다고 말할 수 있어요. 더 엄밀히 말하면, 튜플이 반환값으로 _대응_돼요.
이런 것을 보통 "다형성"이라고 불러요.
수십 년 동안 컴퓨터 과학의 큰 주제였고, 여러 가지 접근 방식이 채택되어 왔어요.
Julia의 접근 방식은 독특하지만, 유일한 것은 아니에요.
+ 연산자는 메서드가 161개인 함수예요).... 스플랫/슬러프 연산자로 이 규칙을 어느 정도 우회할 수 있기는 하죠).dispatched되어 결과를 반환해요.MethodError가 발생해요.Julia는 동적 타이핑을 사용하므로, 메서드 디스패치는 _런타임_에 일어난다는 점을 기억해 두세요.
앞 절은 말이 많았으니, 이번에는 그 아이디어를 코드로 옮겨 봐요. 간단히 하기 위해 아래 함수들은 인자가 하나뿐이지만, 인자가 수십 개인 함수도 흔해요(플로팅 패키지나 머신러닝에서처럼요).
먼저 도형을 나타내는 커스텀 타입 몇 개를 정의해 봐요.
julia> abstract type Shape end
julia> struct Circle <: Shape
radius::Float64
end
julia> struct Square <: Shape
side::Float64
end
julia> struct Rectangle <: Shape
length::Float64
width::Float64
end
다음으로 각 도형의 넓이를 계산하는 메서드를 정의할 수 있어요. 각 메서드는 인자를 하나씩 받고, 그 타입은 매번 서로 다른 구체적인 사용자 정의 타입이에요.
julia> area(c::Circle) = π * c.radius^2
area (generic function with 1 method)
julia> area(s::Square) = s.side^2
area (generic function with 2 methods)
julia> area(r::Rectangle) = r.length * r.width
area (generic function with 3 methods)
이제 도형 몇 개를 만들고 넓이를 계산해 봐요. 각 도형에 맞는 공식을 써 주는 것은 Julia의 디스패치 메커니즘에 맡기면 돼요.
julia> circle = Circle(2.3)
Circle(2.3)
julia> area(circle) # call signature Tuple{Circle}
16.619025137490002
julia> square = Square(1.6)
Square(1.6)
julia> area(square) # call signature Tuple{Square}
2.5600000000000005
julia> rectangle = Rectangle(1.4, 2.1)
Rectangle(1.4, 2.1)
julia> area(rectangle) # call signature Tuple{Rectangle}
2.94
메서드 시그니처는 파라메트릭 타입과 거의 비슷한 방식으로, 더미 타입 값을 받을 수 있어요.
문법이 독특한데, where 절이 필요해요:
julia> same_type(v::Vector{T}, x::T) where {T} = true
same_type (generic function with 1 method)
julia> same_type([1, 2], 3)
true
julia> same_type([1, 2], "three")
ERROR: MethodError: no method matching same_type(::Vector{Int64}, ::String)
위 예제에서는 두 인자가 같은 타입 T를 사용해야 해요. 그 T가 무엇이든 말이죠.
T에 제약을 걸면 더 구체적으로 만들 수 있어요:
julia> same_type(v::Vector{T}, x::T) where {T<:Number} = true
same_type (generic function with 2 methods)
이 메서드에서는 숫자 타입이 서로 같을 때만 true를 반환해요.
더미 변수는 여러 개를 쓸 수 있고, where 절에서 쉼표로 구분해요.
julia> myfunc(a::S, b::T) where {S, T} = true
myfunc (generic function with 1 method)
다중 디스패치는 Julia에서 매우 유연하고 성능도 뛰어나서 아주 폭넓게 쓰여요.
+에 메서드가 161개 있다는 예에서 알 수 있듯이, Julia 프로그래머들은 타입을 좁게 지정한 메서드를 정의하려고 해요.
바이트코드 수준에서 정수 두 개를 더하는 것과 실수 두 개를 더하는 것은 아주 다르고, 정수에 실수를 더하면 타입 프로모션이라는 복잡한 문제가 하나 더 생겨요.
타입이 구체적일수록 컴파일러가 할 수 있는 최적화가 많아져요. Exercism의 장난감 같은 연습 문제에서는 별로 중요하지 않지만, Julia가 겨냥한 대규모 수치 시뮬레이션에서는 성능에 결정적이에요.
메서드가 어떤 입력을 받을지 안다면, 컴파일러에게 꼭 알려 주세요!
이 연습 문제에서는 만남 시뮬레이션을 구현해요. 이를 통해 Julia의 핵심 패러다임인 다중 디스패치의 기초를 익힐 수 있어요.
For this exercise, all function definitions should be a single-line declaration with no if/else logic.
일반적으로 만남이란 한 개체 a가 다른 개체 b를 만나 반응하는 것을 말해요.
encounter(a, b) = "$(name(a)) meets $(name(b)) and $(meets(a, b))."
먼저 고양이와 개가 만나면 무슨 일이 일어나는지 시뮬레이션해 봐요.
먼저 추상 타입 Pet을 추가해요.
그다음 Cat과 Dog 타입을 Pet의 하위 타입으로 정의해요.
각 타입에는 name 필드가 하나씩 있어요.
반려동물의 이름을 반환하는 name() 함수를 구현해요.
julia> fido = Dog("Fido")
Dog("Fido")
julia> name(fido)
"Fido"
다음 만남에 대한 meets() 메서드를 구현해요:
julia> ginger = Cat("Ginger")
Cat("Ginger")
julia> meets(ginger, fido)
"hisses"
이름이 있는 두 개체가 만나면 어떤 일이 일어나는지 알려주도록, encounter() 함수를 메서드 하나로 구현해요.
julia> encounter(ginger, fido)
"Ginger meets Fido and hisses."
산책하다가 말처럼 다른 반려동물을 만나면 어떻게 될까요?
julia> bella = Horse("Bella")
Horse("Bella")
julia> encounter(fido, bella)
"Fido meets Bella and is cautious."
반려동물이 만날 수 있는 대상 중에는 반려동물이 아닌 것도 많아요. 자동차, 사람, 식물, 자연재해, 소행성… 그럼 그때는 어떻게 될까요?
julia> colnago = Bicycle("Colnago")
Bicycle("Colnago")
julia> encounter(ginger, colnago)
"Ginger meets Colnago and runs away."
우리 시뮬레이션에서는 다른 만남도 얼마든지 일어날 수 있어요. 자동차가 개를 만나기도 하고, 두 전자가 서로 만나 상호작용하기도 하죠…
모든 만남을 미리 다루는 것은 불가능하니, 일반적인 대체 동작을 구현해 볼게요.
julia> γ1 = Photon("γ1")
Photon("γ1")
julia> γ2 = Photon("γ2")
Photon("γ2")
julia> encounter(γ1, γ2)
"γ1 meets γ2 and nothing happens."
# This is true, photons just pass through each other.
Exercism에 가입하고 Julia 트랙을 개념 35개연습 문제 128개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.