Ruby 2.4 รวม Fixnum และ Bignum เข้าด้วยกันเป็นคลาสเดียว (Integer) ดังนั้นเป็นเวลาที่ดีในการทบทวนประเภทตัวเลขใน Ruby!
นั่นคือเรื่องที่เราจะพูดถึง 🙂
ภาพรวมประเภทตัวเลข
เริ่มจาก class hierarchy ของคลาสเกี่ยวกับตัวเลขทั้งหมดใน Ruby:
Numeric
Integer
Fixnum
Bignum
Float
Complex
Rational
BigDecimal (Standard Library)
เห็นได้ว่า Numeric เป็น parent ของคลาสตัวเลขทุกตัว จำได้ว่าสามารถใช้ ancestors หา parent classes ของคลาสไหนก็ได้
ตัวอย่าง:
Fixnum.ancestors - Fixnum.included_modules
# [Fixnum, Integer, Numeric, Object, BasicObject]
มา看看ในรูปตาราง:
| Class | คำอธิบาย | ตัวอย่าง |
|---|---|---|
| Integer | Parent class ของ Fixnum และ Bignum |
1 |
| Fixnum | จำนวนเต็มที่เข้ากับ OS integer type (32 หรือ 64 bits) | 1 |
| Bignum | ใช้สำหรับตัวเลขใหญ่กว่า | 111111111111 |
| Float | ทศนิยมที่ไม่แม่นยำ (imprecise) | 5.0 |
| Complex | ใช้คณิตศาสตร์กับจำนวนจินตภาพ | (1+0i) |
| Rational | ใช้แทนเศษส่วน | (2/3) |
| BigDecimal | ทศนิยมที่แม่นยำสมบูรณ์ (perfect precision) | 3.0 |
ความไม่แม่นยำของ Float (Float Imprecision)
คลาส Float ใน Ruby ถูกอธิบายว่า "imprecise" ในเอกสารทางการ
ทำไม?
ตัวอย่าง:
0.2 + 0.1 == 0.3 # false
ทำไมเป็น false?
มาดูผลลัพธ์ของ 0.2 + 0.1:
0.30000000000000004
ตรงนี้! นั่นคือความหมายของ imprecision
เกิดจากวิธีเก็บ float ถ้าต้องการทศนิยมแม่นยำตลอด ใช้ BigDecimal
Float vs BigDecimal
BigDecimal ให้ arbitrary-precision decimal numbers
ตัวอย่าง:
require 'bigdecimal'
BigDecimal("0.2") + BigDecimal("0.1") == 0.3 # true
ทำไมไม่ใช้ BigDecimal ตลอด? เพราะมัน ช้ามาก!
Benchmark:
Calculating -------------------------------------
bigdecimal 21.559k i/100ms
float 79.336k i/100ms
-------------------------------------------------
bigdecimal 311.721k (± 7.4%) i/s - 1.552M
float 3.817M (±11.7%) i/s - 18.803M
Comparison:
float: 3817207.2 i/s
bigdecimal: 311721.2 i/s - 12.25x slower
BigDecimal ช้ากว่า Float 12 เท่า นั่นคือเหตุผลที่มันไม่ใช่ค่าเริ่มต้น 🙂
Fixnum vs Bignum
ในส่วนนี้ ผมอยากสำรวจความต่างระหว่าง Fixnum และ Bignum ก่อน Ruby 2.4
เริ่มจากโค้ด:
1.class # Fixnum
100000000000.class # Bignum
Ruby สร้างคลาสที่ถูกต้องให้อัตโนมัติ และจะ promote Fixnum เป็น Bignum เมื่อจำเป็น
หมายเหตุ: อาจต้องใช้ตัวเลขใหญ่กว่านี้เพื่อให้ได้
Bignumobject ถ้าคุณใช้ Ruby interpreter 64-bit
ทำไมต้องมีคลาสต่างกัน? เพราะทำงานกับตัวเลขใหญ่ต้องการ implementation ต่างกัน และช้ากว่า คล้ายกับ Float vs BigDecimal
คุณสมบัติพิเศษของ Fixnum
คลาส Fixnum มีคุณสมบัติพิเศษ เช่น object id คำนวณจากสูตร
1.object_id # 3
20.object_id # 41
สูตรคือ: (number * 2) + 1
แต่มากกว่านั้น ตอนใช้ Fixnum ไม่มี object ถูกสร้างเลย ไม่มี data ที่ต้องเก็บใน Fixnum เพราะค่า derive จาก object id เอง
เป็นแค่ implementation detail แต่น่าสนใจ 🙂
MRI (Matz's Ruby Interpreter) ใช้ macro สองตัวแปลงระหว่าง value และ object id:
INT2FIX(i) ((VALUE)(((SIGNED_VALUE)(i))<<1 | FIXNUM_FLAG))
FIX2LONG(x) ((long)RSHIFT((SIGNED_VALUE)(x),1))
สิ่งที่เกิดขึ้นคือ "bit shifting" ย้ายบิตซ้ายหรือขวา
Shift ซ้ายหนึ่งตำแหน่ง = คูณ 2 นั่นคือเหตุผลสูตรคือ (number * 2) + 1 +1 มาจาก FIXNUM_FLAG
ในทางกลับกัน Bignum ทำงานเหมือนคลาสปกติ ใช้ object ids ปกติ:
111111111111111.object_id # 23885808
ทั้งหมดหมายถึง Fixnum objects ใกล้ symbols ในระดับ interpreter ส่วน Bignum ใกล้ strings
Integers ใน 2.4
ตั้งแต่ Ruby 2.4 Fixnum และ Bignum deprecated แต่เบื้องหลังยังทำงานเหมือนเดิม
Ruby สลับระหว่าง type อัตโนมัติ
โดยไม่เปลี่ยนคลาส
หมายถึง Integer ตัวเล็กยังทำงานเหมือน Fixnum เดิม
สรุป
ในบทความนี้ คุณเรียนรู้คลาสเกี่ยวกับตัวเลขใน Ruby
เรียนรู้ว่า floats ไม่แม่นยำ และใช้ BigDecimal ถ้าความแม่นยำสำคัญกว่า performance ยังเรียนรู้ว่า Fixnum objects พิเศษระดับ interpreter แต Bignum เป็น regular objects
ถ้าชอบบทความนี้ อย่าลืมสมัครรับข่าวสารในฟอร์มด้านล่าง 🙂