수학 연산과 기초 함수

수학 연산과 기초 함수

Julia는 모든 원시 숫자 타입에 걸쳐 기초 산술 연산자와 비트 연산자를 빠짐없이 제공하고, 표준 수학 함수의 포괄적인 모음에 대해 이식 가능하면서도 효율적인 구현을 함께 제공해요. 이 글에서는 산술·불리언·비트 연산자부터 갱신 연산자, 벡터화된 "점" 연산자, 숫자 비교, 연산자 우선순위, 숫자 변환까지 한 번에 정리해볼게요.

출처: Mathematical Operations and Elementary Functions — The Julia Language

본문

Julia는 모든 원시 숫자 타입에 걸쳐 기초 산술 연산자와 비트 연산자의 완전한 모음을 제공하고, 표준 수학 함수의 포괄적인 컬렉션에 대해 이식 가능하고 효율적인 구현도 제공해요.

산술 연산자

다음 산술 연산자는 모든 원시 숫자 타입에서 지원돼요.

표현식 이름 설명
+x 단항 플러스 항등 연산
-x 단항 마이너스 값을 덧셈 역원에 매핑
x + y 이항 플러스 덧셈 수행
x - y 이항 마이너스 뺄셈 수행
x * y 곱하기 곱셈 수행
x / y 나누기 나눗셈 수행
x ÷ y 정수 나누기 x / y를 정수로 자름
x \ y 역 나누기 y / x와 동일
x ^ y 거듭제곱 xy 제곱
x % y 나머지 rem(x, y)와 동일

식별자나 괄호 바로 앞에 놓인 숫자 리터럴, 예를 들어 2x2(x + y)는 곱셈으로 취급돼요. 단, 다른 이항 연산보다 높은 우선순위를 가져요. 자세한 내용은 숫자 리터럴 계수를 참고하세요.

Julia의 승격 시스템 덕분에 서로 다른 인자 타입이 섞인 산술 연산도 자연스럽고 자동으로 "그냥 잘" 동작해요. 승격 시스템의 자세한 내용은 변환과 승격을 참고하세요.

÷ 부호는 REPL이나 Julia IDE에서 \div<tab>을 입력하면 편리하게 타이핑할 수 있어요. 자세한 내용은 유니코드 입력 매뉴얼 섹션을 참고하세요.

산술 연산자를 사용하는 간단한 예시를 몇 개 볼게요.

julia> 1 + 2 + 3
6

julia> 1 - 2
-1

julia> 3 * 2 / 12
0.5

(관례상, 다른 인접 연산자보다 먼저 적용되는 연산자는 연산자 사이를 좀 더 좁게 띄우는 경향이 있어요. 예를 들어 -x + 2를 보면 먼저 x가 부정되고, 그 결과에 2가 더해진다는 걸 반영하기 위해서죠.)

곱셈에 쓰일 때 false는 *강한 0(strong zero)*으로 동작해요.

julia> NaN * false
0.0

julia> false * Inf
0.0

이것은 0인 것으로 알려진 양에서 NaN 값이 전파되는 것을 막는 데 유용해요. 동기에 대해서는 Knuth (1992)를 참고하세요.

불리언 연산자

다음 불리언 연산자Bool 타입에서 지원돼요.

표현식 이름
!x 부정
x && y 단락 평가 and
x || y 단락 평가 or

부정은 truefalse로, falsetrue로 바꿔요. 단락 평가 연산은 링크된 페이지에서 설명돼요.

Bool은 정수 타입이고, 일반적인 승격 규칙과 숫자 연산자도 모두 정의돼 있다는 점을 기억하세요.

비트 연산자

다음 비트 연산자는 모든 원시 정수 타입에서 지원돼요.

표현식 이름
~x 비트 not
x & y 비트 and
x | y 비트 or
x ⊻ y 비트 xor (배타적 or)
x ⊼ y 비트 nand (not and)
x ⊽ y 비트 nor (not or)
x >>> y 논리 시프트 오른쪽
x >> y 산술 시프트 오른쪽
x << y 논리/산술 시프트 왼쪽

비트 연산자 예시를 몇 개 볼게요.

julia> ~123
-124

julia> 123 & 234
106

julia> 123 | 234
251

julia> 123 ⊻ 234
145

julia> xor(123, 234)
145

julia> nand(123, 123)
-124

julia> 123 ⊼ 123
-124

julia> nor(123, 124)
-128

julia> 123 ⊽ 124
-128

julia> ~UInt32(123)
0xffffff84

julia> ~UInt8(123)
0x84

갱신 연산자

모든 이항 산술·비트 연산자에는 연산 결과를 왼쪽 피연산자에 다시 할당하는 갱신 버전이 있어요. 이항 연산자의 갱신 버전은 연산자 바로 뒤에 =를 붙여 만들 수 있어요. 예를 들어 x += 3이라고 쓰는 것은 x = x + 3이라고 쓰는 것과 같아요.

julia> x = 1
1

julia> x += 3
4

julia> x
4

모든 이항 산술·비트 연산자의 갱신 버전은 다음과 같아요.

+=  -=  *=  /=  \=  ÷=  %=  ^=  &=  |=  ⊻=  >>>=  >>=  <<=

참고: 갱신 연산자는 왼쪽의 변수를 다시 바인딩해요. 그 결과 변수의 타입이 바뀔 수 있어요.

julia> x = 0x01; typeof(x)
UInt8

julia> x *= 2 # x = x * 2와 같음
2

julia> typeof(x)
Int64

벡터화된 "점" 연산자

^ 같은 모든 이항 연산에 대해, 배열에 요소별로 ^를 수행하도록 자동으로 정의되는 대응 "점" 연산 .^이 있어요. 예를 들어 [1, 2, 3] ^ 3은 정의되지 않아요 (비정방 배열을 "세제곱"한다는 표준 수학적 의미가 없으니까요), 하지만 [1, 2, 3] .^ 3은 요소별("벡터화된") 결과 [1^3, 2^3, 3^3]을 계산하는 것으로 정의돼요. !이나 같은 단항 연산자에도 대응하는 .√이 있어서 연산자를 요소별로 적용해요.

julia> [1, 2, 3] .^ 3
3-element Vector{Int64}:
  1
  8
 27

더 구체적으로 말하면 a .^ b브로드캐스트 연산을 수행하는 "점" 호출 (^).(a,b)로 파싱돼요. 스칼라와 배열을 결합할 수 있고, 같은 크기의 배열끼리(연산을 요소별로 수행), 심지어 다른 모양의 배열끼리도(예: 행·열 벡터를 결합해 행렬 생성) 결합할 수 있어요. 게다가 모든 벡터화된 "점 호출"처럼 이 "점 연산자"들은 *융합(fusing)*되기도 해요. 예를 들어 배열 A에 대해 2 .* A.^2 .+ sin.(A) (또는 @. 매크로를 써서 @. 2A^2 + sin(A))를 계산하면, A에 대해 단일 루프를 수행하면서 각 요소 a2a^2 + sin(a)를 계산해요. 특히 f.(g.(x)) 같은 중첩 점 호출은 융합되고, x .+ 3 .* x.^2 같은 "인접한" 이항 연산자는 중첩 점 호출 (+).(x, (*).(3, (^).(x, 2)))와 같아요.

또한 a .+= b(또는 @. a += b) 같은 "점이 찍힌" 갱신 연산자는 a .= a .+ b로 파싱되는데, 여기서 .=은 융합된 제자리 할당 연산이에요 (점 문법 문서 참고).

점 문법은 사용자 정의 연산자에도 적용된다는 점을 기억하세요. 예를 들어 크로네커 곱(kron)에 편리한 중위 문법 A ⊗ B를 주도록 ⊗(A, B) = kron(A, B)를 정의하면, [A, B] .⊗ [C, D]는 추가 코딩 없이 [A⊗C, B⊗D]를 계산해요.

점 연산자를 숫자 리터럴과 결합하면 모호할 수 있어요. 예를 들어 1.+x1. + x를 뜻하는지 1 .+ x를 뜻하는지 불분명해요. 그래서 이 문법은 금지되고, 그런 경우에는 연산자 주변에 공백을 써야 해요.

숫자 비교 (Numeric Comparisons)

모든 원시 숫자 타입에 표준 비교 연산이 정의돼 있어요. 간단한 예시를 볼게요.

julia> 1 == 1
true

julia> 1 == 2
false

julia> 1 != 2
true

julia> 1 == 1.0
true

julia> 1 < 2
true

julia> 1.0 > 3
false

julia> 1 >= 1.0
true

julia> -1 <= 1
true

julia> -1 <= -1
true

julia> -1 <= -2
false

julia> 3 < -0.5
false

정수는 표준 방식, 즉 비트 비교로 비교돼요. 부동소수점 수는 IEEE 754 표준에 따라 비교돼요.

  • 유한한 수는 일반적인 방식으로 정렬돼요.
  • 양의 0은 음의 0과 같지만 크지는 않아요.
  • Inf는 그 자신과 같고, NaN을 제외한 모든 것보다 커요.
  • -Inf는 그 자신과 같고, NaN을 제외한 모든 것보다 작아요.
  • NaN은 자기 자신을 포함해 어떤 것과도 같지 않고, 작지도 크지도 않아요.

마지막 점은 의외일 수 있어서 주목할 가치가 있어요.

julia> NaN == NaN
false

julia> NaN != NaN
true

julia> NaN < NaN
false

julia> NaN > NaN
false

이것은 배열로 작업할 때 골칫거리가 될 수 있어요.

julia> [1 NaN] == [1 NaN]
false

Julia는 숫자가 특수 값인지 검사하는 추가 함수도 제공해요. 해시 키 비교 같은 상황에서 유용할 수 있어요.

함수 검사 내용
isequal(x, y) xy가 동일한지
isfinite(x) x가 유한한 수인지
isinf(x) x가 무한인지
isnan(x) x가 숫자가 아닌지

isequalNaN들을 서로 같다고 간주해요.

julia> isequal(NaN, NaN)
true

julia> isequal([1 NaN], [1 NaN])
true

julia> isequal(NaN, NaN32)
true

isequal은 부호 있는 0을 구분하는 데도 쓸 수 있어요.

julia> -0.0 == 0.0
true

julia> isequal(-0.0, 0.0)
false

부호 있는 정수, 부호 없는 정수, 부동소수점 사이의 혼합 타입 비교는 까다로울 수 있어요. Julia가 그것들을 올바르게 수행하도록 많은 주의를 기울였어요.

다른 타입에 대해 isequal은 기본적으로 ==를 호출해요. 그래서 자신만의 타입에 같음을 정의하려면 == 메서드만 추가하면 돼요. 자신만의 같음 함수를 정의한다면, isequal(x,y)hash(x) == hash(y)를 함의하도록 대응하는 hash 메서드도 정의해야 해요.

연결 비교

대부분의 언어와 달리, Python이 눈에 띄는 예외지만, 비교는 임의로 연결할 수 있어요.

julia> 1 < 2 <= 2 < 3 == 3 > 2 >= 1 == 1 < 3 != 5
true

연결 비교는 수치 코드에서 꽤 자주 편리해요. 연결 비교는 스칼라 비교에 && 연산자를, 요소별 비교에는 & 연산자를 써서 배열에서도 동작하게 해줘요. 예를 들어 0 .< A .< 1A의 해당 요소가 0과 1 사이인 곳이 true인 불리언 배열을 줘요.

연결 비교의 평가 동작을 주목하세요.

julia> v(x) = (println(x); x)
v (generic function with 1 method)

julia> v(1) < v(2) <= v(3)
2
1
3
true

julia> v(1) > v(2) <= v(3)
2
1
false

가운데 표현식은 v(1) < v(2) && v(2) <= v(3)처럼 썼다면 두 번 평가됐을 것을 한 번만 평가해요. 하지만 연결 비교에서 평가 순서는 정의되지 않아요. 연결 비교에는 (출력 같은) 부작용이 있는 표현식을 쓰지 않는 걸 강력히 권장해요. 부작용이 필요하다면 단락 && 연산자를 명시적으로 써야 해요 (단락 평가 참고).

기초 함수

Julia는 포괄적인 수학 함수와 연산자 컬렉션을 제공해요. 이 수학 연산들은 정수, 부동소수점 수, 유리수, 복소수 등 그런 정의가 타당한 곳이라면 가능한 한 넓은 숫자 값의 범위에 정의돼요.

게다가 이 함수들(다른 Julia 함수처럼)은 점 문법 f.(A)로 배열과 다른 컬렉션에 "벡터화된" 방식으로 적용될 수 있어요. 예를 들어 sin.(A)는 배열 A의 각 요소의 사인을 계산해요.

연산자 우선순위와 결합성

Julia는 연산 순서와 결합성을 높은 우선순위부터 낮은 우선순위로 다음과 같이 적용해요.

범주 연산자 결합성
문법 . 뒤에 :: 왼쪽
거듭제곱 ^ 오른쪽
단항 + - ! ~ ¬ √ ∛ ∜ ⋆ ± ∓ <: >: 오른쪽[1]
비트 시프트 << >> >>> 왼쪽
분수 // 왼쪽
곱셈 * / % & \ ÷ 왼쪽[2]
덧셈 + - | ⊻ 왼쪽[2]
문법 : .. 왼쪽
문법 |> 왼쪽
문법 <| 오른쪽
비교 > < >= <= == === != !== <: 비결합
제어 흐름 && 다음 || 다음 ? 오른쪽
=> 오른쪽
할당 = += -= *= /= //= \= ^= ÷= %= |= &= ⊻= <<= >>= >>>= 오른쪽

모든 Julia 연산자의 우선순위 전체 목록은 이 파일의 맨 위, src/julia-parser.scm를 참고하세요. 거기 있는 연산자 중 일부는 Base 모듈에 정의되지 않았지만 표준 라이브러리, 패키지, 사용자 코드로 정의될 수 있다는 점을 기억하세요.

주어진 연산자의 숫자 우선순위는 내장 함수 Base.operator_precedence로도 찾을 수 있어요. 숫자가 높을수록 우선순위가 높아요.

julia> Base.operator_precedence(:+), Base.operator_precedence(:*), Base.operator_precedence(.:)
(11, 12, 17)

julia> Base.operator_precedence(:sin), Base.operator_precedence(:+=), Base.operator_precedence(:(=)) # `:(=)`의 괄호에 주의
(0, 1, 1)

연산자 결합성을 나타내는 심볼은 내장 함수 Base.operator_associativity를 호출해 찾을 수도 있어요.

julia> Base.operator_associativity(:-), Base.operator_associativity(:+), Base.operator_associativity(:^)
(:left, :none, :right)

julia> Base.operator_associativity(:⊗), Base.operator_associativity(:sin), Base.operator_associativity(:→)
(:left, :none, :right)

:sin 같은 심볼은 우선순위 0을 반환한다는 점을 기억하세요. 이 값은 유효하지 않은 연산자를 나타내지, 가장 낮은 우선순위의 연산자를 나타내는 게 아니에요. 마찬가지로 그런 연산자에는 결합성 :none이 할당돼요.

숫자 리터럴 계수, 예를 들어 2x는 다른 어떤 이항 연산보다 높은 우선순위의 곱셈으로 취급돼요. 단 ^에 대해서만 지수로 사용될 때 더 높은 우선순위를 가져요.

julia> x = 3; 2x^2
18

julia> x = 3; 2^2x
64

나란히 붙는 것은 단항 연산자처럼 파싱돼서 지수 주변에서 같은 자연스러운 비대칭을 가져요. -x^y2x^y-(x^y)2(x^y)로 파싱되지만, x^-yx^2yx^(-y)x^(2y)로 파싱돼요.

숫자 변환

Julia는 부정확한 변환을 다루는 방식이 다른 세 가지 형태의 숫자 변환을 지원해요.

  • T(x) 또는 convert(T, x) 표기법은 x를 타입 T의 값으로 변환해요.
    • T가 부동소수점 타입이면 결과는 가장 가까운 표현 가능 값이며, 양의 또는 음의 무한대일 수 있어요.
    • T가 정수 타입이면 xT로 표현 가능하지 않을 때 InexactError가 발생해요.
  • x % T는 정수 x를, T의 비트 수 n에 대해 2^n modulo로 x와 합동인 정수 타입 T의 값으로 변환해요. 다시 말해 이진 표현이 맞도록 잘려요.
  • 반올림 함수는 타입 T를 선택적 인자로 받아요. 예를 들어 round(Int,x)Int(round(x))의 줄임말이에요.

다음 예시들은 서로 다른 형태를 보여줘요.

julia> Int8(127)
127

julia> Int8(128)
ERROR: InexactError: trunc(Int8, 128)
Stacktrace: [...]

julia> Int8(127.0)
127

julia> Int8(3.14)
ERROR: InexactError: Int8(3.14)
Stacktrace: [...]

julia> Int8(128.0)
ERROR: InexactError: Int8(128.0)
Stacktrace: [...]

julia> 127 % Int8
127

julia> 128 % Int8
-128

julia> round(Int8, 127.4)
127

julia> round(Int8, 127.6)
ERROR: InexactError: Int8(128.0)
Stacktrace: [...]

자신만의 변환과 승격을 정의하는 방법은 변환과 승격을 참고하세요.

반올림 함수

함수 설명 반환 타입
round(x) x를 가장 가까운 정수로 반올림 typeof(x)
round(T, x) x를 가장 가까운 정수로 반올림 T
floor(x) x-Inf 방향으로 반올림 typeof(x)
floor(T, x) x-Inf 방향으로 반올림 T
ceil(x) x+Inf 방향으로 반올림 typeof(x)
ceil(T, x) x+Inf 방향으로 반올림 T
trunc(x) x를 0 방향으로 반올림 typeof(x)
trunc(T, x) x를 0 방향으로 반올림 T

나눗셈 함수

함수 설명
div(x, y), x÷y 잘린 나눗셈. 몫을 0 방향으로 반올림
fld(x, y) 바닥 나눗셈. 몫을 -Inf 방향으로 반올림
cld(x, y) 천장 나눗셈. 몫을 +Inf 방향으로 반올림
rem(x, y), x%y 나머지. x == div(x, y)*y + rem(x, y)를 만족. 부호는 x를 따름
mod(x, y) 모듈로. x == fld(x, y)*y + mod(x, y)를 만족. 부호는 y를 따름
mod1(x, y) mod에 오프셋 1. y>0이면 r∈(0, y], y<0이면 r∈[y, 0) 반환. mod(r, y) == mod(x, y)
mod2pi(x) 2π에 대한 모듈로. 0 <= mod2pi(x) < 2pi
divrem(x, y) (div(x, y), rem(x, y)) 반환
fldmod(x, y) (fld(x, y), mod(x, y)) 반환
gcd(x, y...) x, y,...의 가장 큰 양의 공통 약수
lcm(x, y...) x, y,...의 가장 작은 양의 공통 배수

부호와 절댓값 함수

함수 설명
abs(x) x의 크기를 가진 양의 값
abs2(x) x의 크기 제곱
sign(x) x의 부호를 나타내며 -1, 0, +1 중 하나 반환
signbit(x) 부호 비트가 켜져 있는지(true)/꺼져 있는지(false) 나타냄
copysign(x, y) x의 크기와 y의 부호를 가진 값
flipsign(x, y) x의 크기와 x*y의 부호를 가진 값

거듭제곱, 로그와 근

함수 설명
sqrt(x), √x x의 제곱근
cbrt(x), ∛x x의 세제곱근
fourthroot(x), ∜x x의 네제곱근
hypot(x, y) 다른 두 변의 길이가 xy인 직각삼각형의 빗변
exp(x) x에서의 자연지수 함수
expm1(x) 0 근처의 x에 대해 정확한 exp(x) - 1
ldexp(x, n) 정수 값 n에 대해 효율적으로 계산된 x * 2^n
log(x) x의 자연로그
log(b, x) x의 밑 b 로그
log2(x) x의 밑 2 로그
log10(x) x의 밑 10 로그
log1p(x) 0 근처의 x에 대해 정확한 log(1 + x)
exponent(x) x의 이진 지수
significand(x) 부동소수점 수 x의 이진 가수(일명 mantissa)

hypot, expm1, log1p 같은 함수가 왜 필요하고 유용한지 개관하려면, John D. Cook의 훌륭한 두 편의 블로그 글을 보세요. expm1, log1p, erfchypot예요.

삼각·쌍곡선 함수

표준 삼각 함수와 쌍곡선 함수도 모두 정의돼 있어요.

 sin  cos  tan  cot  sec  csc  sinh  cosh  tanh  coth  sech  csch
 asin  acos  atan  acot  asec  acsc  asinh  acosh  atanh  acoth  asech  acsch
 sinc  cosc

이들은 모두 단일 인자 함수이고, atan은 전통적인 atan2 함수에 해당하는 두 인자도 받아요.

추가로 sinpi(x)cospi(x)가 제공돼요. 각각 sin(pi * x)cos(pi * x)를 더 정확하게 계산해줘요.

도를 라디안 대신 사용해 삼각 함수를 계산하려면 함수에 d를 붙이면 돼요. 예를 들어 sind(x)x를 도로 지정했을 때의 사인을 계산해요. 도 버전이 있는 삼각 함수의 전체 목록은 다음과 같아요.

 sind  cosd  tand  cotd  secd  cscd  asind  acosd  atand  acotd  asecd  acscd

특수 함수

다른 많은 특수 수학 함수는 패키지 SpecialFunctions.jl이 제공해요.

  1. 단항 연산자 +-++ 같은 연산자와 구분하기 위해 인자 주변에 명시적인 괄호가 필요해요. 단항 연산자의 다른 합성은 오른쪽 결합성으로 파싱돼요. 예를 들어 √√-a√(√(-a))로 파싱돼요.
  2. 연산자 +, ++, *는 비결합이에요. a + b + c+(+(a, b), c)가 아니라 +(a, b, c)로 파싱돼요. 하지만 +(a, b, c, d...)*(a, b, c, d...)의 폴백 메서드는 모두 기본적으로 왼쪽 결합성 평가를 해요.

더 알아보기