Julia는 대부분의 프로그래밍 작업에 쓸 수 있는 범용 언어예요. 그런데 실제로는 공학과 과학 분야에서 주로 쓰이는 편이에요. 빠르고 유연하며 정교한 수치 계산이 Julia 설계의 핵심이에요.
정수는 소수점이 없는 '딱 떨어지는' 수예요.
기본 개념에서 정숫값은 타입을 지정하지 않고도 변수에 할당할 수 있다는 걸 봤어요.
가독성을 위해 밑줄을 자릿수 구분자로 쓸 수 있어요. 컴파일러는 이 밑줄을 무시해요.
julia> x = 3
3
julia> typeof(x)
Int64
julia> large_number = 1_234_567_890
1234567890
내부적으로 컴파일러는 CPU에 가장 알맞은 부호 있는 정수 타입을 사용해요.
최신 PC에서는 보통 Int64가 쓰이는데, 대부분의 작업에 충분해요.
타입은 나중에 나오는 개념에서 더 자세히 다뤄요. 지금은 수치 타입만 이해해도 이 문서를 읽는 데 충분할 거예요.
나중 개념에서 보겠지만, Julia는 아주 큰 다차원 배열을 기본으로 지원해요. 이런 배열은 사용할 수 있는 메모리 양과 데이터 전송 대역폭 모두에 부담이 될 수 있어요.
프로그래머에게 더 세밀한 제어를 주기 위해, 원하는 부호 있는 또는 부호 없는 정수 타입을 지정하는 것도 가능하고 꽤 흔한 일이에요. 이전에 Python을 써 본 Julia 학습자라면 기본 Python보다 NumPy와 비교해 보면 좋아요.
julia> y = Int8(42)
42
julia> typeof(y)
Int8
julia> z = UInt32(1024)
0x00000400
부호 없는 정수는 저수준 비트 연산에 자주 쓰이기 때문에, Julia가 부호 없는 정수를 기본으로 16진수 형식으로 표시한다는 점에 주의해요.
정수는 각각 0b, 0o, 0x 접두사를 붙여 2진수, 8진수, 16진수로 입력할 수도 있어요.
julia> a = 0x10
0x10
julia> Int(a)
16
julia> b = 0b1010 # displays as hexadecimal
0x0a
BigInt
정수 타입마다 저장할 수 있는 최댓값과 최솟값이 있어요.
julia> typemax(Int8)
127
julia> typemin(Int8)
-128
이 유효 범위를 벗어나면 정수 오버플로가 발생하고, 결과가 아주 이상하게 보여요.
julia> c = Int8(126)
126
julia> c * c
4
126 * 126이 4가 되어서는 안 된다는 점에는 아마 모두 동의할 거예요!
자세히 설명하려면 CPU 레지스터가 정수를 저장하는 방식을 알아야 하는데, 이는 이 개념의 범위를 벗어나요.
더 '넓은' 타입을 쓰면 이 문제를 줄일 수 있어요. Int128이나 UInt128까지 가능하죠. ('넓다'는 말의 뜻은 아래에서 다뤄요.) 하지만 어떤 타입이든 오버플로 문제가 생기기 전까지의 한계는 분명히 있어요.
한 가지 해결책은 BigInt 타입을 쓰는 거예요. 이 타입은 컴퓨터의 메모리만큼만 제한돼요.
Julia가 구현한 임의 정밀도 산술이에요.
julia> 2 ^ 70
0
julia> big = BigInt(2) ^ 70
1180591620717411303424
julia> UInt128(big)
0x00000000000000400000000000000000
2의 70제곱은 Int64에 담기지 않기 때문에 위 예제의 첫 번째 계산은 실패해요. 다만 _오류 메시지가 전혀 나오지 않는다_는 점을 기억해요.
프로그래머라면 주의해야 해요!
2를 BigInt로 캐스팅하면 정답이 나오고, 16진수로 표시하면 이것이 2의 거듭제곱이라는 점이 더 분명해져요.
실수에는 소수점이 있을 수도 있고, 소수점 뒤에 소수 부분이 올 수도 있다는 건 그리 놀랍지 않을 거예요.
julia> f = 3.45
3.45
julia> typeof(f)
Float64
물론 과학적 표기법도 지원해요.
julia> avogadro = 6.02e23
6.02e23
정수와 마찬가지로 기본 타입이 대부분의 목적에 충분하지만, 다른 부호 있는 타입도 쓸 수 있어요. 부호 없는 실수 타입은 없어요.
간단히 f0 접미사를 붙여 Float32 값을 만들 수도 있어요.
julia> f32 = 4.56f0
4.56f0
julia> typeof(f32)
Float32
다른 크기의 실수는 정수와 마찬가지로 명시적인 캐스트가 필요해요.
최댓값과 최솟값은 좀 놀라울지도 몰라요.
julia> typemax(Float64)
Inf
julia> typemin(Float64)
-Inf
무한대도 유효한 값이에요!
하지만 실수가 쓸모 있게 표현할 수 있는 범위는 제한적이라서, 아주 큰 값은 그냥 Inf가 되고 아주 작은 값은 0.0으로 반올림돼요.
다른 함수 두 개를 쓰면 이 한계를 볼 수 있어요. 대략 ± 10 ^ 308 정도예요.
julia> floatmax(Float64)
1.7976931348623157e308
julia> floatmin(Float64)
2.2250738585072014e-308
또한 정밀도에도 한계가 있어서, 유효 숫자가 아주 많지만 유한한 개수만 표현돼요 (Float64는 약 15자리).
기본 개념에서 다뤘듯이, 산술 연산자는 대개 아이들이 배우는 일반적인 산수와 똑같이 동작해요.
거듭제곱에는 **가 아니라 ^를 쓴다는 점만 기억해요 (다른 언어에서는 둘 다 흔해요).
2 + 3 # 5 (addition)
2 - 3 # -1 (subtraction)
2 * 3 # 6 (multiplication)
8 / 2 # 4.0 (division)
8 % 3 # 2 (remainder)
2 ^ 3 # 8 (exponentiation)
그런데 Julia만의 특별한 점 몇 가지는 짚고 넘어갈 만해요.
julia> x = 4.2
4.2
julia> 2 * x
8.4
julia> 2x
8.4
julia> 2.4x
10.08
이건 좀 놀라울 수 있어요.
대부분의 다른 프로그래밍 언어처럼 *를 중위 연산자로 쓰는 건 언제나 가능해요.
하지만 Julia는 코드가 가능한 한 수학 방정식처럼 보여야 한다고 믿는 사람들이 설계했어요.
변수 이름은 반드시 문자로 시작해야 하기 때문에, 이름 앞에 숫자(정수든 실수든)를 붙이면 암시적 곱셈으로 처리돼요.
예를 들어 구의 겉넓이를 구할 때, 4 * pi * r * r 대신 이렇게 할 수 있어요:
julia> surface(r) = 4π * r^2
surface (generic function with 1 method)
julia> surface(3)
113.09733552923255
π는 내장 상수이지만 (그리스) 문자이기도 해요.
그래서 파서는 π와 r을 구분하기 위해 명시적인 *가 하나 필요해요.
중위 연산자로 /를 쓰면 정수를 입력해도 항상 실수 결과가 나와요.
정수 나눗셈에는 더 여러 가지 방법이 있어요:
julia> 10 / 3
3.3333333333333335
julia> div(10, 3)
3
julia> 10 ÷ 3
3
julia> 10 // 3
10//3
div() 함수는 정수 나눗셈을 위한 것으로, 결과는 0 방향으로 버려요. 양수에서는 내림, 음수에서는 올림이에요.
같은 뜻으로 중위 연산자 ÷도 쓸 수 있어요. 역시 더 수학처럼 보이게 하려는 거예요.
Julia를 인식하는 편집기를 쓴다면 \div를 입력한 뒤 <Tab> 키를 눌러요.
// 연산자는 나중에 커리큘럼에서 따로 개념으로 다뤄요.
지금은 //의 결과가 유리수라고만 알아두면 돼요. 대부분 사람들이 분수라고 부르는 것의 공식 명칭이죠.
분자와 분모에서 공약수를 없애서, 두 정수의 비를 이른바 '기약분수' 형태로 만들어요.
julia> rationalnum = 22 // 6
11//3
julia> typeof(rationalnum)
Rational{Int64}
유리수는 알겠어요. 그럼 '무리수'는 어떨까요?
julia> π
π = 3.1415926535897...
julia> typeof(π)
Irrational{:π}
무리수는 두 정수의 비로 나타낼 수 없는 수예요.
π, e(오일러 수), 그리고 √2(2의 제곱근) 같은 많은 근이 대표적인 예예요.
Julia는 수학을 제대로 하려고 해요.
이런 변환은 자주 자동으로 일어나요:
julia> x = 2 + 3.5
5.5
julia> typeof(x)
Float64
Int64에 Float64를 더했더니 Float64 결과가 나왔어요.
사실 덧셈을 하기 전에 정수가 조용히 Float64로 변환됐어요.
Julia에는 수치 타입의 '너비'라는 개념이 있어요.
Int64가 Int16보다 넓어요.하나의 표현식 안에서 여러 타입을 섞어 쓰면, 각 값은 필요에 따라 가장 넓은 타입으로 '승격'돼요.
변환을 강제하려면 Float64(5)처럼 정수를 특정 타입으로 캐스팅하면 돼요.
아니면 그냥 float(5)를 쓰고 컴파일러가 알맞은 타입을 고르게 해도 돼요.
실수에서 정수로 바꾸는 변환은 어쩔 수 없이 더 복잡해요. 소수점 뒤의 값은 어떻게 하고 싶은 걸까요?
round() 함수는 가장 가까운 정수로 변환하고, 4.5처럼 중간에 걸친 값은 가장 가까운 짝수 정수로 반올림해요.floor()는 내림, ceil()은 올림, trunc()는 0 방향으로 버림이에요.Int32()처럼 직접 캐스팅하려고 하면 InexactError를 내며 실패해요.그런데 이 함수들은 기본적으로 원하는 정수 타입을 반환하지 않아요. 원하는 출력 타입을 지정할 수 있어요.
julia> round(4.5)
4.0
julia> round(Int64, 4.5)
4
julia> round(Int, 4.5) # => default integer type
4
julia> ceil(Int16, 4.3)
5
digits 키워드를 쓰면 소수점 아래 자릿수를 지정해 반올림할 수도 있어요.
julia> round(π, digits=10)
3.1415926536
자세한 내용은 매뉴얼을 참고해요.
이건 그냥 오류가 나지 않을까요? 사실 상황은 그렇게 단순하지 않아요.
예상대로 ÷나 //로 하는 정수 나눗셈은 오류가 나요.
/로 하는 실수 나눗셈은 일반적인 컴퓨터 과학보다는 공학적인 접근을 택해요:
julia> 2 / 0
Inf
julia> 0 / 0
NaN
앞에서 다뤘듯이 Julia에서 무한대는 Inf로 표현되는 유효한 실수예요.
분자도 0이면 결과는 수학적으로 정의되지 않아요.
그러면 Julia는 이를 NaN으로 표현되는 '숫자가 아님'으로 처리해요.
이게 이상하게 느껴진다면, 현실 세계의 (그래서 종종 꽤 지저분한) 데이터를 담은 큰 배열을 다루는 상황을 떠올려 봐요. 작업을 진행하려면 문제가 있는 값만 표시해 두고 넘어가는 게 가장 좋아요.
값을 끝없이 직접 검사하는 코드는 작성하기도 지루하고, 런타임 성능도 분명히 떨어뜨려요.
사소한 문제마다 오류 메시지를 내며 멈춘다면, 프로그램이 사용자에게 아주 인기 없게 될 거예요!
Conditionals 개념에서 설명했듯이, 같은지 비교할 때는 보통 == 연산자를 써요.
정수, 문자, 문자열 등에는 잘 맞아요.
하지만 실숫값은 정밀도가 제한적이라서, 같은 결과를 계산하는 방식이 다르면 값이 조금씩 달라질 수 있어요.
Float64에서는 보통 15번째 유효 숫자 근처에서 나타나요. 작은 차이지만 '같음'은 아니죠.
전통적으로 프로그래머에게 주는 조언은 실숫값에 ==를 절대 쓰지 말라는 거예요. 결과를 예측할 수 없으니까요.
널리 쓰이는 대안은 두 값의 차의 절댓값을 허용 오차(흔히 엡실론 또는 ϵ이라고 불러요)와 비교하는 거예요.
그래서 a == b 대신 abs(a - b) < epsilon을 써요.
Julia는 isapprox() 함수로 더 깔끔한 대안을 제공해요.
절대 허용 오차를 쓰려면 isapprox(a, b, atol=epsilon) 문법을 써요.
이 경우에는 atol 키워드가 필요해요.
상대 허용 오차가 더 유용할 때가 많아요.
이게 기본값이라서 isapprox(a, b)는 rtol에 알맞은 값을 골라 보려고 해요 (규칙이 꽤 복잡해요).
비교하는 값의 비율로 상대 허용 오차를 지정할 수도 있어요.
예를 들어 isapprox(a, b, rtol=0.01은 두 값이 서로 1% 이내인지 검사해요.
Julia답게, 기본 경우와 같은 뜻인 수학 연산자도 있어요. a ≈ b이고, 연산자는 \approx를 입력한 뒤 <tab>을 누르면 돼요.
유리수뿐만 아니라, 커리큘럼의 뒷부분에서는 다음도 다뤄요: