2022.10.04
Julia 에서 함수는 인자로 주어지는 튜플을 반환값으로 매핑시키는 객체이다. Julia 함수는 수학적인 함수와는 다른데, 이는 프로그램의 전역적 상태에 영향을 받거나, 변경될 수 있기 때문이다. 함수를 정의하는 기본 문법은 다음과 같다 :
julia> function f(x,y)
x + y
end
f (generic function with 1 method)
이 함수는 x
, y
의 두 인자를 받고 계산된 마지막 표현식을 리턴한다. 여기서는 x + y
이다. (명시적으로 return x + y
를 써주지 않아도 마지막 표현식을 리턴한다는 것에 유의하라.)
좀 더 함수를 간결하게 정의하는 두번째 방법이 있다. 위에서 보인 전통적인 함수 선언 문법은 아래의 간결한 "할당 형식(assignment form)"와 동등하다.
julia> f(x,y) = x + y
f (generic function with 1 method)
할당 형식에서는 함수의 몸체가 단일 표현식이나 begin ~ end
로 표현되는 복합표현식(Compound Expression) 이어야 한다. 짧고 간결한 함수 정의는 Julia 에서 자주 사용된다. 간소한 함수 문법은 따라서 매우 관용어법적(idomatic)이어서, 타이핑과 시작적 노이즈를 매우 줄여준다.
함수는 전통적인 소괄호 문법을 사용하여 호출된다.
julia> f(2,3)
5
괄호가 없다면, f
는 함수 객체를 지칭하며 값처럼 전달 될 수 있다:
julia> g = f;
julia> g(2,3)
5
변수처럼 함수 이름에 유니코드가 사용 될 수 있다:
julia> ∑(x,y) = x + y
∑ (generic function with 1 method)
julia> ∑(2, 3)
5
타입 선언 처럼, 함수에 전달되는 인자의 이름에 ::TypeName
을 덧붙여서 인자의 타입을 선언 할 수 있다. 예를 들어 아래의 함수는 재귀적으로 피보나치 수를 계산하는 함수이다.
fib(n::Integer) = n ≤ 2 ? one(n) : fib(n-1) + fib(n-2)
여기서 ::Integer
를 특정함으로서 n
이 추상적인 정수 타입(Integer
)의 서브타입일 경우에만 함수가 호출될 수 있다.
인자 타입 선언은 보통은 성능에 영향을 주지 않는다 : 인자의 타입이 어떻게 선언되었는지와 상관 없이 Julia 는 함수를 호출한 어떤 것에 전달되는 실제 인자에 맞춰 특벌한 버젼의 함수를 컴파일한다. 예를 들어, fib(1)
은 Int
타입 인자에 특별히 최적화된 버젼으로 fib
을 컴파일하도록 하며, 이것은 fib(7)
이나 fib(15)
에서 다시 호출된다. (인자 타입 선언이 추가적인 컴파일러 특정화를 유발하는 드문 경우에 대해서는 Be aware of when Julia avoids specializing 을 참고하라.) Julia 에서 인자 타입을 선언하는 가장 일반적인 이유는 다음과 같다.
Dispatch : 메서드(Methods) 에 설명되었듯이, 서로 다른 인자 타입에 대해 서로 다른 버전의 함수 (각각을 "메서드(method)" 라 한다) 가 존재하며, 인자의 타입은 어떤 구현이 호출될지를 결정하는데 사용된다. 예를 들자면, 당신은 피보나치 수를 Binet 의 공식 이용하여 정수가 아닌 수까지 확장하는 완전히 다른 알고리즘으로 fib(x::Number) = ...
를 구현할 수 있다.
정확성 : 당신의 함수가 특정한 인자 타입에 대해 오직 정확한 값만을 반환하도록 하는데 타입 선언이 사용될 수 있다. 예를 들면, 당신이 인자 타입을 생략하고 fib(n) = n ≤ 2 ? one(n) : fib(n-1) + fib(n-2)
라고 썼다면, fib(1.5)
는 1.0
이라는 무의미한 결과를 소리 없이 제시할 겻이다.
명확성 : 타입 선언은 기대되는 인자에 대한 문서화의 형태로서 작용한다.
그러나, 인자 타입을 과도하게 제한하는 것은 일반적인 실수이다. 이것은 불필요하게 함수의 응용성을 제한하며, 당신이 인식하지 못하는 상황에서 함수가 재사용되는 것을 막는다. 예를 들어 fib(n::Integer)
함수는 Int
뿐만 아니라 BigInt
(BigFloats and BigInts 를 보라) 라는 임의 정밀도 정수에도 똑같이 잘 기능한다. 파보나치 수는 지수적으로 증가하며 어떤 고정 정밀도 타입(예를 들면 Int
, Overflow behavior) 의 범위를 순식간에 벗어므로 BitInt
가 유용하다. 우리가 fib(n::Int)
로 정의했다면 BigInt
에 사용하는것은 이유 없이 금지될 것이다. 일반적으로 당신은 가장 일반적으로 적용할수 있는 추상 타입을 인자에 사용해야 하며, 의심이 들면 인타 타입을 생략해야 한다. 나중에라도 필요할 경우 인자 타입을 특정할 수 있으며, 따라서 그것을 생략함으로서 성능과 기능을 희생시키지 않는다.
return
키워드함수에서 반환된 값은 계산되는 마지막 표현식의 값이며, 기본적으로 함수 정의에서의 마지막 표현식이다. 앞의 절에서 보기로 제시된 함수 f
에서는 반환값은 x+y
표현식의 값이다. 많은 다른 언어처럼 return
키워드 와 그 다음에 오는 표현식으로 함수가 그 표현식의 결과를 즉각적으로 반환하도록 하는 방법도 있다.
function g(x,y)
return x * y
x + y
end
함수 정의가 상호작용 세선에서 입력되었기 때문에, 이 함수들을 비교하기 용이하다.
julia> f(x,y) = x + y
f (generic function with 1 method)
julia> function g(x,y)
return x * y
x + y
end
g (generic function with 1 method)
julia> f(2,3)
5
julia> g(2,3)
6
물론, g
와 같은 순전히 한줄로 늘어선 함수의 몸체(역자 주 : 원문에서는 linear function body 라는 표현을 썼는데, 선형 함수일리는 없고, 아마도 g
함수 몸체가 분기나 루프문 없이 위에서 아래로 죽 표현식을 평가하기만 하는 형태이기때문에 이렇게 표현한 것이라 생각된다) 형태에서는 x + y
표현은 평가되지 않으며 x * y
를 함수의 마지막 표현식으로 만들고 리턴을 생략할 수 있기 때문에 return
의 사용은 무의미하다. 하지만 다른 흐름 제어(control flow) 와 결합할 때 return
은 실제 쓸모가 있다. 여기 직각삼각형의 두 직각변 길이 x
, y
가 주어졌을 때 빗변의 길이를 오버플로우(overflow)를 피하며 계산하는 함수를 보기로 제시한다.
julia> function hypot(x,y)
x = abs(x)
y = abs(y)
if x > y
r = y/x
return x*sqrt(1+r*r)
end
if y == 0
return zero(x)
end
r = x/y
return y*sqrt(1+r*r)
end
hypot (generic function with 1 method)
julia> hypot(3, 4)
5.0
세계의 반환(return) 지점이 있는데, x
와 y
의 값에 따라 세계의 서로 다른 표현식에 의한 값을 반환한다. 마지막 줄의 return
은 마지막 표현식이므로 생략 될 수 있다.
반환 타입은 ::
연산자를 이용하여 함수에서 특정 될 수 있다. 이것은 반환값을 특정된 타입으로 변환한다.
julia> function g(x, y)::Int8
return x * y
end;
julia> typeof(g(1, 2))
Int8
이 함수는 x
와 y
의 타입에 상관 없이 항상 Int8
타입의 값을 반환한다. 반환 타입에 대해서는 타입 선언(Type Declarations) 에 자세히 기술되어 있다.
리턴 타입은 Julia 에서 드물게 사용된다: 일반적으로는 Julia 의 컴파일러가 자동적으로 반환 타입을 추정할 수 있도록 "타입-안정적"인 함수를 작성해야만 한다. 더 자세한 정보는 성능 팁(Performance Tips) 을 보라
nothing
을 반환하기어떤 값을 반환할 필요가 없는 함수 (어떤 부수적 효과만을 위한 함수) 에 대해서는 Julia 의 관례상 nothing
을 반환한다:
function printx(x)
println("x = $x")
return nothing
end
이것이 관례라는 것은 nothing
이 Julia 의 키워드가 아니라 Nothing
타입의 singlton 객체라는 의미이다. 또한 위에 보기를 든 printx
함수가 다소 부자연스러울 텐데, println
이 이미 nothing
을 반환하므로 return
줄은 불필요하기 때문이다.
return nothing
표현에 대한 두가지 단축 형태가 가능하다. 하나는 return
키워드가 암묵적으로 nothing
을 반환하므로 단족으로 쓰일 수 있다. (역주 : 즉 return
만 쓰더라도 return nothing
과 같다.) 다른 한편, 함수는 암묵적으로 마지막 표션식을 평가하여 반환하므로 nothing
이 마지막 표현식이면 단독으로 사용 될 수 있다. return
이나 nothing
을 단독으로 사용하는 것에 대해 return nothing
을 선호하는 것은 코딩 스타일의 문제이다.
Julia 에서 대부분의 연산자는 특별한 문법을 지원하는 함수일 뿐이다(예외적으로 &&
나 ||
특별한 평가 의미(evaluation sematics) 를 가진다. Short-Circuit Evaluation 에 의해 피연산는 연산자의 평가가 끝나기 전에는 평가되지 않기 때문에, 이 연산자는 함수가 아니다). 따라서, 이 연선자들은 함수처럼 괄호안에 인자를 넣어서 사용 할 수 있다.
julia> 1 + 2 + 3 # infix form
6
julia> +(1,2,3) # functiion appliciiton form
6
이 삽입 형식(infix form)[^1]은 함수 이용 형식과 동치이다 - 사실 전자는 내부적으로 함수를 호출하도록 파싱된다. 이것은 다른 함수 값과 마찬가지로 +
및 *
와 같은 연산자를 할당하고 전달할 수 있다는 것을 뜻한다.
julia> f = +;
julia> f(1,2,3)
6
그러나 f
의 이름으로는 삽입 형식(infix form) 으로 사용 할 수 없다. (역자 주 : 즉 1 f 2
는 오류이다)
소수의 특별한 표현식은 특별한 이름을 가진 함수의 호출에 해당한다.
표현식 (Expression) | 해당되는 함수 |
---|---|
[A B C ...] | hcat |
[A; B; C; ...] | vcat |
[A B; C D; ...] | hvcat |
A' | adjoint |
A[i] | getindex |
A[i] = x | setindex! |
A.n | getproperty |
A.n = x | setproperty! |
Julia 에서 함수는 일급 객체(first-class objects) 이다 : 변수에 할당 될 수 있으며, 그들이 할당된 변수를 이용하여 표준적인 함수 호출로 호출될 수 있어야 한다. 다른 함수의 인자가 될 수 있고, 값으로써 반환 될 수 있다. 아래의 두가지 방법 중 하나를 이용하여 함수를 주어진 이름 없이 익명으로 만들 수 있다.
julia> x -> x^2 + 2x - 1
#1 (generic function with 1 method)
julia> function (x)
x^2 + 2x - 1
end
#3 (generic function with 1 method)
하나의 인자 x
를 받아서 이 값에 대한 다항식 x^2 + 2x -1
의 값을 반환하는 함수를 만들었다. 결과는 일반적인 함수이지만 연속적으로 번호가 부여되는 컴파일러가 생성한 이름(역자 주 : 위에서는 #1
이나 #3
)을 갖고 있다는 것을 확인 할 수 있다.
익명 함수의 주된 사용처는 함수를 인자로 받는 함수에 전달하는 것이다. 고전적인 예는 map 인데 map
은 배열의 각각의 값에 함수를 적용하여 그 결과값을 새로운 배열로 반환하는 함수이다.
julia> map(round, [1.2, 3.5, 1.7])
3-element Vector{Float64}:
1.0
4.0
2.0
변환에 영향을 주는 이름을 가진 함수가 이미 존재해서 map
의 첫번째 인자로 전달되었기 때문에 괜찮았다. 때때로 이름을 가진 함수가 즉각적으로 사용할 수 있도록 준비되어있지 않은 경우가 있다. 이런 경우 악명함수는 이름 없이 단 한번만 사용하는 함수를 만들 수 있도록 해 준다.
julia> map(x -> x^2 + 2x - 1, [1, 3, -1])
3-element Vector{Int64}:
2
14
-2
다수의 인자를 받는 익명 함수는 (x,y,z)->2x+y-z
의 문법을 사용하여 작성될 수 있다. 인자가 필요 없는 익명함수는 ()->3
처럼 작성할 수 있다. 인자가 없는 함수(무인자 함수)라는 아이디어는 이상하게 보일 수 있지만 계산을 "지연"시키는데 유용하다. 이 사용법에서 코드 블록은 무인자 함수로 래핑되며 나중에 f
로 호출하여 호출된다.
예를 들어 다음 get
함수 호출을 생각하자. (역자 주 : get(f::Function, collection, key)
의 문서를 보면 알겠지만 collection
에 key
에 해당되는 값이 있으면 그 값을 반환하지만, 없으면 함수 f
를 실행시킨다.):
get(dict, key) do
# default value calculated here
time() # 현재 시간을 초 단위로 반환한다.
end
위의 코드는 do
와 end
로 둘러쌓인 익명의 함수와 함께 get
을 호출하는 아래의 코드와 동일하다.
get(()->time(), dict, key)
무인자 익명 함수는 (역자 주 : get
함수의 정의에 의해서) key
에 해당하는 값이 dict
에 없을 때 호출되며, time
을 호출하는 것은 이 무인자 익명함수 안에 포함되었기 때문에 지연된다.
Julia 는 tuple
(튜플) 이라 불리는 내장된 자료구조가 있는데, 함수 인자와 반환 값들에 밀접하게 관련되어 있다. 튜플은 어떤 값이나 포함할 수 있는 고정된 길이를 가진 컨테이너이지만 수정은 불가능하다 (즉 immutable 하다.) 튜플은 소괄호와 쉼표로 만들어지며 인덱스로 접근한다.
julia> (1, 1+1)
(1, 2)
julia> (1,)
(1,)
julia> x = (0.0, "hello", 6*7)
(0.0, "hello", 42)
julia> x[2]
"hello"
길이가 1인 튜플은 (1, )
과 같이 쉼표와 같이 사용되어야 한다. (1)
은 단지 괄호를 친 값일 뿐이다. ()
은 길이가 0 인 빈 튜플이다.
튜플의 성분은 이름이 붙여질 수 있는데 이 경우 지명 튜플(named tuple) 이 만들어진다.
julia> x = (a=2, b=1+2)
(a = 2, b = 3)
julia> x[1]
2
julia> x.a
2
지명 튜플은 인덱스 문법 (x[1]
) 뿐만 아니라 도트 문법 (x.a
) 을 이용하여 접근할 수 있으며, 이것을 제외하면 튜플과 매우 유사하다.
할당의 왼쪽 편에 쉼표로 분리된 변수들의 리스트 (괄호 안애 포함될 수도 있다) 가 나타날 수 있다. 오른쪽의 값들은 변수들에 순서대로 할당되면서 그 구조가 해체된다(destructed) .
julia> (a,b,c) = 1:3
1:3
julia> b
2
오른쪽의 값은 반복자(iterator) 이어야 하며 (Iteration interface 를 보라) 왼쪽에 있는 변수의 갯수보다 같거나 많아야 한다.(반복자의 여분의 원소들은 무시된다)
함수에서 다수의 값을 반환할 때는 튜플이나 다른 iterable 한 값을 반환한다. 예를 들어 아래의 함수는 두 값을 반환한다 :
julia> function foo(a,b)
a+b, a*b
end
foo (generic function with 1 method)
만약 interactive session 에서 반환값을 어디서도 할당 하지 않고 호출한다면, 튜플이 호출되는 것을 알 수 있다.
julia> foo(2,3)
(5, 6)
탈구조화하는 할당은 각각의 값을 변수로 추출한다.
julia> x, y = foo(2,3)
(5, 6)
julia> x
5
julia> y
6
자주 사용되는 다른 사용법은 변수의 값을 교환하는 것이다 :
julia> y, x = x, y
(5, 6)
julia> x
6
julia> y
5
단지 반복자의 원소중 일부만이 필요하다면, 일반적인 관례는 불필요한 원소를 오직 _
로만 이루어진 변수에 할당하는 것이다 ( _
만으로 이루어진 변수명은 할당문의 왼쪽에만 올 수 있다. 허용된 면수 이름(Allowed Variable Names) 을 보라)
julia> _, _, _, d = 1:10
1:10
julia> d
4
다른 유효환 오른쪽 표현(left-hand side expressions, 할당문의 오른쪽에만 가능한 표현) 도 할당 리스트의 원소로서 사용 될 수 있는데, 이것은 setindex!
나 setproperty!
함수를 호출하거나, 재귀적으로 반복자의 개별 원소를 탈구조화시킨다.
julia> X = zeros(3);
julia> X[1], (a,b) = (1, (2, 3))
(1, (2, 3))
julia> X
3-element Vector{Float64}:
1.0
0.0
0.0
julia> a
2
julia> b
3
주의
할당에 사용되는
...
는 Julia 1.6 이상에서만 가능하다.
할당의 마지막 기호의 끝에 ...
가 붙어있다면 (slurping 이라 한다), 오른쪽의 반복자에서 남은 것들은 collection 이나 lazy iterator 로서 할당된다.
julia> a, b... = "hello"
"hello"
julia> a
'h': ASCII/Unicode U+0068 (category Ll: Letter, lowercase)
julia> b
"ello"
julia> a, b... = Iterators.map(abs2, 1:4)
Base.Generator{UnitRange{Int64}, typeof(abs2)}(abs2, 1:4)
julia> a
1
julia> b
Base.Iterators.Rest{Base.Generator{UnitRange{Int64}, typeof(abs2)}, Int64}(Base.Generator{UnitRange{Int64}, typeof(abs2)}(abs2, 1:4), 1)
특정한 반복자에 대한 정확한 처리와 custumization 에 대해서는 Base.rest 를 참고하라.
Iteration 에 기반한 탈구조화 대신에 속성 이름을 사용하여 할당의 오른편이 탈구조화 될 수 있다. 이것은 지명 튜플(NamedTuples)의 문법을 따르며 getproperty
를 사용하여 동일한 이름을 가진 할당 오른쪽의 속성을 왼쪽에 있는 각 변수에 할당하여 작동한다.
julia> (; b, a) = (a=1, b=2, c=3)
(a = 1, b = 2, c = 3)
julia> a
1
julia> b
2
탈구조화의 특성은 함수 인자 내부에 사용될 수 있다. 만약 함수 인자의 이름이 기호가 아닌 튜플이라면 (즉, (x, y)
라면) 할당 표현 (x, y) = argument
가 삽입된다.
julia> minmax(x, y) = (y < x) ? (y, x) : (x, y)
julia> gap((min, max)) = max - min
julia> gap(minmax(10, 2))
8
gap
의 정의에서 여분의 괄호쌍이 있음을 알 수 있다. 이 괄호쌍이 없다면 gap
은 두개의 인자를 받는 함수이며, 위의 예는 작동하지 않을 것이다.
비슷하게, 속성 탈구조화는 함수 인자에 사용될 수 있다.
julia> foo((; x, y)) = x + y
foo (generic function with 1 method)
julia> foo((x=1, y=2))
3
julia> struct A
x
y
end
julia> foo(A(3, 4))
7
익명 함수에서, 단족 인자를 탈구조화 시키는것은 여분의 쉼표를 필요로한다 :
julia> map(((x,y),) -> x + y, [(1,2), (3,4)])
2-element Array{Int64,1}:
3
7
임의의 갯수의 인자를 취하는 함수를 작성하는 것이 편리할 때가 있다. 이런 함수는 전통적으로 가변 인자 함수(vararg function)라 하는데 이것은 "variable number of argumens" 의 약자이다. 가변인자 함수는 마지막 인자에 말줄임표 ...
를 붙여서 정의 할 수 있다.
julia> bar(a,b,x...) = (a,b,x)
bar (generic function with 1 method)
변수 a
, b
는 첫 두 인자에 바인딩되며 이며 변수 x
는 첫 두 인자 다음에 오는 bar
에 전달되는 모든 인자(하나도 없을 수도 있다) 들의 iterable collection 에 바인딩된다.
julia> bar(1,2)
(1, 2, ())
julia> bar(1,2,3)
(1, 2, (3,))
julia> bar(1, 2, 3, 4)
(1, 2, (3, 4))
julia> bar(1,2,3,4,5,6)
(1, 2, (3, 4, 5, 6))
위의 모든 경우에 x
는 bar
로 전달되는 값들의 끝부분으로 이루어진 튜플에 바인드 되어 있다.
변수에 인자에 전달되는 값들의 갯수를 재한 할 수 있다: 이것은 매개적으로 제한된 가변인자함수 메서드(Parametrically-constrained Varargs methods) 에 기술되어 있다.
반대로, iterable 컬렉션에 포함된 값을 개별 인수로 함수 호출에 "분할"하는 것이 종종 편리하다. 이것을 위해, 앞서와 같이 ...
를 사용하지만 이번은 함수 호츨 내부에서 사용한다.
julia> x = (3, 4)
(3, 4)
julia> bar(1,2,x...)
(1, 2, (3, 4))
이 경우 전달되는 값들로 이루어진 튜플은 가변 개수의 인수의 정확한 위치에 따른 가변 인자 함수 호출로 연결된다. 그러나 아래의 경우에는 그럴 필가 없다.
julia> x = (2, 3, 4)
(2, 3, 4)
julia> bar(1,x...)
(1, 2, (3, 4))
julia> x = (1, 2, 3, 4)
(1, 2, 3, 4)
julia> bar(x...)
(1, 2, (3, 4))
더우기, 함수 호출로 분할되는 객체는 튜플이 아니어도 iterable 객체이기만 하면 된다.
julia> x = [3,4]
2-element Vector{Int64}:
3
4
julia> bar(1,2,x...)
(1, 2, (3, 4))
julia> x = [1,2,3,4]
4-element Vector{Int64}:
1
2
3
4
julia> bar(x...)
(1, 2, (3, 4))
또한, 인자가 분할되는 함수는 가변인자 함수일 필요가 없다 (비록 가변인자일 경우가 많기는 하지만):
julia> baz(a,b) = a + b;
julia> args = [1,2]
2-element Vector{Int64}:
1
2
julia> baz(args...)
3
julia> args = [1,2,3]
3-element Vector{Int64}:
1
2
3
julia> baz(args...)
ERROR: MethodError: no method matching baz(::Int64, ::Int64, ::Int64)
Closest candidates are:
baz(::Any, ::Any) at none:1
위에서 볼 수 있듯이, 분할되는 컨테이너 안의 원소의 갯수가 맞지 않다면, 마치 너무 많은 인자가 명시적으로 주어진 것처럼 함수 호출에 실패한다.
함수 인자에 적절한 기본값을 제시하는것이 가능할 경우가 많다. 이것은 이용자가 모든 함수 호출에 대해 모든 인자를 전달하는 수고를 덜어준다. 예를 들면 Dates
모듈의 함수 Date(y, [m, d]) 는 주어진 년도 y
, 달 m
, 일 d
를 이용하여 Date
타입을 구성한다. 그러나 m
과 d
인자는 기본값 1
이 주어져 있다. 이 기능은 정확히 다음과 같이 표현된다 :
function Date(y::Int64, m::Int64=1, d::Int64=1)
err = validargs(Date, y, m, d)
err === nothing || throw(err)
return Date(UTD(totaldays(y, m, d)))
end
이 정의는 UTInstant{Day}
타입의 인자를 취하는 Data
함수의 다른 메서드를 호출한다.
이 정의에 의해 함수는 하나, 둘, 혹은 세개의 인자로 호출 될 수 있으며, 하나 혹은 두개의 인자만 특정될 경우 1
이 자동적으로 전달된다 :
julia> using Dates
julia> Date(2000, 12, 12)
2000-12-12
julia> Date(2000, 12)
2000-12-01
julia> Date(2000)
2000-01-01
기본값이 제공되는 인자는 실제로 다른 갯수의 인수로 여러 메서드 정의를 작성하기 위한 편리한 구문에 불과하다(선택적 인수 및 키워드 인수에 대한 노트 를 참고하라). 이것은 메소드 함수를 호출함으로써 Date
함수 예제에서 확인할 수 있다.
어떤 함수는 다수의 인자를 필요로하거나 다수의 동작이 있다. 이러한 함수를 호출하는 방법을 기억하는 것은 어려울 수 있다. 키워드 인자를 사용하면 인자를 위치로만 식별하는 대신 이름으로 식별할 수 있으므로 이러한 복잡한 인터페이스를 더 쉽게 사용하고 확장할 수 있다.
예를 들어 선을 그리는 함수 plot
을 생각해 보자. 이 함수는 선의 스타일, 폭, 색상 등을 제어하기 위한 많은 옵션이 있을 수 있다. 키워드 인자를 허용하는 경우 가능한 호출은 선 너비만 지정하도록 선택한 plot(x, y, width=2)
처럼 보일 수 있다. 이것은 두 가지 용도로 사용된다. 인자를 의미에 따라 레이블을 지정할 수 있으므로 함수 호출시 가독성이 좋아진다. 또한 많은 인자들 중 일부를 순서에 관계없이 전달할 수 있다.
키워드 인수가 있는 함수는 시그니처(signature)[^2]에서 세미콜론을 사용하여 정의된다.
function plot(x, y; style="solid", width=1, color="black")
###
end
함수를 호출할 때는 세미콜론은 선택사항이다 : plot(x, y, width=2)
나 plot(x, y;widt=2)
둘 다 가능하며 다만 전자가 좀 더 일반적이다. 명시적으로 세미콜론을 써야할 경우는 아래에 설명하겠지만 가변인자를 전달할때나 계산된 키워드(computed keywords) 를 전달할 때 뿐이다.
키워드 인자의 기본값은 필요할 때만(그 키워드 인자가 전달되지 않을 때), 왼쪽에서 오른쪽 순서로 전달된다. 따라서 기본 표현식은 앞서 나온 키워드 인자를 참조 할 수 있다.
키워드 인자의 타입은 아래처럼 명시적으로 선언될 수 있다 :
function f(;x::Int=1)
###
end
키워드 인자는 가변인자 함수에 사용될 수 있다 :
function plot(x...; style="solid")
###
end
여분의 키워드 인자는 가변인자 함수에서 ...
를 사용하여 취합할 수 있다.
function f(x; y=0, kwargs...)
###
end
f
내부에서 kwargs
는 지명 튜플에 대한 immutable 한 키-값(key-value) 반복자이다. 지명 튜플 (과 Symbol
타입의 키를 가진 사전(dictionary)) 은 함수를 호출할 때 세미콜론을 이용하여 키워드 인자로 전달 될 수 있다.
메서드 정의에서 키워드 인자에 대한 기본값이 정해지지 않았다면, 함수를 호출 할 때 반드시 전달해야 한다 : 전달하지 않을 경우 UndefKeywordError 예외가 발생한다.
function f(x; y)
###
end
f(3, y=5) # ok, y is assigned
f(3) # throws UndefKeywordError(:y)
세미 콜론 다음에 key => value
표현식으로 전달 할 수 있다. 예를 들어 plot(x, y; :width => 2)
는 plot(x, y, width=2)
와 동등하다. 이런 것은 키워드의 이름이 런타임에 계산될 때 유용하다.
세미콜론 뒤에 베어 식별자(bare identifier, ??) 또는 도트 표현식이 나오는 경우 키워드 인수 이름은 식별자 또는 필드 이름에 의해 암시된다. 예를 들어, plot(x, y; width)
는 plot(x, y; width=width)
과 동일하고 plot(x, y; options.width)
는 plot(x, y; width=options.width)
와 동일하다.
키워드 인자는 본질상 같은 인자를 여러번 특정 할 수 있게 한다. 예를 들면 plot(x, y; options..., width=2)
함수 호출에서 option
키워드 구조가 width
에 대한 값을 포함 할 수 있다. 이 경우 가장 오른쪽에 위치한 값이 우선권을 가진다; 이 경우 width
는 2
라는 값을 갖는다. 하지만 명시적으로 같은 키워드를 여러번 특정하는 경우, 예를 들면 plot(x, y, width=2, width=3)
는 허용되지 않으며 syntax error 가 발생한다.
기본값이 주어진 키워드 인자의 기본값 표현식이 계산될 때, 오직 이전의 인자만 범위에 포함된다. 예를 들어, 다음과 같은 정의를 생각하자.
function f(x, a=b, b=1)
###
end
a=b
의 b
는 외부영역의 b
를 참조하지, 그 다음에 나오는 인자 b
를 참조하지 않는다.[^3]
Do
블록 문법함수를 다른 함수의 인자로 전달하는 것은 강력한 기법이지만 문법이 항상 편리하지는 않다. 함수 인자에 여러줄이 필요할 때 이런 함수에 대한 호출을 코딩하는 것은 어색하다. 예를 들어 다수의 분기를 가진 함수에 대한 map
을 호출한다고 하자.
map(x->begin
if x < 0 && iseven(x)
return 0
elseif x == 0
return 1
else
return x
end
end,
[A, B, C])
Julia 에는 이런 함수를 좀 더 명료하게 재작성하기 위해 do
라는 단어를 예약하였다.
map([A, B, C]) do x
if x < 0 && iseven(x)
return 0
elseif x == 0
return 1
else
return x
end
end
do x
문법은 x
를 인자로 받는 익명 함수를 만들며, 그것을 map
의 첫번째 인자로 전달한다. 유사하게, do a, b
는 두개의 인자를 받는 익명함수를 만든다. do (a, b)
는 하나의 인자를 받는 익명함수를 만들지만, 그 인자는 탈구조화(destructured) 되는 튜플라는 것에 유의하라. 단순한 do
는 그 다음에 오는 것들이 ()-> ...
꼴의 익명함수임을 선언한다.
이 인자들이 "외부의" 함수에 따라 어떻게 초기화 되는가: map
은 차례로 x
를 A
, B
, C
로 정하여 각각에 대한 익명 함수를 호출하는데, 이것은 map(func, [A, B, C])
문법에서 일어나는 것과 같다.
이 문법은 함수 호출이 일반적인 코드블럭과 비슷하게 보이기 때문에, 함수의 사용을 쉽게 만들어 언어를 효율적으로 확장시킨다. map
이외에도 시스템의 상태를 관리 하는 등 다양하게 사용된다. 예를 들어 open
에 사용되는 버젼은 파일이 열린 후 확실이 닫히게(closed) 보장한다.
open("outfile", "w") do io
write(io, data)
end
이것은 다음과 같이 정의된 함수로 완성된다.
function open(f::Function, args...)
io = open(args...)
try
f(io)
finally
close(io)
end
end
여기서는 open
은 우선 쓰기 위해 파일을 열고, 그 결과로 나오는 출력 스트림(output stream) 을 do ... end
블록에 정의된 익명함수로 전달한다. 당신의 함수가 종료된 후, open
은 당신의 함수가 정상적으로 종료되었는지, 아니면 예외를 발생시켰는지에 상관 없이 스트림이 제대로 종료됬는지를 확인한다. (try/finally
구조는 제어 흐름(Control Flow) 에 설명되었다.)
do
블록 구문을 사용하면서 문서나 구현을 확인하여 사용자 함수의 인수가 어떻게 초기화되는지 아는 것은 도움이 된다.
다른 어떤 내부 함수와 마찬가지로 do
블록은 그것을 둘러싼 영역으로부터 변수들을 끌어 낼 수 있다. 예를 들어 위의 보기에서 open...do
안의 data
변수는 외부 영역에서 끌려온 것이다. 끌려온 변수들은 performance tips 에서 논의되었듯이 성능에 영향을 줄 수 있다.
Julia 의 함수는 합성과 파이핑(piping, 혹은 chainging) 을 통해 하나로 결합 될 수 있다.
함수들을 결합하여 결과를 인자로 전달하는 것을 함수 합성이라 한다. 당신은 함수 합셩 연산자 (∘
) 를 사용하여 합성 할 수 있는데, (f ∘ g)(args...)
는 f(g(args...))
와 같다.
REPL 이자 잘 구성된 편집기에서 circ<tab>
으로 합셩 연산자를 입력 할 수 있다.
예를 들어 sqrt
와 +
함수들을 다음과 같이 합성 할 수 있다.
julia> (sqrt ∘ +)(3, 6)
3.0
이것은 우선 숫자들을 더하고, 그 결과의 제곱근을 구한다.
다음 예는 세 함수를 함성하여 문자열에 대해 매핑한다.
julia> map(first ∘ reverse ∘ uppercase, split("you can compose functions like this"))
6-element Vector{Char}:
'U': ASCII/Unicode U+0055 (category Lu: Letter, uppercase)
'N': ASCII/Unicode U+004E (category Lu: Letter, uppercase)
'E': ASCII/Unicode U+0045 (category Lu: Letter, uppercase)
'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)
'E': ASCII/Unicode U+0045 (category Lu: Letter, uppercase)
'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)
함수 파이핑은 이전 함수의 결과를 함수의 적용할 때 사용한다.
julia> 1:10 |> sum |> sqrt
7.416198487095663
여기서sms sum
에 의해 계산된 합이 sqrt
에 전달된다. 동일한 합성은 다음과 같다:
julia> (sqrt ∘ sum)(1:10)
7.416198487095663
파이프 연산자 |>
는 .|>
처럼 bradcasting 과 사용 될 수 있어서 체이닝과 도트 벡터화 구문이 유용하게 합셩될 수 있다.(아래를 보라)
julia> ["a", "list", "of", "strings"] .|> [uppercase, reverse, titlecase, length]
4-element Vector{Any}:
"A"
"tsil"
"Of"
7
파이프와 익명 함수를 결합할 경우, 만약 뒤의 파이프가 익명 함수의 몸체의 일부로 파싱되지 않으려면 소괄호로 묶어야 한다. 비교해 보자 :
julia> 1:3 .|> (x -> x^2) |> sum |> sqrt
3.7416573867739413
julia> 1:3 .|> x -> x^2 |> sum |> sqrt
3-element Vector{Float64}:
1.0
2.0
3.0
[^4] 후자는 1:3 .|> x -> (x^2 |> sum |> sqrt)
와 같다.
기술적인 계산 언어(technical-computing laguage) 에서, 함수에 대한 "벡터화된(vectorized)" 버전을 갖는 것은 일반적이다. 이 경우 주어진 함수 f(x)
와 배열 A
에 대해 f(A)
는 각각의 A
의 원소에 대해 f
를 적용한 값을 포함하는 배열이다. 이런 종류의 문법은 데이터 처리에 유용하지만, 다른 연어에서 벡터화는 성능을 위해 요구된다 : 루프가 느릴 경우, 벡터화된 버젼의 함수는 저수준 언어(low-level langruage)로 작성된 빠른 함수를 호출한다. Julia 는 성능을 위한 벡터화된 함수를 필요로 하지 않으며, 많은 경우 루프를 사용하는 것이 낫지만 (Performance Tips 를 참고하라), 벡터화된 버젼이 편리할 때가 있다. 따라서 어떤 줄리아 함수 f
라도 f.(A)
문법을 이용하여 배열(혹은 아무 컬렉션이든)에 대한 원소별 계산을 하는데 이용 될 수 있다. 예를 들어, sin
은 벡터 A
의 모든 원소에 다음과 같이 적용된다:
julia> A = [1.0, 2.0, 3.0]
3-element Vector{Float64}:
1.0
2.0
3.0
julia> sin.(A)
3-element Vector{Float64}:
0.8414709848078965
0.9092974268256817
0.1411200080598672
물론 당신이 f
를 f(A::AbstractArray) = map(f, A)
와 같이 특별한 벡터 메서드로 작성한다면 이것은 f.(A)
만큼이나 효율적일 것이다. f.(A)
문법의 장점은 어떤 함수가 앞으로 벡터화될지 여부를 라이브러리를 작성하는 사람이 결정하지 않아도 된다는 것이다.
더 일반적으로는, f.(args...)
는 실제로 broadcast(f, agrs...)
와 동등하며, 따라서 (각각 다른 모양을 가졌더라도) 다수의 배열에 대해 연산을 할수도 있으며, 스칼라와 배열의 혼합에 대해서도 (Broadcasting 을 참고하라) 연산을 수행 할 수 있다. 예를 들어, f(x,y) = 3x + 4y
, 라면 f.(pi,A)
는 A
에 포함된 각각의 a
에 대한 f(pi, a)
로 구성된 새로운 배열을 반환할 것이며 f.(vector1, vector2)
는 각 인덱스 i
에 대해 f(vector1[i], vector2[i])
로 이루어진 새로운 벡터를 리텅할 것이다(만약 두 벡터의 길이가 다르면 예외를 던질 것이다).
julia> f(x,y) = 3x + 4y;
julia> A = [1.0, 2.0, 3.0];
julia> B = [4.0, 5.0, 6.0];
julia> f.(pi, A)
3-element Vector{Float64}:
13.42477796076938
17.42477796076938
21.42477796076938
julia> f.(A, B)
3-element Vector{Float64}:
19.0
26.0
33.0
키워드 인자는 브로드케스트 되지 않으며 단지 함수 호출 각각에 대해 전달된다. 예를 들어, round.(x, digits =3)
은 broadcast(x->round(x, digits=3), x)
와 동등하다.
또한 중첩된 f.(args...)
호출은 단일 브로드캐스트 루프로 융합된다. 예를 들어, sin.(cos.(X))
는 broadcast(x -> sin(cos(x)), X)
와 동일하며 [sin(cos(x)) for x in X]
와 유사하다 :X
에 대한 단일 루프만이 존재하며 그 결과에 대해 단일 배열이 할당된다. [반대로, 일반적인 "벡터화된" 언어의 sin(cos(X))
는 먼저 tmp=cos(X)
에 대해 하나의 임시 배열을 할당한 다음 별도의 루프에서 sin(tmp)
를 계산하여 두 번째 배열을 할당한다.] 이 루프 융합은 발생하거나 발생하지 않을 수 있는 컴파일러 최적화가 아니며 중첩된 f.(args...)
호출이 발생할 때마다 구문론적으로 보장되는 것이다(syntactic guarantee). 기술적으로는 "도트 함수가 아닌" 함수 호출이 발생하는 즉시 융합이 중지된다. 예를 들어, sin.(sort(cos.(X)))
에서 sin
및 cos
루프는 중간의 sort
함수 때문에 병합될 수 없다.
마지막으로, 벡터화된 연산의 출력 배열이 사전 할당되면(pre-allocated) 반복된 호출이 결과에 대해 새 배열을 반복해서 할당하지 않기 때문에 (출력의 사전 할당(Pre-allocating outputs) 참조) 일반적으로 가장 효율적이다. 따라서 이에 대한 편리한 구문은 X .= ...
이며, broadcast!(identity, X, ...)
와의 차이점은 broadcast!
루프는 중첩된 "도트" 호출과 융합됩된다는 것 뿐이다. 예를 들어, X .= sin.(Y)
는 broadcast!(sin, X, Y)
와 동일하며 X
를 제자리에서 sin.(Y)
로 덮어쓴다. 왼쪽이 배열 인덱싱 표현식인 경우, 예를 들어 X[begin+1:end] .= sin.(Y)
, 그러면 view
에 대한 broadcast!
로 즉 broadcast!(sin, view(X, firstindex(X)+1:lastindex(X)), Y)
로 번역되서, 왼쪽이 제자리에서 업데이트되도록 한다
표현식에서 많은 연산과 함수 호출에 .
를 추가하는 것은 지루할 수도 있고 가독성이 떨어질 수도 있기 때문에 표현식의 모든 함수 호출, 작업 및 할당을 "도트처리된" 버전으로 변환하기 위해 @.
매크로가 제공됩니다.
julia> Y = [1.0, 2.0, 3.0, 4.0];
julia> X = similar(Y); # pre-allocate output array
julia> @. X = sin(cos(Y)) # equivalent to X .= sin.(cos.(Y))
4-element Vector{Float64}:
0.5143952585235492
-0.4042391538522658
-0.8360218615377305
-0.6080830096407656
.
나 +
와 같은 이진(또는 단항) 연산자는 동일한 메커니즘으로 처리된다: broadcast
호출과 동일하며 다른 중첩된 "도트" 호출과 융합됩니다. X .+= Y
등은 X .= X .+ Y
와 동일하며 융합된 in-place 할당이 된다; 도트 연산자(dot operators)를 참고하라.
아래 예와 같이 |>
를 사용하여 도트 연산과 함수 연결을 결합할 수도 있습니다.
julia> [1:5;] .|> [x->x^2, inv, x->2*x, -, isodd]
5-element Vector{Real}:
1
0.5
6
-4
true
여기서 우리는 이것이 함수를 정의하는 완전한 설명과는 거리가 멀다는 것을 언급해야 한다. Julia는 정교한 타입 시스템을 가지고 있으며 인자 유형에 대한 다중 디스패치(multiple dispatch)를 허용한다. 여기에 제시된 예제는 인수에 대한 타입 지정을 제시하지 않는다. 즉, 모든 타입의 인수에 적용할 수 있다. 타입 시스템은 타입(Types) 에 설명되어 있고 런타임에서의 인자 타입에 대한 다중 디스패치에 의해 선택된 메서드의 관점에서 함수를 정의하는 방법은 [메서드(Method)][(](https://docs.julialang.org/en/v1/manual/functions/#Function-composition-and-piping:~:text=is%20described%20in-,Methods,-.))에 설명되어 있다.
역자주
[^1] : 2+3
과 같이 연산자를 피연산자 사이에 삽입하여 연산하는것.
[^2] : 함수의 시그니쳐는 함수의 정의나 선언 부분에서 이름, 인자, 리턴값의 타입이 나타나는 부분이다. Julia 에서
function add2(a::Float64, b::Float64)::Float64
return a+b
end
와 같이 함수 add2
가 정의되었을 때 function add2(a::Float64, b::Float64)::Float64
를 시그니처라고 한다.
[^3] : 다음을 보자.
julia> b
ERROR: UndefVarError: b not defined
julia> function f(x, a=b, b=1)
println(a)
end
f (generic function with 3 methods)
julia> f(3)
ERROR: UndefVarError: b not defined
Stacktrace:
[1] f(x::Int64)
@ Main ./REPL[45]:2
[2] top-level scope
@ REPL[46]:1
julia> b=4
4
julia> f(3)
4
[^4] 후자는 1:3 .|> x -> (x^2 |> sum |> sqrt)
와 같다.