Raku의 수치 타입
Raku의 수치 타입 (Numerics)
Raku는 정수·부동소수·유리수·복소수 등을 각각 특성에 맞는 타입으로 제공해요. 이 문서는 Int, Num, Complex, Rat, FatRat 같은 핵심 수치 타입과, 분모가 0인 유리수, allomorph, 네이티브 수치의 동작을 하나씩 살펴봐요.
본문
Int (정수)
Int 타입은 임의 크기의 정수를 제공해요. 컴퓨터 메모리가 허락하는 한 커질 수 있지만, 일부 구현은 정말 어마어마한 크기의 정수를 만들라고 하면 수치 오버플로 오류를 던지기도 해요:
say 10**600**600
# OUTPUT: «Numeric overflow»
어떤 언어와 달리, 두 피연산자가 모두 Int일 때 / 연산자로 나누면 반올림 없이 분수를 만들어요.
say 4/5; # OUTPUT: «0.8»
이 나눗셈이 만드는 타입은 Rat 또는 Num이에요. 약분(reduction) 후 분모가 64비트보다 작으면 Rat, 그보다 크면 Num이 나와요.
가능하면 Int 결과를 얻고 싶을 때 div와 narrow 루틴이 유용해요. div 연산자는 나머지를 버리는 정수 나눗셈을 하고, narrow는 숫자를 들어맞는 가장 좁은 타입에 맞춰요:
say 5 div 2; # OUTPUT: «2»
# Result `2` is narrow enough to be an Int:
say (4/2).narrow; # OUTPUT: «2»
say (4/2).narrow.^name; # OUTPUT: «Int»
# But 2.5 has fractional part, so it ends up being a Rat type:
say (5/2).narrow.^name; # OUTPUT: «Rat»
say (5/2).narrow; # OUTPUT: «2.5»
# Denominator is too big for a Rat, so a Num is produced:
say 1 / 10⁹⁹; # OUTPUT: «1e-99»
Raku는 임의 정밀도 분수를 제공하는 FatRat 타입도 있어요. 그런데 위 마지막 예시에서 왜 FatRat 대신 정밀도가 제한된 Num이 나왔을까요? 이유는 성능이에요. 대부분의 연산은 약간의 정밀도 손실로 충분하고, 더 비싼 FatRat을 쓸 필요가 없어요. 추가 정밀도가 필요하다면 직접 인스턴스를 만들어야 해요.
Num (부동소수점)
Num 타입은 배정밀도 부동소수점 소수를 제공해요. 다른 언어에서는 "double"이라고 부르죠.
Num 리터럴은 지수를 e 글자로 구분해서 써요. 지수가 0이어도 e는 필수라는 점을 기억하세요. 그렇지 않으면 Rat 리터럴이 돼버려요:
say 42e0.^name; # OUTPUT: «Num»
say 42.0.^name; # OUTPUT: «Rat»
대소문자 구분 단어 Inf와 NaN은 각각 무한대와 숫자가 아님(not-a-number)을 나타내요. Inf 대신 U+221E INFINITY(∞) 문자를 쓸 수도 있어요.
Raku는 가능한 한 IEEE 754-2008 부동소수점 산술 표준을 따르며, 더 많은 준수를 이후 언어 버전에서 계획 중이에요. 언어는 주어진 Num 리터럴에 대해 표현 가능한 가장 가까운 숫자를 고르는 걸 보장하고, 음의 영(negative zero)과 denormal(비정규)(subnormal이라고도 함)을 지원해요.
say나 put 같은 출력 루틴은 Numeric 타입 출력을 서로 구분하려 애쓰지 않아서 Num을 Int나 Rat로 표시할 수 있어요. 더 명확한 문자열 출력이 필요하면 raku 메서드를 쓰세요:
say 1e0; # OUTPUT: «1»
say .5e0; # OUTPUT: «0.5»
say 1e0.raku; # OUTPUT: «1e0»
say .5e0.raku; # OUTPUT: «0.5e0»
Complex (복소수)
Complex 타입은 복소평면의 수를 나타내요. Complex 객체는 복소수의 실수 부분과 허수 부분을 나타내는 두 개의 Num 객체로 이뤄져요.
Complex를 만들려면 복소수가 아닌 다른 숫자에 접미사 i 연산자를 쓰면 되고, 실수부는 덧셈으로 선택적으로 설정할 수 있어요. NaN이나 Inf 리터럴에 i 연산자를 쓰려면 백슬래시로 분리하세요.
say 42i; # OUTPUT: «0+42i»
say 73+42i; # OUTPUT: «73+42i»
say 73+Inf\i; # OUTPUT: «73+Inf\i»
위 문법은 그냥 덧셈 표현식이므로 우선순위 규칙이 적용돼요. 루틴 매개변수의 리터럴처럼 표현식을 금지하는 곳에서는 쓸 수 없어요.
# Precedence of `*` is higher than that of `+`
say 2 * 73+10i; # OUTPUT: «146+10i»
이런 문제를 피하려면 Complex 리터럴 문법을 쓰면 돼요. 실수부와 허수부를 공백 없이 꺾쇠괄호로 감싸는 방식이에요:
say 2 * <73+10i>; # OUTPUT: «146+20i»
multi how-is-it (<2+4i>) { say "that's my favorite number!" }
multi how-is-it (|) { say "meh" }
how-is-it 2+4i; # OUTPUT: «that's my favorite number!»
how-is-it 3+2i; # OUTPUT: «meh»
Rational (유리수)
Rational 역할(role)을 하는 타입들은 고정밀도·임의 정밀도 소수를 제공해요. 정밀도가 높을수록 성능 패널티가 커지므로 Rational 타입은 두 가지로 나뉘어요: Rat과 FatRat. Rat은 가장 자주 쓰는 변형으로, 요청한 정밀도를 더 이상 담지 못하면 대부분의 경우 Num으로 떨어져요. FatRat은 요청한 정밀도를 모두 제공하기 위해 계속 커지는 임의 정밀도 변형이에요.
Rat
가장 흔한 Rational 타입이에요. 분모가 (약분 후) 64비트까지인 유리수를 지원해요. 더 큰 분모의 Rat 객체를 직접 만들 수는 있지만, 그런 분모가 수학 연산의 결과로 나오면 Num으로 떨어져요.
Rat 리터럴은 많은 언어에서 Num 리터럴과 비슷한 문법을 써요. 점으로 소수임을 나타내죠:
say .1 + .2 == .3; # OUTPUT: «True»
흔한 언어들에서 위와 같은 문장을 실행하면 부동소수점 부정확성 때문에 False가 나와요. Raku에서 같은 결과를 얻으려면 Num 리터럴을 써야 해요:
say .1e0 + .2e0 == .3e0; # OUTPUT: «False»
Int나 Rat 객체에 / 나눗셈 연산자를 써서 Rat을 만들 수도 있어요:
say 3/4; # OUTPUT: «0.75»
say 3/4.2; # OUTPUT: «0.714286»
say 1.1/4.2; # OUTPUT: «0.261905»
위 문법은 그냥 나눗셈 표현식이므로 우선순위 규칙이 적용돼요. 표현식을 금지하는 곳(루틴 매개변수 리터럴)에서는 못 써요.
# Precedence of power operators is higher than division
say 3/2²; # OUTPUT: «0.75»
이런 문제를 피하려면 Rational 리터럴 문법 — 분자와 분모를 공백 없이 꺾쇠괄호로 감싸는 것 — 을 쓰면 돼요:
say <3/2>²; # OUTPUT: «2.25»
multi how-is-it (<3/2>) { say "that's my favorite number!" }
multi how-is-it (|) { say "meh" }
how-is-it 3/2; # OUTPUT: «that's my favorite number!»
how-is-it 1/3; # OUTPUT: «meh»
마지막으로, 분수를 나타내는 No 속성을 가진 유니코드 문자는 Rat 리터럴로 쓸 수 있어요:
say ½ + ⅓ + ⅝ + ⅙; # OUTPUT: «1.625»
Num으로의 강등 (Degradation to Num)
Rat 답을 만드는 수학 연산이 분모가 64비트보다 큰 Rat을 만들려 하면, 그 연산은 대신 Num 객체를 반환해요. 하지만 Rat을 *구성(construct)*할 때(즉 어떤 수학 표현식의 결과가 아닐 때)는 더 큰 분모를 쓸 수 있어요:
my $a = 1 / (2⁶⁴ - 1);
say $a; # OUTPUT: «0.000000000000000000054»
say $a.^name; # OUTPUT: «Rat»
say $a.nude; # OUTPUT: «(1 18446744073709551615)»
my $b = 1 / 2⁶⁴;
say $b; # OUTPUT: «5.421010862427522e-20»
say $b.^name; # OUTPUT: «Num»
my $c = Rat.new(1, 2⁶⁴);
say $c; # OUTPUT: «0.000000000000000000054»
say $c.^name; # OUTPUT: «Rat»
say $c.nude; # OUTPUT: «(1 18446744073709551616)»
say $c.Num; # OUTPUT: «5.421010862427522e-20»
FatRat
마지막 Rational 타입인 FatRat은 요청한 모든 정밀도를 유지하며, 분자·분모를 두 Int 객체로 저장해요. FatRat은 Rat보다 더 "전염성"이 강해서, FatRat을 포함한 많은 수학 연산은 또 다른 FatRat을 만들어 가용한 모든 정밀도를 보존해요. Rat이 Num으로 떨어지는 곳에서 FatRat을 쓰는 수학은 계속 진행돼요:
say ((42 + Rat.new(1,2))/999999999999999999).^name; # OUTPUT: «Rat»
say ((42 + Rat.new(1,2))/9999999999999999999).^name; # OUTPUT: «Num»
say ((42 + FatRat.new(1,2))/999999999999999999).^name; # OUTPUT: «FatRat»
say ((42 + FatRat.new(1,2))/99999999999999999999999).^name; # OUTPUT: «FatRat»
FatRat 객체를 만드는 특별한 연산자나 문법은 없어요. FatRat.new 메서드에 분자를 첫 번째 위치 인자로, 분모를 두 번째로 주면 돼요.
프로그램에서 FatRat을 많이 만들어야 한다면 직접 맞춤 연산자를 만들 수 있어요:
sub infix:<🙼> { FatRat.new: $^a, $^b }
say (1🙼3).raku; # OUTPUT: «FatRat.new(1, 3)»
유리수 출력하기 (Printing rationals)
say나 put 같은 출력 루틴은 Numeric 타입 출력을 구분하려 애쓰지 않아요. 더 명확한 문자열이 필요하면 raku 메서드를 쓰세요:
say 1.0; # OUTPUT: «1»
say ⅓; # OUTPUT: «0.333333»
say 1.0.raku; # OUTPUT: «1.0»
say ⅓.raku; # OUTPUT: «<1/3>»
nude 메서드는 Rational의 약분된 **분자(nu)**와 **분모(de)**를 담은 List를 반환해요.
say <300/12>.nude; # OUTPUT: «(25 1)»
0으로 나누기 (Division by zero)
많은 언어에서 0으로 나누기는 즉시 예외가 돼요. Raku에서는 무엇을 나누느냐, 결과를 어떻게 쓰느냐에 따라 달라져요.
Raku는 IEEE 754-2008 표준을 따르지만, 역사적 이유로 6.c와 6.d 언어 버전은 완전히 준수하지 않아요. Num을 0으로 나누면 Failure를 만들고, Complex를 0으로 나누면 분자가 무엇이든 NaN 성분을 만들어요.
6.e 언어부터는 Num과 Complex 모두 0으로 나누면 분자가 음수·양수·0인지에 따라 각각 -Inf, +Inf, 또는 NaN을 만들어요(Complex의 실수·허수 성분은 Num이라 각각 따로 간주돼요).
Int 수치를 나누면 Rat 객체(또는 약분 후 분모가 64비트보다 크면 Num — 0으로 나눌 때는 해당 없음)를 만들어요. 즉 그런 나눗셈은 Exception이나 Failure을 만들지 않아요. 결과는 폭발적(explosive)일 수 있는 Zero-Denominator(분모 0) 유리수예요.
분모 0 유리수 (Zero-denominator rationals)
분모 0 유리수는 분모가 0인, Rational 역할을 하는 수치(핵심 수치 중에선 Rat과 FatRat)예요. 그런 유리수의 분자는 원래 분자가 음수·0·양수인지에 따라 -1, 0, 1로 정규화돼요.
실제 나눗셈 없이 수행할 수 있는 연산은 폭발적이지 않아요. 예를 들어 nude에서 분자와 분모를 따로 살펴보거나, 예외·실패 없이 수학 연산을 수행할 수 있어요.
분모 0 유리수를 Num으로 변환하는 것은 IEEE 관례를 따르며, 분자가 음수·양수·0인지에 따라 -Inf, Inf, NaN이 돼요. 반대 방향도 마찬가지예요: ±Inf/NaN을 Rational 타입 중 하나로 변환하면 적절한 분자를 가진 분모 0 유리수가 만들어져요:
say <1/0>.Num; # OUTPUT: «Inf»
say <-1/0>.Num; # OUTPUT: «-Inf»
say <0/0>.Num; # OUTPUT: «NaN»
say Inf.Rat.nude; # OUTPUT: «(1 0)»
분자와 분모의 비-IEEE 나눗셈이 필요한 다른 모든 연산은 X::Numeric::DivideByZero 예외를 던져요. 가장 흔한 경우는 분모 0 유리수를 출력하거나 문자열화하려는 시도예요:
say 0/0;
# OUTPUT:
# Attempt to divide by zero using div
# in block <unit> at -e line 1
Allomorph (알로모프)
Allomorph은 두 타입의 서브클래스로, 둘 중 하나처럼 행동할 수 있어요. 예를 들어 IntStr이라는 allomorph는 Int와 Str 타입의 서브클래스라서, Int나 Str 객체를 요구하는 어떤 타입 제약이든 통과해요.
Allomorph는 꺾쇠괄호>를 단독으로 또는 해시 키 조회의 일부로 써서 만들 수 있고, .new 메서드로 직접 만들 수도 있으며, sub MAIN의 매개변수 같은 일부 구조가 제공하기도 해요.
say <42>.^name; # OUTPUT: «IntStr»
say <42e0>.^name; # OUTPUT: «NumStr»
say < 42+42i>.^name; # OUTPUT: «ComplexStr»
say < 1/2>.^name; # OUTPUT: «RatStr»
say <0.5>.^name; # OUTPUT: «RatStr»
@*ARGS = "42";
sub MAIN($x) { say $x.^name } # OUTPUT: «IntStr»
say IntStr.new(42, "42").^name; # OUTPUT: «IntStr»
위 구성 몇 개는 여는 꺾쇠괄호 뒤에 공백이 있어요. 그 공백은 우연이 아니에요. 1/2(Rat, 나눗셈 연산자)와 1+2i(Complex, 덧셈)처럼 연산자를 써서 주로 쓰는 수치를, 연산자 없이 리터럴로 쓸 수 있어요: 꺾쇠괄호와 내부 문자 사이에 공백이 없는 꺾쇠괄호예요. 꺾쇠괄호 안에 공백을 넣으면 컴파일러에 Rat이나 Complex 리터럴뿐 아니라 allomorph(RatStr 또는 ComplexStr)도 원한다고 알려주는 거예요.
수치 리터럴이 연산자를 안 쓰면, 꺾쇠괄호 안에 공백 없이 써도 allomorph가 돼요. (논리: allomorph를 원하지 않았다면 꺾쇠괄호를 안 썼을 거예요. 연산자를 쓰는 숫자는 시그니처 리터럴 같은 구성이 연산자를 허용하지 않아 꺾쇠괄호를 생략할 수 없으니, 그 경우엔 다르죠.)
사용 가능한 allomorph (Available allomorphs)
핵심 언어는 다음 allomorph를 제공해요:
| Type | Allomorph of | Example |
|---|---|---|
| IntStr | Int and Str | <42> |
| NumStr | Num and Str | <42e0> |
| ComplexStr | Complex and Str | < 1+2i> |
| RatStr | Rat and Str | <1.5> |
참고: FatRatStr 타입은 없어요.
Allomorph의 강제(coercion)
allomorph는 단순히 나타내는 타입의 서브클래스라는 점을 기억하세요. Foo로 타입 제약한 변수·매개변수가 Foo의 어떤 서브클래스든 받듯이, Int로 타입 제약한 변수·매개변수는 IntStr allomorph를 받아요:
sub foo(Int $x) { say $x.^name }
foo <42>; # OUTPUT: «IntStr»
my Num $y = <42e0>;
say $y.^name; # OUTPUT: «NumStr»
이것은 매개변수 coercer에도 적용돼요:
sub foo(Int(Cool) $x) { say $x.^name }
foo <42>; # OUTPUT: «IntStr»
주어진 allomorph는 이미 Int 타입의 객체라서, 이 경우 "평범한" Int로 변환되지 않아요.
allomorph를 그 성분 중 하나로 "접는(collapse)" 방법이 없으면 그 힘이 크게 줄어들 거예요. 그래서 강제할 타입의 이름을 가진 메서드를 명시적으로 호출하면 그 성분만 얻어요. .Int 대신 .Numeric을 호출하거나 .Str 메서드 대신 prefix:<~> 연산자를 쓰는 프록시 메서드에도 같아요.
my $al := IntStr.new: 42, "forty two";
say $al.Str; # OUTPUT: «forty two»
say +$al; # OUTPUT: «42»
say <1/99999999999999999999>.Rat.^name; # OUTPUT: «Rat»
say <1/99999999999999999999>.FatRat.^name; # OUTPUT: «FatRat»
allomorph 리스트 전체를 강제하는 편리한 방법은 하이퍼 연산자를 적절한 prefix에 적용하는 거예요:
say map *.^name, <42 50e0 100>; # OUTPUT: «(IntStr NumStr IntStr)»
say map *.^name, +«<42 50e0 100>; # OUTPUT: «(Int Num Int)»
say map *.^name, ~«<42 50e0 100>; # OUTPUT: «(Str Str Str)»
객체 정체성 (Object identity)
위 allomorph 강제 논의는 객체 정체성을 생각할 때 더 중요해져요. 일부 구성은 두 객체가 "같은" 것인지 판정할 때 정체성을 사용해요. 사람 눈에는 allomorphic 42와 일반 42가 "같아" 보여도, 그런 구성이 보기엔 전혀 다른 객체예요:
# "42" shows up twice in the result: 42 and <42> are different objects:
say unique 1, 1, 1, 42, <42>; # OUTPUT: «(1 42 42)»
# Use a different operator to `unique` with:
say unique :with(&[==]), 1, 1, 1, 42, <42>; # OUTPUT: «(1 42)»
# Or coerce the input instead (faster than using a different `unique` operator):
say unique :as(*.Int), 1, 1, 1, 42, <42>; # OUTPUT: «(1 42)»
say unique +«(1, 1, 1, 42, <42>); # OUTPUT: «(1 42)»
# Parameterized Hash with `Any` keys does not stringify them; our key is of type `Int`:
my %h{Any} = 42 => "foo";
# But we use the allomorphic key of type `IntStr`, which is not in the Hash:
say %h<42>:exists; # OUTPUT: «False»
# Must use curly braces to avoid the allomorph:
say %h{42}:exists; # OUTPUT: «True»
# We are using a set operator to look up an `Int` object in a list of `IntStr` objects:
say 42 ∈ <42 100 200>; # OUTPUT: «False»
# Convert it to an allomorph:
say <42> ∈ <42 100 200>; # OUTPUT: «True»
# Or convert the items in the list to plain `Int` objects:
say 42 ∈ +«<42 100 200>; # OUTPUT: «True»
이런 객체 정체성 차이를 염두에 두고 필요할 때 allomorph를 강제하세요.
비-allomorph 변환하기 (Transforming non-Allomorphs)
변수를 명시적으로 Str이나 Numeric 타입으로 만들면 그건 Allomorph이 아니에요:
my $a = "010"; say $a.^name; # OUTPUT: «Str»
my $b = 42; say $b.^name; # OUTPUT: «Int»
그런 다음 명시적으로 Allomorph 상당형으로 변환할 수 있어요:
$a .= Numeric; # OUTPUT: «10»
say $a.^name; # OUTPUT: «Int»
$b .= Str; ; # OUTPUT: «42»
say $b.^name; # OUTPUT: «Str»
혹은 자연스럽게 다른 형태로 강제할 수도 있어요:
my $a = "010"; say $a.^name; # OUTPUT: «Str»
my $b = $a + 1; # OUTPUT: «11»
say $b.^name; # OUTPUT: «Int»
네이티브 수치 (Native numerics)
이름이 말해 주듯 네이티브 수치는 하드웨어가 직접 제공하는 수치에 접근하게 해 줘요. 이는 두 가지를 제공해요: 오버플로/언더플로와 더 나은 성능.
참고: 이 글을 쓰는 시점(2018.05)에 일부 구현(Rakudo 같은)은 네이티브 타입에 대한 세부 사항이 다소 불완전해요. 예를 들어 32비트 머신에서 int64가 사용 가능하고 64비트 크기인지, 그런 하드웨어에서 프로그램이 도는지 어떻게 감지할지 같은 것들이요.
사용 가능한 네이티브 수치 (Available native numerics)
| Native type | Base numeric | Size |
|---|---|---|
| int | integer | 64-bits |
| int8 | integer | 8-bits |
| int16 | integer | 16-bits |
| int32 | integer | 32-bits |
| int64 | integer | 64-bits |
| uint | unsigned integer | 64-bits |
| uint8 | unsigned integer | 8-bits |
| uint16 | unsigned integer | 16-bits |
| uint32 | unsigned integer | 32-bits |
| uint64 | unsigned integer | 64-bits |
| num | floating point | 64-bits |
| num32 | floating point | 32-bits |
| num64 | floating point | 64-bits |
| atomicint | integer | sized to offer CPU-provided atomic operations. (typically 64 bits on 64-bit platforms and 32 bits on 32-bit ones) |
네이티브 수치 만들기 (Creating native numerics)
네이티브 타입 변수·매개변수를 만들려면 사용 가능한 수치 중 하나의 이름을 타입 제약으로 쓰면 돼요:
my int32 $x = 42;
sub foo(num $y) {}
class { has int8 $.z }
때로는 사용 가능한 변수를 만들지 않고 어떤 값을 네이티브 타입으로 강제하고 싶을 수 있어요. .int 같은 강제 메서드는 없어요(메서드 호출은 늦게 바인딩되어 이 목적에 잘 맞지 않아요). 대신 익명 변수를 쓰면 돼요:
some-native-taking-sub( (my int $ = $y), (my int32 $ = $z) )
오버플로/언더플로 (Overflow/Underflow)
특정 네이티브 타입에 안 들어맞는 값을 할당하려 하면 예외가 나요. 네이티브 매개변수에 너무 큰 인자를 주려는 시도도 포함돼요:
my int $x = 2¹⁰⁰;
# OUTPUT:
# Cannot unbox 101 bit wide bigint into native integer
# in block <unit> at -e line 1
sub f(int $x) { $x }; say f 2⁶⁴
# OUTPUT:
# Cannot unbox 65 bit wide bigint into native integer
# in sub f at -e line 1
# in block <unit> at -e line 1
하지만 이미 존재하는 값을 너무 크거나 작게 만드는 식으로 수정하면 오버플로/언더플로 동작이 생겨요:
my int $x = 2⁶³-1;
say $x; # OUTPUT: «9223372036854775807»
say ++$x; # OUTPUT: «-9223372036854775808»
my uint8 $x;
say $x; # OUTPUT: «0»
say $x -= 100; # OUTPUT: «156»
네이티브 타입을 쓰는 객체를 만들 때 프로그래머가 직접 할당하는 게 아니에요. 그래서 이 구성들은 예외를 던지는 대신 오버플로/언더플로 동작을 제공해요.
say Buf.new(1000, 2000, 3000).List; # OUTPUT: «(232 208 184)»
say my uint8 @a = 1000, 2000, 3000; # OUTPUT: «232 208 184»
자동 박싱 (Auto-boxing)
"네이티브 타입"이라고 부르지만, 네이티브 수치는 실제로 메서드가 있는 클래스가 아니에요. 그런데 네이티브가 아닌 버전에서 쓸 수 있는 어떤 메서드든 호출할 수는 있어요. 무슨 일일까요?
my int8 $x = -42;
say $x.abs; # OUTPUT: «42»
이 동작을 "자동 박싱(auto-boxing)"이라고 해요. 컴파일러가 네이티브 타입을 모든 메서드를 가진 완전한 고수준 타입으로 자동 "박싱"하는 거예요. 즉 위 int8은 자동으로 Int로 변환되고, Int 클래스가 호출된 abs 메서드를 제공한 거예요.
이 세부 사항은 네이티브 타입을 성능 향상에 쓸 때 중요해요. 코드가 자동 박싱을 많이 수행한다면, 네이티브 타입이 비네이티브보다 나쁜 성능을 낼 수 있어요:
my $a = -42;
my int $a-native = -42;
{ for ^1000_000 { $a.abs }; say now - ENTER now } # OUTPUT: «0.38180862»
{ for ^1000_000 { $a-native.abs }; say now - ENTER now } # OUTPUT: «0.938720»
위에서 보듯 네이티브 변형이 두 배 이상 느려요. 이유는 메서드 호출이 네이티브 타입을 박싱해야 하지만, 비네이티브 변형은 그럴 필요가 없어서 성능 손실이 나는 거예요.
이 특정 경우엔, 네이티브 타입을 박싱 없이 다룰 수 있는 abs의 서브루틴 형태로 바꾸면 돼요. 다른 경우에는 코드 일부를 비네이티브 타입으로 바꾸는 것 포함, 과도한 자동 박싱을 피할 다른 해결책을 찾아야 할 수 있어요.
my $a = -42;
my int $a-native = -42;
{ for ^1000_000 { abs $a }; say now - ENTER now } # OUTPUT: «0.38229177»
{ for ^1000_000 { abs $a-native }; say now - ENTER now } # OUTPUT: «0.3088305»
기본값 (Default values)
네이티브 타입 뒤에는 클래스가 없으므로, 초기화되지 않은 변수로 얻는 평범한 타입 객체도 없어요. 그래서 네이티브 타입은 자동으로 0으로 초기화돼요. 6.c 언어에서 네이티브 부동소수 타입(num, num32, num64)은 NaN 값으로 초기화됐고, 6.d 언어에서는 기본값이 0e0이에요.
네이티브 디스패치 (Native dispatch)
크기가 예측 가능할 때 네이티브 후보로 더 빠른 알고리즘을 제공하고, 그 외에는 더 느린 비네이티브 대안으로 폴백하도록, 네이티브 후보를 비네이티브 후보와 함께 두는 게 가능해요. 네이티브 후보가 관련된 다중 디스패치에 대한 규칙은 다음과 같아요.
첫째, 네이티브 타입의 크기는 디스패치에서 역할을 하지 않아서 int8은 int16이나 int과 같게 간주돼요:
multi foo(int $x) { say "int" }
multi foo(int32 $x) { say "int32" }
foo my int $x = 42;
# OUTPUT:
# Ambiguous call to 'foo(Int)'; these signatures all match:
# :(int $x)
# :(int32 $x)
둘째, 루틴이 only(즉 multi가 아님)이고 비네이티브 타입을 받는데 호출 시 네이티브가 주어졌거나 그 반대라면, 호출이 가능하도록 인자가 자동 박싱·언박싱돼요. 주어진 인자가 네이티브 매개변수에 들어가기 너무 크면 예외가 던져져요:
-> int {}( 42 ); # OK; auto-unboxing
-> int {}( 2¹⁰⁰ ); # Too large; exception
-> Int {}( 2¹⁰⁰ ); # OK; non-native parameter
-> Int {}( my int $ = 42 ); # OK; auto-boxing
multi 루틴에 관해서는, 받을 네이티브 후보가 없으면 네이티브 인자는 항상 자동 박싱돼요:
multi foo (Int $x) { $x }
say foo my int $ = 42; # OUTPUT: «42»
반대 방향엔 같은 혜택이 없어요. 네이티브 후보만 있다면 비네이티브 인자는 자동 언박싱되지 않고, 매치되는 후보가 없다는 예외가 대신 던져져요(이 비대칭의 이유는 네이티브는 항상 박싱될 수 있지만 비네이티브는 네이티브에 들어가기 너무 클 수 있기 때문이에요):
multi f(int $x) { $x }
my $x = 2;
say f $x;
# OUTPUT:
# Cannot resolve caller f(Int); none of these signatures match:
# (int $x)
# in block <unit> at -e line 1
하지만 인자 중 하나가 네이티브 타입이고 다른 하나가 수치 리터럴인 호출을 할 때는 이 규칙이 면제돼요:
multi f(int, int) {}
f 42, my int $x; # Successful call
이렇게 하면 예를 들어 $n +> 2를 매번 $n +> (my int $ = 2)라고 쓰지 않아도 돼요. 컴파일러는 리터럴이 네이티브 타입에 들어가기 충분히 작다는 걸 알고 네이티브로 변환해요.
원자 연산 (Atomic operations)
언어는 원자적으로 수행되는 게 보장된 일부 연산을 제공해요. 즉 락 없이, 데이터 경쟁 위험 없이 여러 스레드가 실행해도 안전해요.
그런 연산에는 atomicint 네이티브 타입이 필요해요. 이 타입은 CPU가 제공하는 원자 연산을 수행할 수 있게 크기 조정된 점만 빼면 일반 네이티브 int와 비슷해요. 32비트 CPU에서는 보통 32비트, 64비트 CPU에서는 보통 64비트 크기예요.
# !!WRONG!! Might be non-atomic on some systems
my int $x;
await ^100 .map: { start $x⚛++ };
say $x; # OUTPUT: «98»
# RIGHT! The use of `atomicint` type guarantees operation is atomic
my atomicint $x;
await ^100 .map: { start $x⚛++ };
say $x; # OUTPUT: «100»
다중 디스패치에서 int와 비슷한 점도 있어요: atomicint, 일반 int, 그리고 크기 있는 int 변형 모두 디스패처가 같게 간주해서 다중 디스패치로 구분할 수 없어요.
수치 전염성 (Numeric infectiousness)
수치 "전염성(infectiousness)"은 서로 다른 타입의 두 수치가 어떤 수학 연산에 관여할 때 결과 타입을 결정해요. 피연산자 타입보다 결과가 더 자주 해당 타입이면 그 타입이 다른 타입보다 더 전염성이 강하다고 해요. 예를 들어 Num 타입은 Int보다 전염성이 강해서 42e0 + 42가 결과로 Num을 만들 거라 예상할 수 있어요.
전염성은 다음과 같아요. 가장 전염성이 강한 타입부터 나열했어요:
- Complex
- Num
- FatRat
- Rat
- Int
say (2 + 2e0).^name; # Int + Num => OUTPUT: «Num»
say (½ + ½).^name; # Rat + Rat => OUTPUT: «Rat»
say (FatRat.new(1,2) + ½).^name; # FatRat + Rat => OUTPUT: «FatRat»
Allomorph는 자신의 수치 성분과 같은 전염성을 가져요. 네이티브 타입은 자동 박싱되고 박싱된 변형과 같은 전염성을 가져요.