제어 흐름
제어 흐름 (Control Flow)
줄리아는 다양한 제어 흐름(construct)을 제공해요.
- 복합 표현(Compound Expressions):
begin과; - 조건 평가(Conditional Evaluation):
if-elseif-else와?:(삼항 연산자) - 단락 평가(Short-Circuit Evaluation): 논리 연산자
&&("그리고")와||("또는"), 그리고 연결된 비교 - 반복 평가(Repeated Evaluation): 루프
while과for - 예외 처리(Exception Handling):
try-catch,error와throw - 태스크(Tasks, 일명 코루틴 Coroutines):
yieldto
앞의 다섯 가지 제어 흐름 메커니즘은 고수준 프로그래밍 언어의 표준이에요. 태스크는 그렇지 않죠. 태스크는 비지역적(non-local) 제어 흐름을 제공해서, 일시적으로 중단된 계산들 사이를 전환할 수 있게 해 줘요. 이건 아주 강력한 도구예요. 예외 처리와 협력적 멀티태스킹(cooperative multitasking)이 모두 태스크로 구현되어 있거든요. 일상적인 프로그래밍에서 태스크를 직접 쓸 필요는 없지만, 어떤 문제는 태스크를 쓰면 훨씬 쉽게 해결되기도 해요.
본문
복합 표현 (Compound Expressions)
여러 개의 하위 표현식을 순서대로 평가하고 마지막 하위 표현식의 값을 반환하는 단일 표현식이 있으면 편리할 때가 있어요. 줄리아에는 이를 해결하는 두 가지 표현식이 있는데, begin 블록과 ; 체인(chain)이에요. 두 복합 표현식 구조 모두 값은 마지막 하위 표현식의 값이에요. begin 블록의 예시를 볼게요.
julia> z = begin
x = 1
y = 2
x + y
end
3
이런 것들은 아주 작고 단순한 표현식이라 한 줄에 넣을 수도 있는데, 그럴 때 ; 체인 문법이 유용해져요.
julia> z = (x = 1; y = 2; x + y)
3
이 문법은 함수(Functions)에서 소개하는 간결한 한 줄 함수 정의 형태와 함께 쓸 때 특히 유용해요. begin 블록이 꼭 여러 줄이어야 한다거나 ; 체인이 꼭 한 줄이어야 한다는 요구는 없어요. 그게 일반적일 뿐이죠.
julia> begin x = 1; y = 2; x + y end
3
julia> (x = 1;
y = 2;
x + y)
3
조건 평가 (Conditional Evaluation)
조건 평가는 불리언(boolean) 표현식의 값에 따라 코드 일부를 평가하거나 평가하지 않게 해 줘요. if-elseif-else 조건 문법의 구조는 다음과 같아요.
if x < y
println("x is less than y")
elseif x > y
println("x is greater than y")
else
println("x is equal to y")
end
조건 표현식인 x < y가 참이면 해당 블록이 평가돼요. 아니면 조건 표현식 x > y가 평가되고, 그것이 참이면 해당 블록이 평가돼요. 둘 다 참이 아니면 else 블록이 평가됩니다. 실제 동작을 볼게요.
julia> function test(x, y)
if x < y
println("x is less than y")
elseif x > y
println("x is greater than y")
else
println("x is equal to y")
end
end
test (generic function with 1 method)
julia> test(1, 2)
x is less than y
julia> test(2, 1)
x is greater than y
julia> test(1, 1)
x is equal to y
elseif와 else 블록은 선택적이고, elseif 블록은 원하는 만큼 많이 쓸 수 있어요. if-elseif-else 구조의 조건 표현식은 그중 첫 번째가 참으로 평가될 때까지 순서대로 평가되고, 그 후에 그에 딸린 블록이 평가되며 더 이상의 조건 표현식이나 블록은 평가되지 않아요.
if 블록은 "누수성(leaky)"이 있어요. 즉, 지역 스코프(local scope)를 도입하지 않는다는 뜻이에요. 다시 말해 if 절 안에서 정의한 새 변수는 이전에 정의되지 않았더라도 if 블록 밖에서 사용할 수 있죠. 그래서 위의 test 함수를 다음과 같이도 정의할 수 있어요.
julia> function test(x,y)
if x < y
relation = "less than"
elseif x == y
relation = "equal to"
else
relation = "greater than"
end
println("x is ", relation, " y.")
end
test (generic function with 1 method)
julia> test(2, 1)
x is greater than y.
변수 relation은 if 블록 안에서 선언됐지만 밖에서 사용됐어요. 다만 이런 동작에 기댈 때는 모든 가능한 코드 경로에서 변수에 값을 정의하는지 반드시 확인해야 해요. 위 함수를 다음과 같이 바꾸면 런타임 오류가 발생합니다.
julia> function test(x,y)
if x < y
relation = "less than"
elseif x == y
relation = "equal to"
end
println("x is ", relation, " y.")
end
test (generic function with 1 method)
julia> test(1,2)
x is less than y.
julia> test(2,1)
ERROR: UndefVarError: `relation` not defined in local scope
Stacktrace:
[1] test(::Int64, ::Int64) at ./none:7
if 블록은 값도 반환해요. 다른 많은 언어에서 온 사용자들에게는 직관적이지 않게 보일 수도 있지만요. 이 값은 선택된 분기에서 마지막으로 실행된 문장의 반환값일 뿐이에요. 그래서
julia> x = 3
3
julia> if x > 0
"positive!"
else
"negative..."
end
"positive!"
매우 짧은 조건문(한 줄짜리)은 줄리아에서는 흔히 단락 평가(Short-Circuit Evaluation)로 표현된다는 점을 기억해 두세요. 다음 절에서 다룰게요.
C, MATLAB, Perl, Python, Ruby와 달리 — Java나 몇몇 더 엄격한 정적 타입 언어처럼 — 조건 표현식의 값이 true나 false가 아닌 것은 오류예요.
julia> if 1
println("true")
end
ERROR: TypeError: non-boolean (Int64) used in boolean context
이 오류는 조건이 잘못된 타입이었음을 알려줘요. 요구되는 Bool 대신 Int64였다는 뜻이죠.
소위 "삼항 연산자(ternary operator)"인 ?:는 if-elseif-else 문법과 밀접하게 연관돼 있지만, 긴 코드 블록의 조건부 실행이 아니라 단일 표현식 값 사이의 조건부 선택이 필요할 때 써요. 대부분의 언어에서 피연산자를 세 개나 받는 유일한 연산자라서 그런 이름이 붙었어요.
a ? b : c
? 앞의 표현식 a는 조건 표현식이에요. 삼항 연산은 조건 a가 참이면 : 앞의 표현식 b를 평가하고, 거짓이면 : 뒤의 표현식 c를 평가해요. ?와 : 주변의 공백은 필수라는 점에 주의하세요. a?b:c 같은 표현식은 유효한 삼항 표현식이 아니에요. (?와 : 뒤에 개행(newline)은 허용돼요.)
이 동작을 이해하는 가장 쉬운 방법은 예시를 보는 거예요. 아까 예시에서 println 호출은 세 분기 모두가 공유하고, 실제 선택지는 어느 리터럴 문자열을 출력할지뿐이었어요. 이건 삼항 연산자로 더 간결하게 쓸 수 있어요. 명확하게 하기 위해 먼저 두 갈래 버전을 시도해 볼게요.
julia> x = 1; y = 2;
julia> println(x < y ? "less than" : "not less than")
less than
julia> x = 1; y = 0;
julia> println(x < y ? "less than" : "not less than")
not less than
표현식 x < y가 참이면 삼항 연산자 표현식 전체가 문자열 "less than"으로 평가되고, 아니면 "not less than"으로 평가돼요. 원래의 세 갈래 예시는 삼항 연산자를 여러 번 연결(chaining)해야 해요.
julia> test(x, y) = println(x < y ? "x is less than y" :
x > y ? "x is greater than y" : "x is equal to y")
test (generic function with 1 method)
julia> test(1, 2)
x is less than y
julia> test(2, 1)
x is greater than y
julia> test(1, 1)
x is equal to y
연결을 쉽게 하기 위해, 이 연산자는 오른쪽에서 왼쪽으로 결합합니다.
if-elseif-else와 마찬가지로 : 앞뒤의 표현식은 조건 표현식이 각각 true 또는 false로 평가될 때만 평가된다는 점이 중요해요.
julia> v(x) = (println(x); x)
v (generic function with 1 method)
julia> 1 < 2 ? v("yes") : v("no")
yes
"yes"
julia> 1 > 2 ? v("yes") : v("no")
no
"no"
단락 평가 (Short-Circuit Evaluation)
줄리아의 &&와 || 연산자는 각각 논리 "그리고(and)"와 "또는(or)" 연산에 대응하며, 주로 그 목적으로 사용돼요. 하지만 여기에 추가로 단락 평가(short-circuit evolution)라는 성질이 있어요. 아래에서 설명하듯 두 번째 인자를 반드시 평가하지 않을 수 있죠. (단락 동작 없이 논리 "그리고"와 "또는"으로 쓸 수 있는 비트 &와 | 연산자도 있지만, 평가 순서상 &와 |는 &&와 ||보다 우선순위가 높다는 점을 조심하세요.)
단락 평가는 조건 평가와 상당히 비슷해요. &&와 || 불리언 연산자를 가진 대부분의 명령형 프로그래밍 언어에서 볼 수 있는 동작이죠. 이 연산자들로 연결된 일련의 불리언 표현식에서, 전체 체인의 최종 불리언 값을 정하는 데 필요한 최소 개수의 표현식만 평가돼요. 어떤 언어(예: Python)는 이것을 and(&&)와 or(||)라고 부르죠. 명시적으로 말하면:
- 표현식
a && b에서 하위 표현식b는a가 참으로 평가될 때만 평가돼요. - 표현식
a || b에서 하위 표현식b는a가 거짓으로 평가될 때만 평가돼요.
이유는 a가 거짓이면 b의 값이 무엇이든 a && b는 거짓이어야 하고, 마찬가지로 a가 참이면 b의 값이 무엇이든 a || b는 참이어야 하기 때문이에요. &&와 ||는 모두 오른쪽 결합이지만, &&가 ||보다 우선순위가 높아요. 이 동작을 직접 실험해 보기 쉽습니다.
julia> t(x) = (println(x); true)
t (generic function with 1 method)
julia> f(x) = (println(x); false)
f (generic function with 1 method)
julia> t(1) && t(2)
1
2
true
julia> t(1) && f(2)
1
2
false
julia> f(1) && t(2)
1
false
julia> f(1) && f(2)
1
false
julia> t(1) || t(2)
1
true
julia> t(1) || f(2)
1
true
julia> f(1) || t(2)
1
2
true
julia> f(1) || f(2)
1
2
false
&&와 || 연산자의 다양한 조합의 결합성과 우선순위도 같은 방식으로 쉽게 실험할 수 있어요.
이 동작은 줄리아에서 아주 짧은 if 문의 대안을 만들 때 자주 쓰여요. if <cond> <statement> end 대신 <cond> && <statement>라고 쓸 수 있죠. (if ! <cond> <statement> end 대신 <cond> || <statement>라고 쓸 수 있어요. (
예를 들어, 재귀 팩토리얼 루틴을 이렇게 정의할 수 있어요.
julia> function fact(n::Int)
n >= 0 || error("n must be non-negative")
n == 0 && return 1
n * fact(n-1)
end
fact (generic function with 1 method)
julia> fact(5)
120
julia> fact(0)
1
julia> fact(-1)
ERROR: n must be non-negative
Stacktrace:
[1] error at ./error.jl:33 [inlined]
[2] fact(::Int64) at ./none:2
[3] top-level scope
단락 동작 없는 불리언 연산은 수학 연산과 기초 함수(Mathematical Operations and Elementary Functions)에서 소개한 비트 연산자 &와 |로 수행할 수 있어요. 이것들은 보통 함수인데, 마침 중위 연산자 문법을 지원하고 항상 자신의 인자를 평가합니다.
julia> f(1) & t(2)
1
2
false
julia> t(1) | t(2)
1
2
true
if, elseif 또는 삼항 연산자에 쓰는 조건 표현식처럼, &&나 ||의 피연산자는 불리언 값(true 또는 false)이어야 해요. 조건 체인에서 마지막 항목을 제외한 어디에든 비불리언 값을 쓰는 것은 오류예요.
julia> 1 && true
ERROR: TypeError: non-boolean (Int64) used in boolean context
반면 조건 체인의 끝에는 어떤 타입의 표현식이든 쓸 수 있어요. 앞선 조건들에 따라 평가되어 반환됩니다.
julia> true && (x = (1, 2, 3))
(1, 2, 3)
julia> false && (x = (1, 2, 3))
false
반복 평가: 루프 (Repeated Evaluation: Loops)
표현식을 반복 평가하는 구조는 두 가지가 있어요. while 루프와 for 루프죠. while 루프 예시를 볼게요.
julia> i = 1;
julia> while i <= 3
println(i)
global i += 1
end
1
2
3
while 루프는 조건 표현식(여기서는 i <= 3)을 평가하고, 그것이 참인 동안 루프 본문을 계속 평가해요. while 루프를 처음 만날 때 조건 표현식이 거짓이면 본문은 절대 평가되지 않습니다.
for 루프는 흔한 반복 평가 관용구를 더 쉽게 작성하게 해 줘요. 위의 while 루프처럼 위아래로 세는 일은 흔하니까, for 루프로 더 간결하게 표현할 수 있어요.
julia> for i = 1:3
println(i)
end
1
2
3
여기서 1:3은 범위(range) 객체로, 1, 2, 3이라는 수열을 나타내요. for 루프는 이 값들을 순회하면서 각각을 차례로 변수 i에 할당해요. 일반적으로 for 구조는 1:3이나 1:3:13(매 3번째 정수 1, 4, 7, …, 13을 나타내는 StepRange) 같은 범위에서부터 배열 같은 더 일반적인 컨테이너, 그리고 사용자 코드나 외부 패키지가 정의한 이터레이터까지 어떤 "반복 가능한(iterable)" 객체(또는 "컨테이너")든 순회할 수 있어요. 범위가 아닌 컨테이너의 경우 = 대신 in 또는 ∈ 키워드를 쓰는 게 일반적이지만 완전히 동등해요. 코드가 더 읽기 좋게 하려고 그러는 거죠.
julia> for i in [1,4,0]
println(i)
end
1
4
0
julia> for s ∈ ["foo","bar","baz"]
println(s)
end
foo
bar
baz
다양한 종류의 반복 가능한 컨테이너는 매뉴얼의 이후 절에서 소개하고 논의할게요 (예: 다차원 배열(Multi-dimensional Arrays)).
이전의 while 루프 형태와 for 루프 형태 사이의 꽤 중요한 차이 중 하나는 변수가 보이는 스코프(scope)예요. for 루프는 감싸는 스코프에 같은 이름의 변수가 이미 존재하든 말든, 항상 본문에 새로운 반복 변수를 도입해요. 즉 한편으로는 i를 루프 전에 선언할 필요가 없다는 뜻이고, 다른 한편으로는 루프 밖에서는 보이지 않으며, 밖의 같은 이름의 변수에도 영향을 주지 않는다는 뜻이에요. 이를 테스트하려면 새 대화형 세션 인스턴스나 다른 변수 이름이 필요해요.
julia> for j = 1:3
println(j)
end
1
2
3
julia> j
ERROR: UndefVarError: `j` not defined in `Main`
julia> j = 0;
julia> for j = 1:3
println(j)
end
1
2
3
julia> j
0
후자의 동작을 바꿔 기존 지역 변수를 재사용하려면 for outer를 사용하세요.
변수 스코프, outer, 그리고 줄리아에서의 동작에 대한 자세한 설명은 변수의 스코프(Scope of Variables)를 참고하세요.
테스트 조건이 거짓이 되기 전에 while 반복을 끝내거나, 반복 가능한 객체의 끝에 도달하기 전에 for 루프 순회를 멈추는 것이 편리할 때가 있어요. 이는 break 키워드로 할 수 있어요.
julia> i = 1;
julia> while true
println(i)
if i >= 3
break
end
global i += 1
end
1
2
3
julia> for j = 1:1000
println(j)
if j >= 3
break
end
end
1
2
3
break 키워드가 없으면 위의 while 루프는 스스로 종료되지 않고, for 루프는 1000까지 순회할 거예요. 두 루프 모두 break 덕분에 일찍 벗어났어요.
다른 상황에서는 한 번의 반복을 멈추고 즉시 다음 반복으로 넘어가는 게 편리할 때가 있어요. continue 키워드가 이걸 해 줘요.
julia> for i = 1:10
if i % 3 != 0
continue
end
println(i)
end
3
6
9
이 예시는 다소 꾸민 예시인데, 조건을 부정하고 println 호출을 if 블록 안에 넣으면 같은 동작을 더 명확하게 만들 수 있으니까요. 실사용에서는 continue 뒤에 평가할 코드가 더 많고, 여러 지점에서 continue를 호출하는 경우가 많아요.
여러 개의 중첩된 for 루프를 하나의 바깥 루프로 합쳐서, 그 반복 가능한 것들의 데카르트 곱(cartesian product)을 형성할 수 있어요.
julia> for i = 1:2, j = 3:4
println((i, j))
end
(1, 3)
(1, 4)
(2, 3)
(2, 4)
이 문법에서 반복 가능한 것들은 여전히 바깥 루프 변수를 참조할 수 있어요. 예를 들어 for i = 1:n, j = 1:i는 유효합니다. 하지만 이런 루프 안의 break 문은 안쪽 루프뿐만 아니라 루프 전체 중첩을 빠져나가요. 내부 루프가 실행될 때마다 두 변수(i와 j) 모두 현재 반복 값으로 설정돼요. 따라서 i에 대한 할당은 이후 반복에 보이지 않습니다.
julia> for i = 1:2, j = 3:4
println((i, j))
i = 0
end
(1, 3)
(1, 4)
(2, 3)
(2, 4)
이 예시를 각 변수에 대해 for 키워드를 쓰도록 다시 작성하면 출력이 달라져서 두 번째와 네 번째 값에 0이 들어갈 거예요.
zip을 쓰면 단일 for 루프에서 여러 컨테이너를 동시에 순회할 수 있어요.
julia> for (j, k) in zip([1 2 3], [4 5 6 7])
println((j,k))
end
(1, 4)
(2, 5)
(3, 6)
zip을 쓰면 전달된 컨테이너들의 하위 이터레이터를 담은 튜플인 이터레이터가 만들어져요. zip 이터레이터는 모든 하위 이터레이터를 순서대로 순회하며, for 루프의 i번째 반복에서 각 하위 이터레이터의 i번째 요소를 선택해요. 하위 이터레이터 중 하나라도 소진되면 for 루프는 멈춰요.
예외 처리 (Exception Handling)
예상치 못한 조건이 발생하면 함수는 호출자에게 합리적인 값을 돌려주지 못할 수 있어요. 이런 경우 예외적인 조건을 진단 오류 메시지를 출력하면서 프로그램을 종료하는 게 최선일 수도 있고, 프로그래머가 그런 예외 상황을 처리하는 코드를 제공했다면 그 코드가 적절한 조치를 취하게 하는 게 최선일 수도 있어요.
내장 예외 (Built-in Exceptions)
예상치 못한 조건이 발생하면 예외(exception)가 던져져요. 아래에 나열된 내장 예외는 모두 정상적인 제어 흐름을 방해합니다.
| 예외 | |
|---|---|
ArgumentError |
인자가 잘못된 값을 가지는 경우 |
BoundsError |
인덱스가 배열의 범위를 벗어난 경우 |
CompositeException |
요청된 작업이 여러 하위 작업의 예외로 인해 실패한 경우 |
DimensionMismatch |
객체의 치수가 일치하지 않는 경우 |
DivideError |
정수 나눗셈에서 0으로 나눈 경우 |
DomainError |
인자가 함수의 정의역을 벗어난 경우 |
EOFError |
파일의 끝에 도달했는데 더 읽기를 원한 경우 |
ErrorException |
특정한 오류 조건에 대한 일반 오류 메시지. throw(ErrorException("<메시지>"))가 error("<메시지>")의 축약이에요. |
InexactError |
타입 변환이 정확하게 수행될 수 없는 경우 |
InitError |
모듈 초기화 중 런타임 오류가 발생한 경우 |
InterruptException |
터미널에서 Ctrl-C(또는 그와 동등한 수단)로 프로세스를 중단시킨 경우 |
InvalidStateException |
함수가 잘못된 상태의 객체에서 호출된 경우 |
KeyError |
딕셔너리(Dictionary)에서 키를 찾을 수 없는 경우 |
LoadError |
파일을 include하거나 eval할 때 발생한 오류 |
MethodError |
일치하는 메서드가 없는 경우 |
MissingException |
missing 값을 사용할 수 없는 경우 |
OutOfMemoryError |
메모리를 할당할 수 없는 경우 |
ReadOnlyMemoryError |
메모리 읽기 전용 위치에 쓰기를 시도한 경우 |
RemoteException |
원격 작업의 예외 포착 |
ProcessFailedException |
자식 프로세스가 실패한 경우 |
OverflowError |
정수 연산의 결과가 너무 크거나 작은 경우 |
StackOverflowError |
스택이 넘친 경우 |
SystemError |
시스템 호출이 실패한 경우 |
TypeError |
타입 주장(assertion)이 실패한 경우. convert가 T(x) 형태로 호출되었지만 실패한 경우에도 발생 |
UndefRefError |
정의되지 않은 객체의 항목이나 필드에 접근한 경우 |
UndefVarError |
기호가 현재 스코프에 정의되지 않은 경우 |
UnicodeError |
유니코드 문자열의 인덱싱/반복 실패 |
예를 들어 sqrt 함수는 음의 실수에 적용하면 DomainError를 던져요.
julia> sqrt(-1)
ERROR: DomainError with -1.0:
sqrt was called with a negative real argument but will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
[...]
자신만의 예외를 다음과 같이 정의할 수 있어요.
julia> struct MyCustomException <: Exception end
throw 함수
예외는 throw로 명시적으로 만들 수 있어요. 예를 들어 음수가 아닌 수에 대해서만 정의된 함수는 인자가 음수면 DomainError를 던지도록 작성할 수 있어요.
julia> f(x) = x>=0 ? exp(-x) : throw(DomainError(x, "argument must be non-negative"))
f (generic function with 1 method)
julia> f(1)
0.36787944117144233
julia> f(-1)
ERROR: DomainError with -1:
argument must be non-negative
Stacktrace:
[1] f(::Int64) at ./none:1
괄호 없는 DomainError는 예외가 아니라 예외의 타입이라는 점에 주의하세요. Exception 객체를 얻으려면 호출해야 해요.
julia> typeof(DomainError(nothing)) <: Exception
true
julia> typeof(DomainError) <: Exception
false
추가로, 일부 예외 타입은 오류 보고에 쓰이는 인자를 하나 이상 받아요.
julia> throw(UndefVarError(:x))
ERROR: UndefVarError: `x` not defined
이 메커니즘은 UndefVarError가 작성된 방식을 따라 커스텀 예외 타입으로 쉽게 구현할 수 있어요.
julia> struct MyUndefVarError <: Exception
var::Symbol
end
julia> Base.showerror(io::IO, e::MyUndefVarError) = print(io, e.var, " not defined")
예를 들어,
size(A) == size(B) || throw(DimensionMismatch("size of A not equal to size of B"))
보다
size(A) == size(B) || throw(DimensionMismatch("Size of A not equal to size of B"))
을 선호합니다. 다만 때로는 첫 글자를 대문자로 유지하는 게 말이 될 때도 있어요. 예를 들어 함수의 인자가 대문자일 때처럼요.
size(A,1) == size(B,2) || throw(DimensionMismatch("A has first dimension..."))
Errors
error 함수는 정상적인 제어 흐름을 방해하는 ErrorException을 생성하는 데 쓰여요.
음수의 제곱근을 취하는 순간 실행을 즉시 멈추고 싶다고 가정해 볼게요. 이렇게 하려면 인자가 음수일 때 오류를 일으키는 까다로운 sqrt 버전을 정의할 수 있어요.
julia> fussy_sqrt(x) = x >= 0 ? sqrt(x) : error("negative x not allowed")
fussy_sqrt (generic function with 1 method)
julia> fussy_sqrt(2)
1.4142135623730951
julia> fussy_sqrt(-1)
ERROR: negative x not allowed
Stacktrace:
[1] error at ./error.jl:33 [inlined]
[2] fussy_sqrt(::Int64) at ./none:1
[3] top-level scope
fussy_sqrt가 다른 함수에서 음수 값으로 호출되면, 호출 함수의 실행을 계속하려고 시도하는 대신 즉시 반환하고 대화형 세션에 오류 메시지를 표시해요.
julia> function verbose_fussy_sqrt(x)
println("before fussy_sqrt")
r = fussy_sqrt(x)
println("after fussy_sqrt")
return r
end
verbose_fussy_sqrt (generic function with 1 method)
julia> verbose_fussy_sqrt(2)
before fussy_sqrt
after fussy_sqrt
1.4142135623730951
julia> verbose_fussy_sqrt(-1)
before fussy_sqrt
ERROR: negative x not allowed
Stacktrace:
[1] error at ./error.jl:33 [inlined]
[2] fussy_sqrt at ./none:1 [inlined]
[3] verbose_fussy_sqrt(::Int64) at ./none:3
[4] top-level scope
try/catch 문
try/catch 문은 예외를 검사하고, 평소라면 애플리케이션을 망가뜨릴 수 있는 일을 우아하게 처리할 수 있게 해 줘요. 예를 들어 아래 코드에서 제곱근 함수는 평소라면 예외를 던져요. 그 주위에 try/catch 블록을 두면 여기서 이를 완화할 수 있어요. 이 예외를 어떻게 처리할지는 직접 선택하면 돼요. 로그를 남기거나, 대체 값을 반환하거나, 아래 예시처럼 문장만 출력하는 방식이 있죠. 예상치 못한 상황을 어떻게 처리할지 정할 때 고려할 한 가지는 try/catch 블록을 쓰는 것이 조건 분기로 처리하는 것보다 훨씬 느리다는 점이에요. 아래에는 try/catch 블록으로 예외를 처리하는 예를 더 보여줄게요.
julia> try
sqrt("ten")
catch e
println("You should have entered a numeric value")
end
You should have entered a numeric value
try/catch 문은 예외를 변수에 저장할 수도 있게 해 줘요. 아래의 꾸민 예시는 x가 인덱싱 가능하면 x의 두 번째 요소의 제곱근을 계산하고, 아니면 x를 실수로 가정하고 그 제곱근을 반환해요.
julia> sqrt_second(x) = try
sqrt(x[2])
catch y
if isa(y, DomainError)
sqrt(complex(x[2], 0))
elseif isa(y, BoundsError)
sqrt(x)
else
rethrow() # ensure other exceptions can bubble up the call stack
end
end
sqrt_second (generic function with 1 method)
julia> sqrt_second([1 4])
2.0
julia> sqrt_second([1 -4])
0.0 + 2.0im
julia> sqrt_second(9)
3.0
julia> sqrt_second(-9)
ERROR: DomainError with -9.0:
sqrt was called with a negative real argument but will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
[...]
julia> sqrt_second([1 nothing])
ERROR: MethodError: no method matching sqrt(::Nothing)
The function `sqrt` exists, but no method is defined for this combination of argument types.
[...]
위처럼 rethrow를 사용해서 원래 예외로 스택을 계속 풀어내면 더 높은 수준의 예외 처리기가 그 예외를 다룰 수 있어요. 위처럼 예외 타입으로 필터링할 때는 다른 타입의 예외가 호출자에게 숨겨지지 않도록 else rethrow()를 포함하는 것이 종종 중요해요.
catch 다음의 기호는 항상 예외의 이름으로 해석된다는 점에 주의하세요. 그래서 try/catch 표현식을 한 줄로 쓸 때 주의가 필요해요. 다음 코드는 오류가 발생한 경우 x의 값을 반환하는 데 동작하지 않아요.
try bad() catch x end
대신 세미콜론을 쓰거나 catch 뒤에 줄바꿈을 넣으세요.
try bad() catch; x end
try bad()
catch
x
end
try/catch 구조의 힘은 깊게 중첩된 계산을 호출 함수 스택의 훨씬 더 높은 수준으로 즉시 풀어낼 수 있는 능력에 있어요. 오류가 발생하지 않았지만 스택을 풀고 더 높은 수준으로 값을 전달하는 것이 바람직한 상황도 있어요. 줄리아는 더 고급 오류 처리를 위해 backtrace, catch_backtrace, current_exceptions 함수를 제공합니다.
else 절 (else Clauses)
Julia 1.8 : 이 기능은 최소한 Julia 1.8이 필요해요.
어떤 경우에는 오류 상황을 적절히 처리할 뿐만 아니라, try 블록이 성공할 때만 어떤 코드를 실행하고 싶을 수 있어요. 이를 위해 catch 블록 뒤에 else 절을 지정할 수 있는데, 이전에 오류가 던져지지 않았을 때 실행돼요. 이 코드를 try 블록에 넣는 것보다 else 절에 넣는 이점은 추가 오류가 catch 절에 조용히 잡히지 않는다는 점이에요.
local x
try
x = read("file", String)
catch
# handle read errors
else
# do something with x
end
try, catch, else, finally 절은 각각 자신만의 스코프 블록을 도입하므로, 변수가 try 블록에서만 정의되면 else나 finally 절에서는 접근할 수 없어요.
julia> try
foo = 1
catch
else
foo
end
ERROR: UndefVarError: `foo` not defined in `Main`
Suggestion: check for spelling errors or missing imports.
바깥 스코프 어디에서든 변수에 접근할 수 있게 하려면 try 블록 밖에서 local 키워드를 사용하세요.
finally 절 (finally Clauses)
상태를 변경하거나 파일 같은 자원을 사용하는 코드에서는 코드가 끝날 때 정리 작업(파일 닫기 같은)이 필요한 경우가 일반적이에요. 예외는 코드 블록이 정상적인 끝에 도달하기 전에 빠져나갈 수 있으므로 이 작업을 잠재적으로 복잡하게 만들어요. finally 키워드는 주어진 코드 블록이 어떻게 빠져나가든 상관없이 블록이 빠져나갈 때 어떤 코드를 실행할 수단을 제공해요.
예를 들어 열린 파일을 닫는 것을 보장하는 방법은 이래요.
f = open("file")
try
# operate on file f
finally
close(f)
end
제어가 try 블록을 떠날 때(예를 들어 return 때문에, 또는 그냥 정상적으로 끝나서) close(f)가 실행돼요. try 블록이 예외 때문에 빠져나가면 그 예외는 계속 전파됩니다. catch 블록은 try와 finally와 결합할 수도 있어요. 이 경우 finally 블록은 catch가 오류를 처리한 뒤에 실행돼요.
try/catch/else/finally 표현식을 평가할 때 전체 표현식의 값은 finally 블록을 제외한 마지막으로 실행된 블록의 값이에요. 예를 들어:
julia> try
1
finally
2
end
1
julia> try
error("")
catch
1
else
2
finally
3
end
1
julia> try
0
catch
1
else
2
finally
3
end
2
태스크 (Tasks, aka Coroutines)
태스크는 계산을 유연하게 일시 중단하고 재개할 수 있게 해 주는 제어 흐름 기능이에요. 여기서는 완전성을 위해 간단히만 언급할게요. 자세한 논의는 비동기 프로그래밍(Asynchronous Programming)을 참고하세요.
더 알아보기 (Learn more)
- julia 공식 메뉴얼 — Control Flow (원문)
- Functions
- Variables and Scoping
- Asynchronous Programming
- Multi-dimensional Arrays
제어 흐름을 이해하는 핵심은 각 구조가 "언제 평가되고 언제 평가되지 않는지"를 파악하는 거예요. if 블록이 값도 반환하고 스코프를 새로 만들지 않는다는 점, &&/||가 단락 평가를 한다는 점, for 루프가 항상 새 반복 변수를 만든다는 점을 코드로 직접 확인해 보면 막연했던 부분이 정리될 거예요.