Generic programming ใน Ada — เขียนสัญญาด้วยชนิดข้อมูล เพื่อนำกลับมาใช้ใหม่แบบ zero-cost
· อัปเดตเมื่อ: · Go Komura · Ada, Programming Language, Generics, Type System, Static Typing, Contract Model, Zero Cost Abstraction, GNAT, Alire, ความน่าเชื่อถือสูง, การนำโค้ดกลับมาใช้ใหม่
1. บทนำ — ไม่ใช่ “รับอะไรก็ได้” แต่เป็น “สัญญาว่าจะรับอะไร”
เมื่อจะเขียนโค้ดที่นำกลับมาใช้ใหม่ได้ในภาษาแบบ static typing มักเจอปัญหาเดิมทันที สแตกที่เขียนไว้สำหรับจำนวนเต็มอยากใช้กับสตริงด้วย อาร์เรย์ทศนิยมลอยตัวก็อยากใช้สถิติชุดเดียวกัน ตรรกะเรียงน้อยไปมากก็อยากใช้เรียงมากไปน้อยด้วย แต่ถ้าคัดลอกโค้ดเดิมทีละชนิด การแก้ครั้งหลังมักตกหล่น ในทางกลับกัน ถ้าออกแบบให้รับอะไรก็ได้ด้วย void* หรือ cast type safety ก็พัง
คำตอบของ Ada คือ generic (generic units)
เจเนอริกของ Ada ไม่ใช่การแทนที่ข้อความล้วน ๆ มันรับชนิดข้อมูล ค่า ซับโปรแกรม และแม้แต่ package ทั้งก้อนเป็น formal parameter แล้วตรวจชนิดแบบสแตติกตอน instantiate นั่นคือไม่ไปตรวจตอนรันว่าชนิดนี้ใช้ได้จริงหรือไม่ แต่ยืนยันตอนคอมไพล์ว่าชิ้นส่วนนี้ทำตามสัญญาหรือไม่
flowchart LR
accTitle: ทางเลือกเมื่อจะนำโค้ดกลับมาใช้ใหม่
accDescr: เปรียบเทียบการคัดลอกวาง การรับผ่าน void* หรือ cast กับ generic ของ Ada ที่ type-safe ตรวจตอนคอมไพล์ และไม่มี dispatch ส่วนเกินตอนรัน
A[ตรรกะที่อยากนำกลับมาใช้ใหม่] --> B{จะนำกลับมาใช้ใหม่อย่างไร}
B --> C[คัดลอกแล้ววาง]
B --> D[void* / Object / cast]
B --> E[generic ของ Ada]
C --> C1[พลาดแก้ไม่ครบได้ง่าย]
D --> D1[ผิดพลาดตอนรันหรือเสีย type safety ได้ง่าย]
E --> E1[type-safe]
E --> E2[ตรวจตอนคอมไพล์]
E --> E3[ไม่มี dispatch ส่วนเกินตอนรัน]
บทความนี้จัดระเบียบ generic programming ของ Ada ตามลำดับนี้
- generic subprogram
- generic package
- type parameter, value parameter, subprogram parameter
- type category อย่าง
private,range <>,digits <> - ตัวอย่างอิมพลีเมนต์: เรียงลำดับ, สแตก, สถิติ,
Count_If, key-value store - higher-order generic ผ่าน formal package parameter
- contract model ของ Ada และแนวคิดออกแบบในงานจริง
1.1 กลุ่มผู้อ่าน และสิ่งที่นำกลับไปใช้ได้จากบทความนี้
บทความนี้เขียนโดยสมมติผู้อ่านกลุ่มนี้
- ผู้ที่เคยใช้ template ของ C++, generic ของ C# หรือ Java หรือ generic ของ Rust อย่างใดอย่างหนึ่ง
- ผู้ที่ยังไม่คล่องไวยากรณ์ Ada แต่สนใจแนวคิดออกแบบแบบ “เขียนสัญญาด้วยชนิดข้อมูล”
- ผู้ที่กำลังพิจารณานโยบายออกแบบชิ้นส่วนนำกลับมาใช้ใหม่ ในงานบำรุงระยะยาว งานฝังตัว และงานความน่าเชื่อถือสูง
ในด้านไวยากรณ์ Ada ถ้าคุ้น package (แยก specification .ads กับ body .adb) และ parameter mode in / out / in out ก็อ่านต่อได้ ถ้าสองอย่างนี้ยังไม่ชัวร์ อ่าน “เสน่ห์ของภาษา Ada” ก่อนจะเข้าใจเร็วขึ้น
สิ่งที่นำกลับไปใช้ได้ไม่ใช่ไวยากรณ์ Ada ล้วน ๆ แต่เป็นวินัยออกแบบที่ว่า ชิ้นส่วนที่นำกลับมาใช้ใหม่ได้เรียกร้องอะไร ให้เขียนเป็นชนิดข้อมูลก่อนลงมืออิมพลีเมนต์ แนวคิดนี้เอาไปใช้ตอนตัดสินใจว่าจะเขียน interface constraint ของ C# หรือ concepts ของ C++20 แค่ไหนก็ได้โดยตรง
1.2 คู่มือการอ่าน — ไม่จำเป็นต้องอ่านทั้งหมด
บทความนี้มี 20 บท ไม่ต้องอ่านไล่ทั้งเล่ม ก็เลือกอ่านตามเป้าหมายได้ดังนี้
| เป้าหมาย | บทที่ควรอ่าน |
|---|---|
| อยากจับแนวคิดให้เร็วในเวลาสั้น | บทที่ 4 (โมเดลพื้นฐาน) → บทที่ 6 (generic subprogram ขั้นต่ำ) → บทที่ 13 (contract model) |
| อยากเขียนชิ้นส่วนเอง | บทที่ 4 → บทที่ 6 → บทที่ 7 (generic package) → บทที่ 8 (ฉีดพฤติกรรม) → บทที่ 9 (type category) |
| อยากได้แค่แนวตัดสินใจออกแบบ | บทที่ 13 → บทที่ 14 (จะทำอะไรให้เป็น generic) → บทที่ 15 (จุดที่สะดุด) → บทที่ 17 (เช็กลิสต์) |
| อยากลงมือทำ | สร้างสภาพแวดล้อมในบทที่ 3 แล้วเริ่มจากตัวอย่างจบในบทที่ 6 และ 7 |
เส้นทางสั้นสุดคือ สามบท คือ 4, 6 และ 13 สามบทนี้ครบแกน formal parameter, instantiation และ contract model บทที่ 5 และ 9 เป็นรายการ formal parameter จึงอ่านแบบเปิดพจนานุกรมเมื่อต้องการก็ได้
หัวข้อนี้อยู่ต่อจากซีรีส์ “เสน่ห์ของภาษา Ada” “แนะนำ formal verification ด้วย SPARK” “ภาวะพร้อมกันที่ปลอดภัย” และ “ระบบเรียลไทม์” ของบล็อกนี้ เราขุดแนวคิด “ให้ชนิดข้อมูลพูดการออกแบบ” ของ Ada จากมุม generic
แผนที่ความรู้ของบทความนี้
เจเนอริกของ Ada คือกลไกการนำกลับมาใช้ใหม่ที่รับชนิดข้อมูล ค่า ซับโปรแกรม และ package เป็น formal parameter แล้วตรวจชนิดแบบสแตติกตอน instantiate ด้วย new generic subprogram และ generic package คือสองเสาหลักของกลไกนี้ โดย formal type parameter เขียนสัญญาว่า body ใช้ปฏิบัติการอะไรได้บ้างผ่านหมวดอย่าง private, range <> และ digits <> ฉีดพฤติกรรมอย่างการเปรียบเทียบและ predicate ด้วย formal subprogram parameter และรับ generic package ที่ instantiate แล้วมาเป็นชิ้นส่วนผ่าน formal package parameter ด้วย contract model ที่ตรวจชนิดได้ด้วยตัว body เองภายในขอบเขตของ formal parameter เท่านั้น ปัญหาที่ template ของ C++ มีมาตามประวัติว่าเพิ่งเห็นข้อผิดพลาดตอน instantiate จึงไม่เกิดใน Ada ตั้งแต่ระดับโครงสร้าง ถ้ามี GNAT กับ Alire ก็ทดลองกลไกนี้ได้ในสภาพแวดล้อมฟรี
flowchart LR
accTitle: แผนที่ความรู้ generic programming ของ Ada
accDescr: แผนภาพที่แสดงว่า generic subprogram และ generic package ตั้งอยู่บนสัญญา (contract model) ของ formal parameter ที่เป็นชนิดข้อมูล ค่า ซับโปรแกรม และ package ว่าสัญญาถูกตรวจตอน instantiate ด้วย new และความต่างจากปัญหาข้อผิดพลาดตอน instantiate ที่ template ของ C++ มีมาตามประวัติ
ada_generics["generic ของ Ada (generic unit)"]
ada_generic_contract_model["contract model ของ Ada generic"]
ada["Ada (ภาษาโปรแกรม)"]
ada_generic_subprogram["ซับโปรแกรม generic (Generic Subprogram)"]
ada_generic_package["แพ็กเกจ generic (Generic Package)"]
ada_generic_formal_type["พารามิเตอร์ Formal Type"]
ada_generic_formal_subprogram["พารามิเตอร์ Formal Subprogram"]
ada_generic_formal_package["พารามิเตอร์ Formal Package"]
ada_generic_instantiation["การสร้างอินสแตนซ์ (instantiation)"]
ada_generic_formal_object["พารามิเตอร์ Formal Object (ค่า)"]
late_instantiation_error["ข้อผิดพลาดเทมเพลตที่รู้ได้ตอนสร้างอินสแตนซ์เท่านั้น"]
cpp_templates["เทมเพลต C++"]
gnat["GNAT"]
alire["Alire"]
ada -->|"อิมพลีเมนต์"| ada_generics
ada_generics -->|"ใช้"| ada_generic_subprogram
ada_generics -->|"ใช้"| ada_generic_package
ada_generic_subprogram -.->|"ต้องมี"| ada_generic_formal_type
ada_generics -->|"ใช้"| ada_generic_formal_subprogram
ada_generics -->|"ใช้"| ada_generic_formal_package
ada_generics -->|"ต้องมี"| ada_generic_instantiation
ada_generic_formal_type -->|"กำหนดค่าด้วย"| ada_generic_instantiation
ada_generic_package -.->|"ต้องมี"| ada_generic_formal_type
ada_generic_package -.->|"ต้องมี"| ada_generic_formal_object
ada_generic_formal_subprogram -->|"อิมพลีเมนต์"| ada_generic_contract_model
ada_generic_formal_type -->|"อิมพลีเมนต์"| ada_generic_contract_model
ada_generic_contract_model -->|"ป้องกัน"| late_instantiation_error
cpp_templates -.->|"อาจก่อให้เกิด"| late_instantiation_error
ada_generic_formal_package -->|"ต้องมี"| ada_generic_instantiation
gnat -->|"อิมพลีเมนต์"| ada
gnat -.->|"กำหนดค่าด้วย"| alire
ada_generic_subprogram -.->|"ใช้"| ada_generic_formal_subprogram
ada_generic_package -.->|"ใช้"| ada_generic_formal_subprogram
ada_generic_instantiation -.->|"ต้องมี"| ada_generic_formal_object
ada_generic_formal_object -->|"อิมพลีเมนต์"| ada_generic_contract_model
ในแผนภาพ เส้นทึบหมายถึงความสัมพันธ์ที่ถือเสมอ และเส้นประหมายถึงความสัมพันธ์ที่มีเงื่อนไข (เงื่อนไขอยู่ในการอธิบายของแต่ละความสัมพันธ์ในหน้าละเอียด) รายการความสัมพันธ์ทั้งหมด (รวม 21 รายการ พร้อมหลักฐานและระดับความเชื่อมั่น) และนิยามของแนวคิดหลักรวบรวมไว้ที่ หน้าละเอียดของแผนที่ความรู้ (เป็นภาษาญี่ปุ่น) ข้อมูล: JSON-LD / Turtle
2. แผนที่ของบทความนี้
ก่อนอื่นจับภาพรวมด้วยแผนภาพ ถ้าเข้าใจเจเนอริกของ Ada แค่ว่า “ฟีเจอร์ที่รับชนิดข้อมูลเป็นอาร์กิวเมนต์” จะแคบเกินไป ในทางปฏิบัติเราประกอบ subprogram, package, subprogram parameter, value parameter และ formal package parameter ตามหน่วยที่อยากนำกลับมาใช้ใหม่
mindmap
root((Ada Generics))
Generic Subprogram
Swap
Count_If
Sort
Generic Package
Stack
Statistics
KV Store
Formal Parameters
Type
private
limited private
range box
mod box
digits box
delta box
discrete box
Object
Max_Size
Threshold
Subprogram
Less function
Equals function
Predicate
Package
with package P is new Generic
แนวคิดออกแบบ
Contract Model
Static Checking
Zero-Cost Abstraction
แยก Specification กับ Body
คำว่า box ในรูปหมายถึง <> (box compound delimiter) ของ Ada เพราะ Mermaid วาด <> ตามตัวไม่ได้ จึงเขียนว่า box เฉพาะในรูป
วิธีอ่านบทความนี้ตรงไปตรงมา ครึ่งแรกตามไวยากรณ์ ครึ่งหลังเป็นการตัดสินใจออกแบบ ถ้าเพิ่งอ่าน Ada เป็นครั้งแรก ไม่ต้องท่องไวยากรณ์ละเอียดในรอบแรก ให้โฟกัสว่า “ใส่อะไรเป็น formal parameter” และ “formal parameter นั้นอนุญาตปฏิบัติการอะไร”
2.1 พจนานุกรมศัพท์สั้น ๆ
คำที่วนซ้ำต่อจากนี้ จัดคู่ไว้ก่อน ในเอกสาร Ada มักเรียก generic ว่า generic unit และบทความนี้ใช้ generic subprogram กับ generic package ซึ่งความหมายเดียวกับเจเนอริก
| คำในบทความนี้ | อังกฤษ | ความหมาย |
|---|---|---|
| generic unit | generic unit | คำประกาศที่ขึ้นต้นด้วย generic ชื่อรวมของ generic subprogram และ generic package |
| formal parameter | generic formal parameter | อาร์กิวเมนต์ฝั่งผู้รับ ที่เขียนระหว่าง generic กับตัวประกาศหลัก เป็นชนิดข้อมูล ค่า ซับโปรแกรม หรือ package |
| formal part | generic formal part | ส่วนที่เรียง formal parameter เอง อ่านแทนได้ว่า “ที่เขียนสัญญา” |
| actual parameter | generic actual parameter | ชนิดข้อมูล ค่า ซับโปรแกรม หรือ package ที่ส่งจริงตอน instantiate |
| instantiation | instantiation | การสร้าง subprogram หรือ package ปกติจาก generic unit ด้วย new |
| box | <> |
ชื่อของสัญลักษณ์ <> ใน Ada เช่น range <> แปลว่าชนิดจริงจะไปกำหนดตอน instantiate |
| contract model | contract model | วิธีของ Ada ที่เขียน body ภายในสัญญาใน formal parameter แล้วตรวจชนิดได้ด้วยตัว body เอง (บทที่ 13) |
ในเนื้อความเราเขียนตามสัญลักษณ์ Ada ว่า range <>, digits <> แต่ ในรูปเท่านั้นที่เขียน range box, digits box เพราะ Mermaid วาดป้ายที่มี <> ตามตัวไม่ได้ ความหมายเดียวกับในเนื้อความ ตามตาราง Ada เองเรียก <> ว่า box ดังนั้นคำในรูปก็ไม่หลุดจากศัพท์ของ Ada
3. สภาพแวดล้อมการรันและวิธีคอมไพล์
โค้ดในบทความนี้สมมติ GNAT 15.x ขึ้นไป GNAT เป็นคอมไพเลอร์ตัวแทนของ Ada ติดตั้งผ่าน Alire ได้ Alire คือ package manager ของ Ada / SPARK และใช้จัดการ toolchain กับ build ได้ด้วย
gnat --version
# GNAT 15.2.1
ติดตั้ง GNAT จาก Alire (package manager ของ Ada) ด้วย alr install gnat_native gprbuild แล้วใส่ PATH
ตัวอย่างในบทความนี้สมมติวางในรีโพสิทอรีดังนี้
flowchart TB
accTitle: โครงไฟล์ตัวอย่าง
accDescr: ตัวอย่างอยู่ใน ada-generic-programming/src/snippets เป็นไฟล์ .ada ทีละหัวข้อ พร้อม README.md ที่ราก
R[ada-generic-programming/] --> S[src/]
S --> N[snippets/]
N --> A[01_swap.ada]
N --> B[02_stack.ada]
N --> C[03_sort.ada]
N --> D[04_statistics.ada]
N --> E[05_filter.ada]
N --> F[06_kv_store.ada]
R --> README[README.md]
ตัวอย่างที่รวมหลายหน่วยคอมไพล์ไว้ในไฟล์เดียว ให้แยกด้วย gnatchop แล้วค่อย gnatmake
mkdir work
cd work
gnatchop ../src/snippets/01_swap.ada
gnatmake -gnata swap_demo
./swap_demo
-gnata เป็นออปชันเปิด assertion ไม่จำเป็นต่อการใช้ generic เอง แต่ในตัวอย่างสำหรับเรียน ช่วยตรวจสัญญาและเงื่อนไขขอบได้ง่ายขึ้น
sequenceDiagram
accTitle: ลำดับ gnatchop แล้ว gnatmake
accDescr: ผู้พัฒนาส่งไฟล์ .ada ไฟล์เดียว ให้ gnatchop แยกเป็น .ads .adb และ main แล้ว gnatmake bind กับ link จนได้ไฟล์ executable
participant Dev as ผู้พัฒนา
participant Chop as gnatchop
participant Build as gnatmake
participant Exe as ไฟล์ executable
Dev->>Chop: ส่งไฟล์ .ada ไฟล์เดียว
Chop-->>Dev: แยกเป็น .ads / .adb / main
Dev->>Build: gnatmake -gnata main
Build-->>Dev: bind และ link จนจบ
Dev->>Exe: ./main
Exe-->>Dev: ผลลัพธ์การรัน
4. โมเดลพื้นฐานของ generic ใน Ada
เจเนอริกของ Ada คิดเป็นสามอย่างนี้จะเข้าใจง่าย
- เขียน generic unit
- เขียน formal parameter ในส่วน
generic - ฝั่งผู้ใช้ instantiate ด้วย
new
flowchart LR
accTitle: โมเดลสามขั้นของ generic ใน Ada
accDescr: จากประกาศ generic ผ่าน formal parameter และ body ไปสู่ instantiation ด้วย new แล้วใช้เป็น subprogram หรือ package ปกติ
G[ประกาศ generic] --> F[formal parameter]
F --> B[generic body]
B --> I[instantiation ด้วย new]
I --> U[ใช้เป็น subprogram หรือ package ปกติ]
F --> F1[ชนิดข้อมูล]
F --> F2[ค่า]
F --> F3[ซับโปรแกรม]
F --> F4[package]
ตัวอย่างเช่น ถ้าทำให้การสลับค่าสองตัวเป็น generic จะใส่แค่ชนิดข้อมูลเป็น formal parameter ได้
generic
type Element is private;
procedure Generic_Swap (A, B : in out Element);
ณ จุดนี้ยังเรียก Generic_Swap ไม่ได้ นี่คือ “แม่แบบการสลับที่ใช้ได้กับชนิด Element ใดก็ได้” เมื่อให้ชนิดจริง จึงจะกลายเป็น procedure ปกติครั้งแรก
procedure Swap_Integer is new Generic_Swap (Integer);
วาดเป็นความสัมพันธ์ได้แบบนี้
flowchart TB
accTitle: Instantiation ของ Generic_Swap ตามชนิด
accDescr: Generic_Swap ที่รับ Element เป็น private ถูก instantiate เป็น Swap ของ Integer Character และ My_Record แล้วสลับตัวแปรชนิดนั้น
Template[Generic_Swap<br/>type Element is private] -->|ส่ง Integer| SwapInt[Swap_Integer]
Template -->|ส่ง Character| SwapChar[Swap_Character]
Template -->|ส่ง My_Record| SwapRecord[Swap_My_Record]
SwapInt --> ICall[สลับตัวแปร Integer]
SwapChar --> CCall[สลับตัวแปร Character]
SwapRecord --> RCall[สลับตัวแปร My_Record]
จุดสำคัญคือ body ของแม่แบบถูกเขียนด้วยปฏิบัติการที่ใช้กับ Element ได้เท่านั้น ถ้าประกาศ type Element is private; ใช้การกำหนดค่าและการเทียบเท่าได้ แต่เปรียบเทียบมากน้อยหรือคำนวณเลขใช้ไม่ได้ นั่นคือตัวประกาศ generic เองบอกว่า “ชิ้นส่วนนี้สมมติอะไรได้บ้าง”
5. ชนิดของ formal parameter — คำศัพท์ของ generic ใน Ada
สิ่งที่เจเนอริกของ Ada รับได้ไม่ใช่แค่ชนิดข้อมูล จุดนี้ต่างจาก generic ทั่วไปของ C# หรือ Java อย่างชัด
flowchart TB
accTitle: ชนิดของ formal parameter
accDescr: Formal parameter ของ Ada มีชนิดข้อมูล ค่าวัตถุ ซับโปรแกรม และ package พร้อมตัวอย่างเช่น private digits box และ with function
P[generic formal parameters] --> T[type parameter]
P --> O[object / value parameter]
P --> S[subprogram parameter]
P --> PKG[package parameter]
T --> T1[type Element is private]
T --> T2[type Index is box]
T --> T3[type Real is digits box]
O --> O1[Max_Size : Positive]
O --> O2[Default_Value : Element]
S --> S1[with function Less...]
S --> S2[with procedure Put ...]
PKG --> P1[with package P is new ...]
คำว่า box ในรูปหมายถึง <> (box compound delimiter) ของ Ada เพราะ Mermaid วาด <> ตามตัวไม่ได้ จึงเขียนว่า box เฉพาะในรูป
จัด formal parameter ที่พบบ่อยเป็นตารางได้ดังนี้
| ชนิด | ตัวอย่าง | ความหมาย |
|---|---|---|
| type parameter | type Element is private; |
รูปแบบพื้นฐานที่รับชนิด definite ที่ไม่ limited ใดก็ได้ |
| limited type parameter | type Element is limited private; |
รับชนิดที่คัดลอกไม่ได้ด้วย |
| discrete type | type Index is (<>); |
ชนิดที่ใช้เป็นดัชนีอาร์เรย์ได้ เช่น จำนวนเต็มหรือ enumeration |
| signed integer type | type Count is range <>; |
สมมติเลขคณิตจำนวนเต็มอย่าง +, -, เปรียบเทียบมากน้อยได้ |
| modular integer type | type Word is mod <>; |
จัดการบิตหรือจำนวนเต็มแบบมอดุลาร์ |
| floating-point type | type Real is digits <>; |
เช่น Float, Long_Float, ชนิดทศนิยมลอยตัวที่ผู้ใช้กำหนด |
| fixed-point type | type Money is delta <>; |
จัดการเลขคณิตทศนิยมคงที่ |
| value parameter | Max_Size : Positive; |
ตรึงขนาดหรือเกณฑ์ต่อ instance |
| subprogram | with function Predicate (...) return Boolean; |
ฉีดพฤติกรรมอย่างฟังก์ชันเปรียบเทียบหรือ predicate |
| package | with package P is new Some_Generic (<>); |
รับ generic package ที่ instantiate แล้วมาเป็นชิ้นส่วน |
เพราะมีคลังคำนี้ Ada จึงเขียนแบบ “รับเฉพาะชนิดที่ทำปฏิบัติการนี้ได้” ได้เป็นธรรมชาติ ไม่ใช่แบบ “รับอะไรก็ได้ แล้วไปทำของอันตรายข้างใน”
6. Generic subprogram — เข้าใจโครงขั้นต่ำด้วย Generic_Swap
ตัวอย่างแรก ดู Generic_Swap ที่สลับตัวแปรสองตัวของชนิดใดก็ได้
6.1 Specification
generic
type Element is private;
procedure Generic_Swap (A, B : in out Element);
ส่วนที่ตามหลัง generic คือ formal parameter ที่นี่รับชนิดชื่อ Element is private แปลว่าเมื่อมองจาก generic body จะไม่รู้ representation ภายในของชนิดนั้น
จากประกาศนี้รู้ได้สองอย่าง
Generic_Swapใช้กับชนิดElementใดก็ได้- ใน body จะไม่อิงโครงสร้างภายในหรือการเปรียบเทียบมากน้อยของ
Element
6.2 Body
procedure Generic_Swap (A, B : in out Element) is
Temp : constant Element := A;
begin
A := B;
B := Temp;
end Generic_Swap;
ใน body นี้ใช้กับ Element แค่การกำหนดค่า ไม่ได้ใช้ A < B หรือ A + B ดังนั้นชนิดที่กำหนดค่าได้ เช่น Integer, Character, record, enumeration จึงใช้ได้อย่างเป็นธรรมชาติ
flowchart LR
accTitle: การสลับค่าด้วยตัวแปรชั่วคราว
accDescr: ก่อนเรียก A เป็น 10 และ B เป็น 20 หลังคัดลอกผ่าน Temp ค่าของ A และ B สลับกัน
subgraph Before[ก่อนเรียก]
A1[A = 10]
B1[B = 20]
end
A1 --> T[Temp = A]
B1 --> A2[A = B]
T --> B2[B = Temp]
subgraph After[หลังเรียก]
A2[A = 20]
B2[B = 10]
end
6.3 Instantiation
ฝั่งผู้ใช้ใช้ new
procedure Swap_Int is new Generic_Swap (Integer);
procedure Swap_Char is new Generic_Swap (Character);
จากนี้ Swap_Int และ Swap_Char เรียกเป็น procedure ปกติได้
with Ada.Text_IO; use Ada.Text_IO;
procedure Swap_Demo is
generic
type Element is private;
procedure Generic_Swap (A, B : in out Element);
procedure Generic_Swap (A, B : in out Element) is
Temp : constant Element := A;
begin
A := B;
B := Temp;
end Generic_Swap;
procedure Swap_Int is new Generic_Swap (Integer);
X : Integer := 10;
Y : Integer := 20;
begin
Put_Line ("Before: X=" & Integer'Image (X) & ", Y=" & Integer'Image (Y));
Swap_Int (X, Y);
Put_Line ("After : X=" & Integer'Image (X) & ", Y=" & Integer'Image (Y));
end Swap_Demo;
ภาพการรันเป็นดังนี้
Before: X= 10, Y= 20
After : X= 20, Y= 10
ส่งตัวแปร Float ให้ Swap_Int (X, Y); ไม่ได้ เพราะ Swap_Int เป็น procedure ปกติที่ instantiate สำหรับ Integer แล้ว ถ้ามองเจเนอริกไม่ใช่ “รูที่อะไรก็ยัดได้” แต่เป็น “กลไกสร้างของจริงที่ปลอดภัยทีละชนิด” จะเข้าใจง่ายขึ้น
7. Generic package — ใช้ชนิดและค่าเป็นพารามิเตอร์
เมื่ออยากนำกลับมาใช้ใหม่ไม่ใช่ซับโปรแกรมเดียว แต่รวมหลายปฏิบัติการกับสถานะภายใน ให้ใช้ generic package ตัวอย่างคลาสสิกคือสแตก
สแตกเปลี่ยนแค่ชนิดสมาชิกกับขนาดสูงสุด ตรรกะพื้นฐานก็เหมือนกัน
flowchart TB
accTitle: Generic_Stack รับชนิดกับขนาด
accDescr: Generic_Stack รับ Element_Type กับ Max_Size แล้วให้ Push Pop Size และถูก instantiate เป็นสแตก Integer Float และสตริง
G[Generic_Stack] --> P1[Element_Type]
G --> P2[Max_Size]
G --> Ops[Push / Pop / Size / Is_Empty / Is_Full]
G --> I1[Int_Stack<br/>Element=Integer<br/>Max_Size=5]
G --> I2[Float_Stack<br/>Element=Float<br/>Max_Size=3]
G --> I3[String_Stack<br/>Element=Unbounded_String<br/>Max_Size=20]
7.1 Specification
generic
type Element_Type is private;
Max_Size : Positive;
package Generic_Stack is
procedure Push (Item : Element_Type);
function Pop return Element_Type;
function Is_Empty return Boolean;
function Is_Full return Boolean;
function Size return Natural;
Stack_Overflow : exception;
Stack_Underflow : exception;
end Generic_Stack;
ที่นี่ใช้ formal parameter สองชนิด
Element_Typeเป็น type parameterMax_Sizeเป็น value parameter
Max_Size เป็น Positive จึง instantiate ด้วยขนาด 0 หรือติดลบไม่ได้ แบบนี้ value parameter ก็ใส่ข้อจำกัดด้วยชนิดได้เช่นกัน
7.2 Body
package body Generic_Stack is
subtype Index_Type is Positive range 1 .. Max_Size;
type Storage_Type is array (Index_Type) of Element_Type;
Data : Storage_Type;
Top : Natural := 0;
procedure Push (Item : Element_Type) is
begin
if Top = Max_Size then
raise Stack_Overflow;
end if;
Top := Top + 1;
Data (Top) := Item;
end Push;
function Pop return Element_Type is
Result : Element_Type;
begin
if Top = 0 then
raise Stack_Underflow;
end if;
Result := Data (Top);
Top := Top - 1;
return Result;
end Pop;
function Is_Empty return Boolean is
begin
return Top = 0;
end Is_Empty;
function Is_Full return Boolean is
begin
return Top = Max_Size;
end Is_Full;
function Size return Natural is
begin
return Top;
end Size;
end Generic_Stack;
สิ่งสำคัญใน package body นี้คือ Data กับ Top ถูกสร้างแยกต่อ instance
package Int_Stack is new Generic_Stack (Integer, 5);
package Float_Stack is new Generic_Stack (Float, 3);
สองตัวนี้มาจากแม่แบบเดียวกัน แต่ไม่แชร์สถานะภายใน
flowchart LR
accTitle: สถานะของ Int_Stack กับ Float_Stack แยกกัน
accDescr: Generic_Stack สร้าง Int_Stack และ Float_Stack แต่ละตัวมี Top และ Data ของตัวเอง ไม่แชร์สถานะ
Template[Generic_Stack] --> IntStack[Int_Stack]
Template --> FloatStack[Float_Stack]
subgraph I[สถานะของ Int_Stack]
ITop[Top]
IData[Data : อาร์เรย์ Integer]
end
subgraph F[สถานะของ Float_Stack]
FTop[Top]
FData[Data : อาร์เรย์ Float]
end
IntStack --> I
FloatStack --> F
7.3 การเปลี่ยนสถานะของสแตก
สแตกดูเป็นเครื่องจักรสถานะจะเข้าใจง่าย
stateDiagram-v2
accTitle: การเปลี่ยนสถานะของสแตก
accDescr: สแตกเคลื่อนระหว่าง Empty NonEmpty และ Full ด้วย Push กับ Pop และไป Overflow หรือ Underflow เมื่อทำนอกสัญญา
[*] --> Empty
Empty --> NonEmpty: Push
NonEmpty --> NonEmpty: Push / Pop
NonEmpty --> Empty: Pop ตัวสุดท้ายออก
NonEmpty --> Full: Push จนถึง Max_Size
Full --> NonEmpty: Pop
Full --> Overflow: Push
Empty --> Underflow: Pop
7.4 ตัวอย่างการใช้งาน
with Ada.Text_IO; use Ada.Text_IO;
procedure Stack_Demo is
generic
type Element_Type is private;
Max_Size : Positive;
package Generic_Stack is
procedure Push (Item : Element_Type);
function Pop return Element_Type;
function Is_Empty return Boolean;
function Is_Full return Boolean;
function Size return Natural;
Stack_Overflow : exception;
Stack_Underflow : exception;
end Generic_Stack;
package body Generic_Stack is
subtype Index_Type is Positive range 1 .. Max_Size;
type Storage_Type is array (Index_Type) of Element_Type;
Data : Storage_Type;
Top : Natural := 0;
procedure Push (Item : Element_Type) is
begin
if Top = Max_Size then
raise Stack_Overflow;
end if;
Top := Top + 1;
Data (Top) := Item;
end Push;
function Pop return Element_Type is
Result : Element_Type;
begin
if Top = 0 then
raise Stack_Underflow;
end if;
Result := Data (Top);
Top := Top - 1;
return Result;
end Pop;
function Is_Empty return Boolean is (Top = 0);
function Is_Full return Boolean is (Top = Max_Size);
function Size return Natural is (Top);
end Generic_Stack;
package Int_Stack is new Generic_Stack (Integer, 5);
begin
Int_Stack.Push (10);
Int_Stack.Push (20);
Int_Stack.Push (30);
Put_Line ("Size=" & Natural'Image (Int_Stack.Size));
Put_Line ("Pop =" & Integer'Image (Int_Stack.Pop));
Put_Line ("Pop =" & Integer'Image (Int_Stack.Pop));
Put_Line ("Size=" & Natural'Image (Int_Stack.Size));
end Stack_Demo;
build แล้วรันตามขั้นตอนเดียวกับบทที่ 3
gnatchop ../src/snippets/02_stack.ada
gnatmake -gnata stack_demo
./stack_demo
Size= 3
Pop = 30
Pop = 20
Size= 1
ช่องว่างหนึ่งตัวทางขวาของ = มาจากสเปกที่ 'Image ของชนิดจำนวนเต็มใส่ช่องว่างหนึ่งตัวหน้าค่าที่ไม่ติดลบ Push สามครั้ง Pop สองครั้ง ดังนั้น Size สุดท้ายเป็น 1 อนึ่ง ถ้าสแตกเต็ม (ในตัวอย่างนี้ 5 ช่องตาม Max_Size) แล้วยัง Push จะ raise Int_Stack.Stack_Overflow ถ้าว่างแล้ว Pop จะ raise Int_Stack.Stack_Underflow
Generic package ในงานจริงมักได้ผลกับ “คอนเทนเนอร์เล็ก” “บัฟเฟอร์ความยาวคงที่” “ring buffer” “คิว log” “hardware abstraction layer” โดยเฉพาะใน Ada การตรึงขนาดเป็นชนิดหรือ value parameter แบบสแตติก มักเข้ากับระบบความน่าเชื่อถือสูงกว่าการขยับขนาดตอนรัน
8. Formal subprogram parameter — ฉีดพฤติกรรมเข้าไป
รับแค่ชนิดข้อมูลยังมีสิ่งที่แสดงไม่ได้ เช่น การเรียงลำดับต้องการไม่ใช่แค่ชนิดสมาชิก แต่ยังต้องการตรรกะเปรียบเทียบว่าอะไรมาก่อน
ใน Ada ใส่ฟังก์ชันเปรียบเทียบนี้เป็น formal parameter ของ generic ได้
flowchart LR
accTitle: ฉีดฟังก์ชันเปรียบเทียบเข้า Generic_Insertion_Sort
accDescr: Generic_Insertion_Sort รับชนิดสมาชิก ดัชนี อาร์เรย์ และฟังก์ชันเปรียบเทียบ เพื่อเรียงน้อยไปมาก มากไปน้อย หรือลำดับที่กำหนดเอง
A[Generic_Insertion_Sort] --> T[Item_Type]
A --> I[Index]
A --> ARR[Item_Array]
A --> CMP[ฟังก์ชันเปรียบเทียบ]
CMP --> ASC[ใช้การเปรียบเทียบมาตรฐาน]
CMP --> DESC[ส่ง Greater เพื่อเรียงมากไปน้อย]
CMP --> CUSTOM[ส่งลำดับที่กำหนดเอง]
8.1 Specification
generic
type Item_Type is private;
type Index is (<>);
type Item_Array is array (Index range <>) of Item_Type;
with function "<" (Left, Right : Item_Type) return Boolean is <>;
procedure Generic_Insertion_Sort (Items : in out Item_Array);
ที่นี่มี formal parameter สี่ตัว
Item_Type: ชนิดสมาชิกของอาร์เรย์Index: ชนิดดัชนีของอาร์เรย์Item_Array: ชนิดอาร์เรย์จริง"<": ฟังก์ชันเปรียบเทียบ
type Index is (<>); รับ discrete type ได้ทั้งชนิดจำนวนเต็มและ enumeration ไม่ได้จำกัดดัชนีอาร์เรย์แค่ Positive แต่ใช้ enumeration อย่าง Day ได้ นี่คือจุดที่เป็นแบบ Ada
is <> ใน with function "<" ... is <>; แปลว่าถ้าละ actual parameter จะใช้ตัวดำเนินการมาตรฐานที่มองเห็นได้ หรือฟังก์ชันที่เข้ากัน ดังนั้นชนิดอย่าง Integer ที่มี < อยู่แล้ว ไม่ต้องระบุฟังก์ชันเปรียบเทียบก็ใช้ได้
8.2 Body
procedure Generic_Insertion_Sort (Items : in out Item_Array) is
J : Index;
Key : Item_Type;
begin
if Items'Length <= 1 then
return;
end if;
for I in Index'Succ (Items'First) .. Items'Last loop
Key := Items (I);
J := I;
while J > Items'First and then Key < Items (Index'Pred (J)) loop
Items (J) := Items (Index'Pred (J));
J := Index'Pred (J);
end loop;
Items (J) := Key;
end loop;
end Generic_Insertion_Sort;
insertion sort ไม่เหมาะกับอาร์เรย์ใหญ่ แต่เหมาะกับคำอธิบาย generic เพราะแค่สลับฟังก์ชันเปรียบเทียบ ก็ใช้โครงลูปเดียวกันได้ทั้งเรียงน้อยไปมากและมากไปน้อย
flowchart TB
accTitle: ขั้นตอน insertion sort
accDescr: หยิบ Key จากซ้าย เลื่อนสมาชิกก่อนหน้าไปทางขวาจนกว่าจะเจอที่แทรก แล้วทำซ้ำจนอาร์เรย์เรียงแล้ว
Start[อาร์เรย์ยังไม่เรียง] --> Pick[หยิบ Key จากซ้ายไปขวา]
Pick --> Compare{Key มาก่อนสมาชิกก่อนหน้าหรือไม่? }
Compare -->|Yes| Shift[เลื่อนสมาชิกก่อนหน้าไปทางขวา]
Shift --> Compare
Compare -->|No| Insert[แทรก Key]
Insert --> Done{ทำจนตัวสุดท้ายแล้วหรือยัง?}
Done -->|No| Pick
Done -->|Yes| End[อาร์เรย์เรียงแล้ว]
8.3 สร้างทั้งเรียงน้อยไปมากและมากไปน้อยจาก body เดียวกัน
type Int_Array is array (Positive range <>) of Integer;
procedure Sort_Asc is new Generic_Insertion_Sort
(Item_Type => Integer,
Index => Positive,
Item_Array => Int_Array);
function Greater (Left, Right : Integer) return Boolean is
(Left > Right);
procedure Sort_Desc is new Generic_Insertion_Sort
(Item_Type => Integer,
Index => Positive,
Item_Array => Int_Array,
"<" => Greater);
Sort_Asc ใช้ < มาตรฐาน ส่วน Sort_Desc สลับฟังก์ชันเปรียบเทียบด้วย "<" => Greater
flowchart LR
accTitle: เรียงน้อยไปมากและมากไปน้อยจาก body เดียวกัน
accDescr: ข้อมูลชุดเดียวกันถูก Sort_Asc ด้วยการเปรียบเทียบมาตรฐาน และ Sort_Desc ด้วย Greater ได้ลำดับกลับกัน
Data[99, 3, 47, 12] --> A[Sort_Asc<br/>การเปรียบเทียบมาตรฐาน]
Data --> D[Sort_Desc<br/>ส่ง Greater เป็นฟังก์ชันเปรียบเทียบ]
A --> AO[3, 12, 47, 99]
D --> DO[99, 47, 12, 3]
กลไกนี้ใกล้กับการส่ง comparison function object เป็น template argument ใน C++ หรือการขอลำดับด้วย trait bound ใน Rust แต่ใน Ada เขียนชัดว่า “ส่งฟังก์ชันรูปนี้” เป็น formal subprogram parameter
8.4 ตัวอย่างรัน
ถ้าวาง Generic_Insertion_Sort เป็นไฟล์แยก (generic_insertion_sort.ads / .adb) ตามโครงบทที่ 18 ฝั่งที่เรียกจะเป็นแบบนี้
with Ada.Text_IO; use Ada.Text_IO;
with Generic_Insertion_Sort;
procedure Sort_Demo is
type Int_Array is array (Positive range <>) of Integer;
function Greater (Left, Right : Integer) return Boolean is
(Left > Right);
procedure Sort_Asc is new Generic_Insertion_Sort
(Item_Type => Integer,
Index => Positive,
Item_Array => Int_Array);
procedure Sort_Desc is new Generic_Insertion_Sort
(Item_Type => Integer,
Index => Positive,
Item_Array => Int_Array,
"<" => Greater);
procedure Show (Label : String; Items : Int_Array) is
begin
Put (Label);
for V of Items loop
Put (Integer'Image (V));
end loop;
New_Line;
end Show;
Asc : Int_Array := (99, 3, 47, 12);
Desc : Int_Array := (99, 3, 47, 12);
begin
Sort_Asc (Asc);
Sort_Desc (Desc);
Show ("Asc :", Asc);
Show ("Desc:", Desc);
end Sort_Demo;
gnatchop ../src/snippets/03_sort.ada
gnatmake -gnata sort_demo
./sort_demo
Asc : 3 12 47 99
Desc: 99 47 12 3
ยืนยันได้ว่า procedure สองตัวที่สร้างจาก body ของ Generic_Insertion_Sort เดียวกัน กลับลำดับกันได้แค่สลับฟังก์ชันเปรียบเทียบ ช่องว่างหน้าสมาชิกมาจาก 'Image ของชนิดจำนวนเต็มที่ใส่ช่องว่างหนึ่งตัวหน้าค่าที่ไม่ติดลบ
9. Type category — เขียนสัญญาที่เจาะจงกว่า private
type T is private; สะดวก แต่ทำอะไรก็ได้ไม่ใช่ กับชนิด private จะเอาเลขคณิตสี่อย่างหรือเปรียบเทียบมากน้อยมาใช้โดยอัตโนมัติไม่ได้ Ada จึงระบุหมวดให้ formal type parameter ได้
flowchart TB
accTitle: หมวดของ formal type
accDescr: Formal type แยกเป็น private limited private discrete range mod digits delta และ access พร้อมตัวอย่างชนิดที่เข้าแต่ละหมวด
FormalType[Formal Type] --> Private[private]
FormalType --> Limited[limited private]
FormalType --> Discrete[discrete box: discrete type]
FormalType --> Signed[range box: signed integer]
FormalType --> Modular[mod box: modular integer]
FormalType --> Float[digits box: floating-point]
FormalType --> Fixed[delta box: fixed-point]
FormalType --> Access[ชนิด access]
Discrete --> Enum[enumeration]
Discrete --> Int[ชนิดจำนวนเต็ม]
Float --> F1[Float]
Float --> F2[Long_Float]
Float --> F3[ชนิดทศนิยมลอยตัวที่ผู้ใช้กำหนด]
คำว่า box ในรูปหมายถึง <> (box compound delimiter) ของ Ada เพราะ Mermaid วาด <> ตามตัวไม่ได้ จึงเขียนว่า box เฉพาะในรูป
9.1 ระบุหมวดแล้วได้อะไร
เช่น การคำนวณค่าเฉลี่ยและความแปรปรวนต้องการบวก ลบ คูณ หาร ชนิด private สมมติปฏิบัติการเหล่านี้ไม่ได้ จึงจำกัดเป็นชนิดทศนิยมลอยตัว
generic
type Real is digits <>;
type Real_Array is array (Positive range <>) of Real;
package Generic_Statistics is
function Mean (Values : Real_Array) return Real;
function Variance (Values : Real_Array) return Real;
end Generic_Statistics;
type Real is digits <>; ทำให้รู้ว่า Real เป็นชนิดทศนิยมลอยตัว ดังนั้นใน generic body ใช้ +, -, *, / ได้
9.2 Body
package body Generic_Statistics is
function Mean (Values : Real_Array) return Real is
Sum : Real := 0.0;
begin
if Values'Length = 0 then
return 0.0;
end if;
for V of Values loop
Sum := Sum + V;
end loop;
return Sum / Real (Values'Length);
end Mean;
function Variance (Values : Real_Array) return Real is
M : constant Real := Mean (Values);
Sum : Real := 0.0;
begin
if Values'Length = 0 then
return 0.0;
end if;
for V of Values loop
declare
D : constant Real := V - M;
begin
Sum := Sum + D * D;
end;
end loop;
return Sum / Real (Values'Length);
end Variance;
end Generic_Statistics;
9.3 ใช้กับ Float และ Long_Float
type Float_Array is array (Positive range <>) of Float;
type Long_Array is array (Positive range <>) of Long_Float;
package Float_Stats is new Generic_Statistics (Float, Float_Array);
package Long_Stats is new Generic_Statistics (Long_Float, Long_Array);
สถิติชุดเดียวกันนำกลับมาใช้ใหม่กับชนิดทศนิยมลอยตัวที่ความละเอียดต่างกันได้
flowchart LR
accTitle: Generic_Statistics ใช้กับความละเอียดต่างกัน
accDescr: Generic_Statistics ที่ Real เป็น digits box ถูก instantiate เป็น Float_Stats Long_Stats และ My_Real_Stats เพื่อคำนวณ Mean กับ Variance
Stats[Generic_Statistics<br/>Real is digits box] --> FS[Float_Stats]
Stats --> LS[Long_Stats]
Stats --> MS[My_Real_Stats]
FS --> FCalc[Mean / Variance ด้วย Float]
LS --> LCalc[Mean / Variance ด้วย Long_Float]
MS --> MCalc[Mean / Variance ด้วย Real ที่ผู้ใช้กำหนด]
9.4 ตัวอย่างรัน
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Float_Text_IO; use Ada.Float_Text_IO;
with Generic_Statistics;
procedure Statistics_Demo is
type Float_Array is array (Positive range <>) of Float;
package Float_Stats is new Generic_Statistics (Float, Float_Array);
Samples : constant Float_Array := (1.0, 2.0, 3.0, 4.0);
begin
Put ("Mean = ");
Put (Float_Stats.Mean (Samples), Fore => 1, Aft => 3, Exp => 0);
New_Line;
Put ("Variance = ");
Put (Float_Stats.Variance (Samples), Fore => 1, Aft => 3, Exp => 0);
New_Line;
end Statistics_Demo;
gnatchop ../src/snippets/04_statistics.ada
gnatmake -gnata statistics_demo
./statistics_demo
Mean = 2.500
Variance = 1.250
Ada.Float_Text_IO คือไลบรารีมาตรฐานที่ instantiate Ada.Text_IO.Float_IO สำหรับ Float ระบุ Exp => 0 จะได้ทศนิยมปกติไม่ใช่รูปเลขชี้กำลัง Aft => 3 คือจำนวนหลักส่วนทศนิยม โปรดทราบว่า Variance นี้เป็นความแปรปรวนประชากร (หารด้วย Values'Length) ไม่ใช่ความแปรปรวนตัวอย่าง
9.5 การระบุหมวดคือ “สเปกระดับชนิดข้อมูล”
การระบุหมวดไม่ใช่แค่ไวยากรณ์เพื่อให้คอมไพเลอร์เงียบ แต่เป็นสเปกที่บอกผู้อ่านด้วยว่าชิ้นส่วนนี้เรียกร้องอะไร
| สิ่งที่อยากทำ | formal type ที่เหมาะ | เหตุผล |
|---|---|---|
| สลับ เก็บ ดึงออก | private |
กำหนดค่าได้ก็พอ |
| จัดการทรัพยากรที่คัดลอกไม่ได้ | limited private |
ไม่สมมติการกำหนดค่า |
| ดัชนีอาร์เรย์ การไล่สถานะ enumeration | (<>) |
ใช้ First, Last, Succ, Pred ได้ |
| ผลรวมจำนวนเต็ม ตัวนับ | range <> |
สมมติเลขคณิตจำนวนเต็มได้ |
| บิตมาสก์ ตัวนับวน | mod <> |
สมมติเลขคณิตมอดุลาร์ได้ |
| ค่าเฉลี่ย ความแปรปรวน คำนวณเชิงตัวเลข | digits <> |
สมมติเลขคณิตทศนิยมลอยตัวได้ |
| จำนวนเงิน ปริมาณควบคุมที่ความละเอียดคงที่ | delta <> |
สมมติเลขคณิตทศนิยมคงที่ได้ |
10. การฉีด predicate — เขียน Count_If แบบ Ada
Formal subprogram parameter ใช้ได้ไม่แค่ฟังก์ชันเปรียบเทียบ แต่ใช้กับ predicate ได้ด้วย Predicate คือฟังก์ชันที่รับค่าแล้วคืน Boolean
บทบาทใกล้ Func<T, bool> ของ C#, Predicate<T> ของ Java, แลมบ์ดาหรือ function object ของ C++ ใน Ada แสดงเป็น formal subprogram ของ generic ได้
10.1 Specification
generic
type Element is private;
type Index is (<>);
type Array_Type is array (Index range <>) of Element;
with function Predicate (Item : Element) return Boolean;
function Generic_Count_If (Arr : Array_Type) return Natural;
ที่นี่ไม่ได้ใส่ is <> ให้ Predicate เพราะไม่ได้มีฟังก์ชัน predicate มาตรฐานที่มองเห็นได้อยู่แล้ว จึงออกแบบให้ฝั่งผู้ใช้ต้องส่งมาเสมอ
10.2 Body
function Generic_Count_If (Arr : Array_Type) return Natural is
Count : Natural := 0;
begin
for Item of Arr loop
if Predicate (Item) then
Count := Count + 1;
end if;
end loop;
return Count;
end Generic_Count_If;
ลำดับการทำงานตรงไปตรงมา
flowchart LR
accTitle: การฉีด predicate เข้า Generic_Count_If
accDescr: ไล่สมาชิกอาร์เรย์ ถ้า Predicate เป็นจริงเพิ่ม Count แล้วคืน Count
Arr[อาร์เรย์] --> Loop[ไล่แต่ละสมาชิก]
Loop --> P{"Predicate(Item)?"}
P -->|True| Inc[เพิ่ม Count]
P -->|False| Skip[ไม่ทำอะไร]
Inc --> Next[ไปสมาชิกถัดไป]
Skip --> Next
Next --> Result[คืน Count]
10.3 นับเลขคู่และนับตามเกณฑ์
type Int_Array is array (Positive range <>) of Integer;
function Is_Even (N : Integer) return Boolean is
(N mod 2 = 0);
function Is_Large (N : Integer) return Boolean is
(N > 50);
function Count_Even is new Generic_Count_If
(Element => Integer,
Index => Positive,
Array_Type => Int_Array,
Predicate => Is_Even);
function Count_Large is new Generic_Count_If
(Element => Integer,
Index => Positive,
Array_Type => Int_Array,
Predicate => Is_Large);
จากตรรกะไล่ชุดเดียวกัน สร้างฟังก์ชันสองตัวที่ต่างกันแค่เงื่อนไขได้
flowchart TB
accTitle: นับเลขคู่กับนับเกินเกณฑ์จากตัวเดียวกัน
accDescr: Generic_Count_If ถูก instantiate เป็น Count_Even ด้วย Is_Even และ Count_Large ด้วย Is_Large บนข้อมูลชุดเดียวกัน
G[Generic_Count_If] --> E[Count_Even<br/>Predicate = Is_Even]
G --> L[Count_Large<br/>Predicate = Is_Large]
Data[12, 7, 88, 3, 56, 91, 44, 19, 62] --> E
Data --> L
E --> ER[จำนวนเลขคู่]
L --> LR[จำนวนที่มากกว่า 50]
ในตัวอย่างนี้ การไล่อาร์เรย์ การจัดการตัวนับ และการคืนผล เป็นของร่วมทั้งหมด ส่วนที่ฉีดเป็นฟังก์ชันมีแค่ “จะนับอะไร” นี่คือรูปพื้นฐานของการออกแบบเชิง higher-order ใน Ada
10.4 ตัวอย่างรัน
with Ada.Text_IO; use Ada.Text_IO;
with Generic_Count_If;
procedure Count_If_Demo is
type Int_Array is array (Positive range <>) of Integer;
function Is_Even (N : Integer) return Boolean is
(N mod 2 = 0);
function Is_Large (N : Integer) return Boolean is
(N > 50);
function Count_Even is new Generic_Count_If
(Element => Integer,
Index => Positive,
Array_Type => Int_Array,
Predicate => Is_Even);
function Count_Large is new Generic_Count_If
(Element => Integer,
Index => Positive,
Array_Type => Int_Array,
Predicate => Is_Large);
Data : constant Int_Array := (12, 7, 88, 3, 56, 91, 44, 19, 62);
begin
Put_Line ("Even =" & Natural'Image (Count_Even (Data)));
Put_Line ("Large =" & Natural'Image (Count_Large (Data)));
end Count_If_Demo;
gnatchop ../src/snippets/05_filter.ada
gnatmake -gnata count_if_demo
./count_if_demo
Even = 5
Large = 4
จาก 9 สมาชิกของ Data เลขคู่คือ 12, 88, 56, 44, 62 รวม 5 ตัว ที่มากกว่า 50 คือ 88, 56, 91, 62 รวม 4 ตัว
11. การประกอบหลายพารามิเตอร์ — key-value store ทั่วไป
ชิ้นส่วนในโลกจริงมักไม่จบที่ type parameter ตัวเดียว ต้องประกอบเงื่อนไขหลายอย่าง เช่น ชนิดของคีย์กับค่า วิธีเทียบคีย์ จำนวนรายการสูงสุด
ที่นี่ใช้ key-value store แบบความยาวคงที่ง่าย ๆ เป็นตัวอย่าง
flowchart TB
accTitle: Generic_KV_Store ประกอบหลายพารามิเตอร์
accDescr: Generic_KV_Store รับชนิดคีย์ ชนิดค่า ฟังก์ชันเทียบคีย์ และ Max_Entries แล้วให้ Put Get Contains สำหรับ settings store แคชเล็ก และพจนานุกรมความยาวคงที่งานฝังตัว
KV[Generic_KV_Store] --> K[Key_Type]
KV --> V[Value_Type]
KV --> EQ[ฟังก์ชันเทียบคีย์]
KV --> M[Max_Entries]
KV --> Ops[Put / Get / Contains]
Ops --> Use1[settings store]
Ops --> Use2[แคชขนาดเล็ก]
Ops --> Use3[พจนานุกรมความยาวคงที่งานฝังตัว]
11.1 Specification
generic
type Key_Type is private;
type Value_Type is private;
with function "=" (Left, Right : Key_Type) return Boolean is <>;
Max_Entries : Positive := 50;
package Generic_KV_Store is
procedure Put (Key : Key_Type; Val : Value_Type);
function Get (Key : Key_Type) return Value_Type;
function Contains (Key : Key_Type) return Boolean;
Key_Not_Found : exception;
Store_Full : exception;
end Generic_KV_Store;
Package นี้มี formal parameter สี่ตัว
| พารามิเตอร์ | ชนิด | บทบาท |
|---|---|---|
Key_Type |
ชนิดข้อมูล | ชนิดของคีย์ |
Value_Type |
ชนิดข้อมูล | ชนิดของค่า |
"=" |
ซับโปรแกรม | การตัดสินว่าคีย์ตรงกัน |
Max_Entries |
ค่า | จำนวนรายการสูงสุด |
Max_Entries มีค่าเริ่มต้น := 50 ดังนั้นถ้าไม่ระบุ จะได้ที่เก็บ 50 รายการ
11.2 Body
package body Generic_KV_Store is
subtype Index_Type is Positive range 1 .. Max_Entries;
type Key_Array is array (Index_Type) of Key_Type;
type Value_Array is array (Index_Type) of Value_Type;
type Used_Array is array (Index_Type) of Boolean;
Keys : Key_Array;
Values : Value_Array;
Used : Used_Array := (others => False);
function Find_Index (Key : Key_Type) return Natural is
begin
for I in Index_Type loop
if Used (I) and then Keys (I) = Key then
return I;
end if;
end loop;
return 0;
end Find_Index;
function Find_Free return Natural is
begin
for I in Index_Type loop
if not Used (I) then
return I;
end if;
end loop;
return 0;
end Find_Free;
procedure Put (Key : Key_Type; Val : Value_Type) is
Pos : Natural := Find_Index (Key);
begin
if Pos = 0 then
Pos := Find_Free;
if Pos = 0 then
raise Store_Full;
end if;
Used (Pos) := True;
Keys (Pos) := Key;
end if;
Values (Pos) := Val;
end Put;
function Get (Key : Key_Type) return Value_Type is
Pos : constant Natural := Find_Index (Key);
begin
if Pos = 0 then
raise Key_Not_Found;
end if;
return Values (Pos);
end Get;
function Contains (Key : Key_Type) return Boolean is
begin
return Find_Index (Key) /= 0;
end Contains;
end Generic_KV_Store;
อิมพลีเมนต์นี้เป็น linear search จึงไม่เหมาะกับข้อมูลปริมาณมาก แต่ในสถานการณ์ที่ความยาวคงที่ ขนาดเล็ก และไม่จองหน่วยความจำแบบไดนามิกเป็นเรื่องสำคัญ รูปนี้ใช้ง่าย
sequenceDiagram
accTitle: ลำดับ Put ของ KV store
accDescr: ฝั่งที่เรียก Put แล้วสโตร์หาดัชนีคีย์ ถ้ามีอยู่แล้วเขียนทับค่า ถ้าเป็นคีย์ใหม่หาช่องว่างแล้วบันทึก Keys Values Used จากนั้น Get คืนค่าที่ Pos
participant App as ฝั่งที่เรียก
participant Store as อินสแตนซ์ Generic_KV_Store
participant Data as Keys/Values/Used
App->>Store: Put(Key, Value)
Store->>Data: Find_Index(Key)
alt มีคีย์อยู่แล้ว
Store->>Data: Values(Pos) := Value
else คีย์ใหม่
Store->>Data: Find_Free
Store->>Data: Keys(Pos) := Key
Store->>Data: Values(Pos) := Value
Store->>Data: Used(Pos) := True
end
App->>Store: Get(Key)
Store->>Data: Find_Index(Key)
Data-->>Store: Pos
Store-->>App: Values(Pos)
11.3 ตัวอย่าง instantiation
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;
with Generic_KV_Store;
procedure KV_Demo is
package Int_String_Store is new Generic_KV_Store
(Key_Type => Integer,
Value_Type => Unbounded_String,
Max_Entries => 10);
begin
Int_String_Store.Put (1, To_Unbounded_String ("Ada"));
Int_String_Store.Put (2, To_Unbounded_String ("SPARK"));
if Int_String_Store.Contains (1) then
Put_Line ("1 => " & To_String (Int_String_Store.Get (1)));
else
Put_Line ("1 => (not found)");
end if;
-- Put ของคีย์ที่มีอยู่แล้วจะเขียนทับ
Int_String_Store.Put (1, To_Unbounded_String ("Ada 2022"));
Put_Line ("1 => " & To_String (Int_String_Store.Get (1)));
if Int_String_Store.Contains (9) then
Put_Line ("9 => " & To_String (Int_String_Store.Get (9)));
else
Put_Line ("9 => (not found)");
end if;
end KV_Demo;
gnatchop ../src/snippets/06_kv_store.ada
gnatmake -gnata kv_demo
./kv_demo
1 => Ada
1 => Ada 2022
9 => (not found)
ถ้าเรียก Get โดยไม่ตรวจด้วย Contains เมื่อไม่มีคีย์จะ raise Key_Not_Found ให้เลือกอย่างใดอย่างหนึ่งระหว่างแยกสาขาด้วย Contains แบบตัวอย่างบน หรือเขียน exception handler
ละ "=" ไว้ เพราะ Integer มีตัวดำเนินการเท่ามาตรฐาน และ is <> ทำให้ใช้ตัวนั้น
ถ้าคีย์เป็นสตริงที่ไม่สนตัวพิมพ์ใหญ่เล็ก ก็ส่งฟังก์ชันเท่าที่กำหนดเองได้
function Same_Key (Left, Right : Unbounded_String) return Boolean is
(To_Lower (To_String (Left)) = To_Lower (To_String (Right)));
package String_Key_Store is new Generic_KV_Store
(Key_Type => Unbounded_String,
Value_Type => Integer,
"=" => Same_Key,
Max_Entries => 100);
12. Formal package parameter — ทำให้ generic เป็นชิ้นส่วนได้มากขึ้น
เจเนอริกของ Ada รับ package ทั้งก้อนเป็น formal parameter ได้ เมื่อใช้แบบนี้ “instance ที่สร้างจาก generic package ตัวหนึ่ง” กลายเป็นอินพุตของ generic อีกตัวได้
flowchart LR
accTitle: สแตกถูกส่งต่อเข้า logger
accDescr: Generic_Stack ถูก instantiate เป็น Int_Stack แล้วส่งต่อเข้า Generic_Stack_Logger จนได้ Int_Stack_Logger_Instance
GS[Generic_Stack] --> IS[Int_Stack]
IS --> Logger[Generic_Stack_Logger]
Logger --> Logged[Int_Stack_Logger_Instance]
12.1 Logger ที่รับสแตก
สมมติรับ instance ของ Generic_Stack ก่อนหน้า แล้วทำ logger ที่แสดงขนาด
generic
with package Stack is new Generic_Stack (<>);
package Generic_Stack_Logger is
procedure Print_Size;
end Generic_Stack_Logger;
Body เป็นดังนี้
with Ada.Text_IO; use Ada.Text_IO;
package body Generic_Stack_Logger is
procedure Print_Size is
begin
Put_Line ("Stack size =" & Natural'Image (Stack.Size));
end Print_Size;
end Generic_Stack_Logger;
ฝั่งผู้ใช้สร้างสแตกก่อน แล้วส่งสแตกนั้นให้ logger
package Int_Stack is new Generic_Stack
(Element_Type => Integer,
Max_Size => 10);
package Int_Stack_Logger is new Generic_Stack_Logger
(Stack => Int_Stack);
การออกแบบแบบนี้ประกอบชิ้นส่วน generic เข้าด้วยกันได้
flowchart TB
accTitle: ประกอบ Generic_Stack กับ Generic_Stack_Logger
accDescr: ขั้นที่ 1 สร้าง Int_Stack จาก Generic_Stack ขั้นที่ 2 ส่ง Int_Stack เข้า Generic_Stack_Logger เพื่อได้ Print_Size
subgraph Layer1[ขั้นที่ 1]
T1[Generic_Stack] --> I1[Int_Stack]
end
subgraph Layer2[ขั้นที่ 2]
T2[Generic_Stack_Logger] --> I2[Int_Stack_Logger]
end
I1 --> T2
I2 --> API[Print_Size]
หน้าที่ใกล้ template template parameter ของ C++ แต่ใน Ada เขียนชัดว่า “รับ instance ของ generic package นี้” ในโค้ด Ada ขนาดใหญ่ สะดวกตอนแยกคอนเทนเนอร์ อัลกอริทึม log การตรวจ และตัวช่วยทดสอบ แล้วประกอบกัน
13. Contract Model — ความคิดที่สำคัญที่สุดของ generic ใน Ada
สิ่งสำคัญต่อความเข้าใจเจเนอริกของ Ada คือ contract model
Generic body ต้องเขียนด้วยปฏิบัติการที่ formal parameter สัญญาไว้เท่านั้น เช่น ประกาศแค่ type Element is private; แล้วใช้ < กับ Element ไม่ได้ ถ้าอยากใช้ < ต้องระบุเป็น formal subprogram หรือทำให้ type category เจาะจงขึ้น
flowchart TB
accTitle: Contract model ของ generic ใน Ada
accDescr: Formal part เป็นสัญญา body ถูกตรวจชนิดภายในสัญญานั้น และตอน instantiate จะตรวจว่า actual parameter ทำตามสัญญา แล้วได้ package หรือ subprogram ปกติ
Spec[generic formal part<br/>สัญญา] --> Body[generic body<br/>อิมพลีเมนต์ภายในสัญญา]
Body --> Check1[ตรวจชนิดด้วยตัว body เอง]
Spec --> Inst[instantiation]
Actual[actual parameters<br/>ชนิดจริง ฟังก์ชันจริง ค่าจริง] --> Inst
Inst --> Check2[ตรวจว่า actual parameter ทำตามสัญญาหรือไม่]
Check2 --> Instance[package / subprogram ปกติ]
การออกแบบนี้คุ้มครองทั้งผู้ใช้ generic และผู้เขียน generic
13.1 ความต่างจากมุมมองของ C++ template
Template ของ C++ ทรงพลัง แต่ตามประวัติมีลักษณะว่า “ต้อง instantiate ตัว template body ก่อนจึงจะเห็นข้อผิดพลาด” C++20 concepts ทำให้ดีขึ้น แต่เจเนอริกของ Ada เป็นโมเดลที่เขียนสัญญาให้ชัดตั้งแต่ต้น
flowchart LR
accTitle: Ada กับ C++ template ต่างกันตรงไหน
accDescr: Ada เขียนสัญญาใน formal part แล้วตรวจ body ภายในสัญญา ก่อนตรวจ actual ตอน instantiate ส่วน C++ template ตามประวัติ concretize ข้อกำหนดตอน instantiate และระบุข้อจำกัดด้วย concepts ได้
subgraph Ada[Ada]
A1[เขียนสัญญาใน formal part] --> A2[ตรวจ body ภายในสัญญา]
A2 --> A3[ตรวจ actual ตอน instantiation]
end
subgraph CPP[C++ templates]
C1[เขียน template body] --> C2[ตอน instantiate ข้อกำหนดถูกทำให้เป็นรูปธรรม]
C2 --> C3[ระบุข้อจำกัดด้วย concepts ได้]
end
Generic ของ Java หรือ C# ให้น้ำหนักความสัมพันธ์กับชนิดอ้างอิง ข้อจำกัด type erasure และการแสดงตอนรัน ด้านเจเนอริกของ Ada เอนไปทางสร้าง instance รูปธรรมตอนคอมไพล์
| มุมมอง | Ada | C++ | Java | Rust |
|---|---|---|---|---|
| วิธีเขียนสัญญา | เขียนชนิด ค่า ฟังก์ชัน package ใน formal part | templates / concepts | type parameter กับ bounds | trait bounds |
| การตรวจ body | ตรวจภายในสัญญาของ formal parameter | เน้น concretize ตอน instantiate | ตรวจภายใน bounds | ตรวจภายใน trait bounds |
| ต้นทุนตอนรัน | resolve แบบสแตติกเป็นหลัก | generate แบบสแตติกเป็นหลัก | ได้รับผลจาก type erasure | monomorphize เป็นหลัก |
| value parameter | มี | มี | จำกัด | const generics |
| ใช้ซับโปรแกรมเป็น formal parameter | มี | แสดงด้วย function object เป็นต้น | แลมบ์ดา / functional interface | closure / ฟังก์ชัน / trait |
| ใช้ package เป็น formal parameter | มี | เช่น template template | ไม่มี | คนละเรื่องกับโครงโมดูล |
รายละเอียดภาษาต่างกัน แต่จุดเด่นของ Ada คือ “เขียนสัญญาเป็นไวยากรณ์ก่อน”
14. การตัดสินใจออกแบบในงานจริง — อะไรควรทำให้เป็น generic
เจเนอริกสะดวก แต่ไม่ได้แปลว่าควรทำให้ทุกอย่างเป็น generic ในงานจริงตัดสินใจแบบนี้จะพลาดยากลง
flowchart TB
accTitle: จะทำให้ส่วนไหนเป็น generic
accDescr: แยกกรณีที่ต่างกันแค่ชนิด ขนาดหรือเกณฑ์ พฤติกรรมเปรียบเทียบ หรือสถานะภายใน API เพื่อเลือก type parameter value parameter formal subprogram หรือ generic package
Start[มีตรรกะที่อยากนำกลับมาใช้ใหม่] --> Q1{ต่างกันแค่ชนิด?}
Q1 -->|Yes| GType[พิจารณา type parameter]
Q1 -->|No| Q2{ขนาดหรือเกณฑ์ก็ต่าง?}
Q2 -->|Yes| GObject[เพิ่ม value parameter]
Q2 -->|No| Q3{พฤติกรรมเปรียบเทียบหรือตัดสินต่าง?}
Q3 -->|Yes| GSubp[เพิ่ม formal subprogram]
Q3 -->|No| Q4{อยากรวมสถานะภายในหรือ API?}
Q4 -->|Yes| GPackage[generic package]
Q4 -->|No| Normal[subprogram ปกติก็พอ]
14.1 กรณีที่ generic subprogram เหมาะ
Generic subprogram เหมาะกับอัลกอริทึมที่ไม่มีสถานะ
SwapSortCount_IfFind- การแปลงแบบ
Map Min/Max
เมื่อตัวอัลกอริทึมสั้น อินพุตกับเอาต์พุตชัด ซับโปรแกรมอ่านง่ายกว่า package
14.2 กรณีที่ generic package เหมาะ
Generic package เหมาะเมื่ออยากมีหลายปฏิบัติการและสถานะภายในคู่กับชนิด
- สแตกความยาวคงที่
- ring buffer
- พจนานุกรมขนาดเล็ก
- ชุดประมวลผลสถิติ
- abstraction ของ I/O ต่ออุปกรณ์
- ชุดคำนวณชนิดตัวเลขที่มีระบบหน่วย
โดยเฉพาะใน Ada ที่แยก package specification เป็น API สาธารณะ และ package body เป็นอิมพลีเมนต์ จึงใช้ generic package เป็น “แม่แบบโมดูลที่ type-safe” ได้
flowchart LR
accTitle: แยก spec กับ body ของ generic package
accDescr: Formal part เป็นสัญญาของชนิด ค่า และฟังก์ชัน ไปเลี้ยงทั้ง spec ที่ผู้ใช้เห็น และ body ที่ซ่อนจากผู้ใช้
Spec[package spec<br/>API สาธารณะ] --> User[ฝั่งผู้ใช้]
Body[package body<br/>อิมพลีเมนต์ภายใน] -.ซ่อน.-> User
Formal[generic formal part<br/>สัญญาของชนิด ค่า ฟังก์ชัน] --> Spec
Formal --> Body
14.3 เริ่มด้วย formal parameter น้อย ๆ
ถ้า formal parameter เยอะเกินไป instantiation อ่านยาก เริ่มจากน้อยที่สุด แล้วเพิ่มเมื่อมีเหตุที่ต้องสลับ จึงปลอดภัย
-- ตัวอย่างที่อ่านยากขึ้นง่าย
package X is new Generic_Foo
(A, B, C, D, E, F, G);
-- คงเจตนาไว้ด้วย named association
package X is new Generic_Foo
(Element_Type => Integer,
Index_Type => Positive,
Buffer_Size => 128,
"<" => Less);
ใน Ada ใช้ named association ตอน instantiate ได้ จุดสำคัญของการออกแบบไปปรากฏที่ instantiation ดังนั้นโค้ดงานจริงมักบำรุงง่ายกว่าถ้าเขียนแบบระบุชื่อ
15. จุดที่มักสะดุด
เจเนอริกของ Ada ทรงพลัง แต่มีจุดที่สะดุดง่ายในตอนแรก
15.1 ชนิด private เปรียบเทียบมากน้อยไม่ได้
Body แบบต่อไปนี้เขียนไม่ได้
generic
type Element is private;
function Bad_Min (A, B : Element) return Element;
function Bad_Min (A, B : Element) return Element is
begin
if A < B then -- คอมไพล์ไม่ผ่านตรงนี้
return A;
else
return B;
end if;
end Bad_Min;
Element ถูกประกาศแค่ private จึงไม่รับประกันว่าใช้ < ได้ ถ้าอยากเปรียบเทียบ ให้เพิ่มเข้าสัญญาแบบนี้
generic
type Element is private;
with function "<" (Left, Right : Element) return Boolean is <>;
function Generic_Min (A, B : Element) return Element;
flowchart LR
accTitle: ต้องใส่ฟังก์ชันเปรียบเทียบในสัญญา
accDescr: ถ้า body ต้องการเปรียบเทียบต้องเขียนฟังก์ชันเปรียบเทียบใน formal part เพื่อตรวจตอน instantiate ถ้ามีแค่ private จะคอมไพล์ body ไม่ผ่าน
Need[อยากใช้การเปรียบเทียบใน body] --> Contract[เขียนฟังก์ชันเปรียบเทียบใน formal part]
Contract --> OK[ยืนยันว่าเปรียบเทียบได้ตอน instantiate]
Need --> NoContract[มีแค่ private]
NoContract --> Error[คอมไพล์ generic body ไม่ผ่าน]
15.2 is <> ไม่ใช่ “อนุมานให้อัตโนมัติทุกอย่าง”
is <> สะดวก แต่ไม่ใช่เวทมนตร์ ณ จุด instantiate ต้องมองเห็นตัวดำเนินการหรือซับโปรแกรมที่เข้ากันได้ ถ้าฟังก์ชันเปรียบเทียบที่กำหนดเองอยู่ใน package อื่น ให้ with หรือ use ให้ถูก หรือส่งแบบระบุชื่อจะปลอดภัย
procedure Sort_By_Age is new Generic_Insertion_Sort
(Item_Type => Person,
Index => Positive,
Item_Array => Person_Array,
"<" => Younger_Than);
15.3 exception ของแต่ละ instance เป็นคนละตัว
ถ้าประกาศ exception ใน specification ของ generic package แต่ละ instance จะได้คนละ exception
package Int_Stack is new Generic_Stack (Integer, 5);
package Float_Stack is new Generic_Stack (Float, 3);
ในกรณีนี้ Int_Stack.Stack_Overflow กับ Float_Stack.Stack_Overflow ถูกมองเป็นคนละ exception ถ้าอยากให้เป็น exception ร่วม ให้พิจารณาประกาศ exception นอก generic
flowchart TB
accTitle: exception ของแต่ละ instance เป็นคนละตัว
accDescr: Stack_Overflow ที่ประกาศใน Generic_Stack กลายเป็น Int_Stack.Stack_Overflow และ Float_Stack.Stack_Overflow ซึ่งเป็นคนละ exception
Generic[Generic_Stack<br/>ประกาศ Stack_Overflow] --> I[Int_Stack.Stack_Overflow]
Generic --> F[Float_Stack.Stack_Overflow]
I -.คนละตัว.-> F
15.4 ขนาดโค้ดอาจเพิ่มขึ้น
เจเนอริกเลี่ยงการอ้างอิงทางอ้อมที่เกินจำเป็นตอนรันได้ง่าย ในทางกลับกัน เพราะสร้าง instance ต่อชนิด ถ้าจำนวน instance มาก ขนาดโค้ดอาจโต
นี่คือ trade-off เดียวกับ template ของ C++ และการ monomorphize ของ Rust ในงานความน่าเชื่อถือสูง งานฝังตัว และงานใกล้เรียลไทม์ มักแลกด้วยการลดความไม่แน่นอนตอนรัน แล้วไปจัดการขนาดสิ่งที่ generate ตอน build
flowchart LR
accTitle: ขนาดโค้ดเมื่อมีหลาย instance
accDescr: Generic body เดียว generate โค้ด Integer Float Long_Float และ My_Type ข้อดีคือเลี่ยงการตรวจชนิดตอนรัน ข้อเสียคือ instance เยอะแล้วขนาดโต
Generic[generic body เดียว] --> I1[ฉบับ Integer]
Generic --> I2[ฉบับ Float]
Generic --> I3[ฉบับ Long_Float]
Generic --> I4[ฉบับ My_Type]
I1 --> Code[โค้ดที่ถูก generate]
I2 --> Code
I3 --> Code
I4 --> Code
Code --> Pros[เลี่ยงการตรวจชนิดตอนรันและการ boxing ได้ง่าย]
Code --> Cons[ถ้า instance เยอะ ต้องระวังขนาดที่โต]
15.5 เมื่อไรควรใช้ limited private
type Element is private; สมมติการกำหนดค่า ถ้าจัดการของที่ไม่ควรถูกคัดลอก เช่น file handle, lock, device handle ให้พิจารณา limited private
generic
type Resource is limited private;
with procedure Close (R : in out Resource);
procedure Generic_Use_And_Close (R : in out Resource);
ในการออกแบบที่รับชนิดคัดลอกไม่ได้ อัลกอริทึมที่ใส่ procedure หรือการออกแบบที่ระบุการอ้างอิงให้ชัด มักปลอดภัยกว่าคอนเทนเนอร์ที่เก็บค่า
16. รวมแพตเทิร์นออกแบบเล็ก ๆ
จากนี้จัดรูปที่ใช้บ่อยในงานจริงให้สั้น
16.1 ให้ Min เฉพาะค่าที่เปรียบเทียบได้
generic
type Element is private;
with function "<" (Left, Right : Element) return Boolean is <>;
function Generic_Min (A, B : Element) return Element;
function Generic_Min (A, B : Element) return Element is
begin
if A < B then
return A;
else
return B;
end if;
end Generic_Min;
flowchart LR
accTitle: Generic_Min ต้องการฟังก์ชันเปรียบเทียบ
accDescr: ชนิด Element ต้องมีฟังก์ชันเปรียบเทียบ จึงให้ Generic_Min คืนค่าที่เล็กกว่าได้
T[Element] --> C[ต้องการฟังก์ชันเปรียบเทียบ]
C --> M[Generic_Min]
M --> R[คืนตัวที่เล็กกว่า]
16.2 ใช้เกณฑ์เป็น value parameter
generic
type Count_Type is range <>;
Threshold : Count_Type;
function Generic_Is_Over (Value : Count_Type) return Boolean;
function Generic_Is_Over (Value : Count_Type) return Boolean is
begin
return Value > Threshold;
end Generic_Is_Over;
Value parameter เหมาะกับค่าที่อยากตรึงเป็นสมบัติของ instance ไม่ใช่ค่าตั้งตอนรัน
16.3 ฉีดช่องทางส่งออก
generic
type Element is private;
with procedure Put (Item : Element);
procedure Generic_Print_Twice (Item : Element);
procedure Generic_Print_Twice (Item : Element) is
begin
Put (Item);
Put (Item);
end Generic_Print_Twice;
รูปนี้สลับปลายทางได้ ทั้งมาตรฐานเอาต์พุต, log, และบัฟเฟอร์สำหรับทดสอบ
flowchart TB
accTitle: ฉีด Put เพื่อสลับช่องทางส่งออก
accDescr: Generic_Print_Twice รับ Put เป็น formal subprogram แล้วชี้ไปคอนโซล log หรือบัฟเฟอร์ทดสอบได้
Print[Generic_Print_Twice] --> Put[รับ Put เป็น formal subprogram]
Put --> Console[ส่งออกคอนโซล]
Put --> Log[ส่งออกเป็น log]
Put --> Test[บัฟเฟอร์สำหรับทดสอบ]
16.4 ไม่ตรึงชนิดดัชนีของอาร์เรย์
ใน Ada ชนิดดัชนีของอาร์เรย์ก็เป็นข้อมูลชนิดที่สำคัญ ไม่ยึด Positive เสมอไป ใส่ชนิดดัชนีเป็น formal parameter ตามต้องการ จะนำกลับมาใช้ใหม่ได้กว้างขึ้น
generic
type Element is private;
type Index is (<>);
type Array_Type is array (Index range <>) of Element;
procedure Generic_Clear (Arr : in out Array_Type; Value : Element);
การออกแบบนี้รองรับได้ทั้งอาร์เรย์ดัชนี Positive และอาร์เรย์ดัชนี enumeration
flowchart LR
accTitle: ไม่ตรึงชนิดดัชนีของอาร์เรย์
accDescr: Index ที่เป็น discrete ใช้ได้ทั้งช่วง Positive enumeration ชื่อ Day หรือ State และชนิดจำนวนเต็มที่กำหนดเอง
Index[Index is discrete] --> Positive[Positive range]
Index --> Day[enumeration Day]
Index --> State[enumeration State]
Index --> Slot[ชนิดจำนวนเต็มที่กำหนดเอง]
17. เช็กลิสต์ให้ API เป็นแบบ Ada
ตอนเขียน generic ทบทวนด้วยมุมนี้ท้ายสุด จะอ่านง่ายขึ้น
flowchart TB
accTitle: เช็กลิสต์ออกแบบ generic
accDescr: ตรวจว่า formal parameter น้อยที่สุด ปฏิบัติการที่ต้องใช้ถูกเขียนใน formal part หมวดชนิดเหมาะสม instantiate อ่านง่าย และรับได้ทั้งสถานะ exception และขนาดโค้ด
C[เช็คการออกแบบ generic] --> C1[formal parameter น้อยที่สุดหรือยัง]
C --> C2[เขียนปฏิบัติการที่ต้องใช้ใน formal part แล้วหรือยัง]
C --> C3[หมวดอย่าง private / range / digits เหมาะหรือยัง]
C --> C4[instantiate อ่านง่ายด้วย named association ได้หรือยัง]
C --> C5[คิดถึงสถานะและ exception ต่อ instance หรือยัง]
C --> C6[รับการโตของขนาดโค้ดได้หรือยัง]
C --> C7[มี instance สำหรับทดสอบหรือยัง]
สรุปเป็นข้อความได้ดังนี้
- ปฏิบัติการที่ body ใช้ ต้องมองเห็นได้เป็นสัญญาของ formal parameter เสมอ
- ถ้า
privateพอก็ใช้privateถ้าต้องการเลขคณิต ให้ใช้range <>หรือdigits <> - พฤติกรรมที่เปลี่ยนตามชนิด เช่น เปรียบเทียบ แฮช ส่งออก แปลง ให้เป็น formal subprogram
- ถ้าขนาดหรือเกณฑ์เป็นสมบัติของ instance ให้เป็น value parameter
- ถ้ามีสถานะ ให้คิด generic package ก่อน ถ้าไม่มีสถานะ ให้คิด generic subprogram ก่อน
- ตอน instantiate อาร์กิวเมนต์ยิ่งเยอะ ยิ่งใช้ named association
- ออกแบบโดยสมมติว่า exception และสถานะภายในเป็นอิสระต่อ instance
18. ตัวอย่างโครงสร้างทั้งชุดของตัวอย่าง
ถ้าแยกตัวอย่างในบทความเป็นไฟล์ โครงแบบนี้จะอ่านง่าย
flowchart TB
accTitle: โครงไฟล์ตัวอย่างทั้งชุด
accDescr: ราก ada-generic-programming มี src/generics สำหรับ spec กับ body และ src/demos สำหรับโปรแกรมสาธิตแต่ละหัวข้อ
Root[ada-generic-programming] --> Src[src]
Src --> G[generics]
Src --> D[demos]
G --> SwapSpec[generic_swap.ads]
G --> SwapBody[generic_swap.adb]
G --> StackSpec[generic_stack.ads]
G --> StackBody[generic_stack.adb]
G --> SortSpec[generic_insertion_sort.ads]
G --> SortBody[generic_insertion_sort.adb]
G --> StatsSpec[generic_statistics.ads]
G --> StatsBody[generic_statistics.adb]
G --> CountSpec[generic_count_if.ads]
G --> CountBody[generic_count_if.adb]
G --> KVSpec[generic_kv_store.ads]
G --> KVBody[generic_kv_store.adb]
D --> SwapDemo[swap_demo.adb]
D --> StackDemo[stack_demo.adb]
D --> SortDemo[sort_demo.adb]
D --> StatsDemo[statistics_demo.adb]
D --> CountDemo[count_if_demo.adb]
D --> KVDemo[kv_demo.adb]
ตัวอย่างเล็กสำหรับบทความ รวมในไฟล์เดียวแล้ว gnatchop ก็สะดวก แต่ถ้าคิดงานจริงและการบำรุงระยะยาว การแยก specification .ads กับ body .adb จะเป็นโครงแบบ Ada กว่า
19. สรุป — กำหนดขอบเขตการนำกลับมาใช้ใหม่ด้วยชนิดข้อมูล
Generic programming ของ Ada ไม่ใช่แค่ “ฟีเจอร์เขียนโค้ดที่ไม่ผูกกับชนิด” สาระจริงคือ เขียนให้ชัดว่าชิ้นส่วนที่นำกลับมาใช้ใหม่ได้เรียกร้องอะไร เป็นสัญญาของชนิดข้อมูล ซับโปรแกรม และค่า
flowchart LR
accTitle: จากสัญญาถึงการนำกลับมาใช้ใหม่
accDescr: เขียนสัญญา แล้วเขียน generic body ส่งชนิด ค่า ฟังก์ชันเพื่อ instantiate ใช้แบบ type-safe แล้วนำกลับมาใช้ใหม่โดยไม่คัดลอก
Contract[เขียนสัญญา] --> Generic[เขียน generic body]
Generic --> Instance[ส่งชนิด ค่า ฟังก์ชันแล้ว instantiate]
Instance --> Safe[ใช้แบบ type-safe]
Safe --> Reuse[นำกลับมาใช้ใหม่โดยไม่คัดลอก]
อย่างที่เห็นในบทความนี้ เจเนอริกของ Ada ใส่สิ่งต่อไปนี้เป็น formal parameter ได้
- ชนิดข้อมูล
- ค่า
- ซับโปรแกรม
- package
ยิ่งกว่านั้น ชนิดข้อมูลระบุหมวดได้ละเอียด เช่น private, limited private, range <>, mod <>, digits <>, delta <>, (<>) ด้วยเหตุนี้ generic body จึงไม่อิงปฏิบัติการที่ “ไม่รู้ว่าทำได้หรือไม่” แต่อิมพลีเมนต์อย่างปลอดภัยด้วยปฏิบัติการที่เขียนในสัญญาเท่านั้น
ในสินทรัพย์ C หรือ C++ รุ่นเก่า บางครั้งใช้มาโคร, void*, ฟังก์ชันพอยน์เตอร์, การแยกชนิดด้วยมือ เพื่อนำกลับมาใช้ใหม่ เจเนอริกของ Ada แทนที่งานพวกนั้นได้จำนวนมาก ในรูปที่ type-safe และอ่านง่าย โดยเฉพาะงานบำรุงระยะยาว งานฝังตัว งานเรียลไทม์ และซอฟต์แวร์ความน่าเชื่อถือสูง การออกแบบที่ “กำหนดขอบเขตตอนคอมไพล์” มีคุณค่าสูง
20. ขอบเขตงานให้คำปรึกษาที่เกี่ยวข้อง
KomuraSoft LLC รับพัฒนาแอป Windows สำรวจและแก้สินทรัพย์เดิม จัดระเบียบรอยต่อ COM / ActiveX / 32bit / 64bit รวมถึงที่ปรึกษาเทคนิคและรีวิวการออกแบบ นอกจากงานออกแบบที่เอน static typing และความน่าเชื่อถือสูงแบบ Ada แล้ว ธีมที่ในงานจริงมักอยู่ใกล้กันคือจะจัดระเบียบสินทรัพย์ C/C++, C#, VB6, MFC, COM เดิมอย่างไร เพื่อยืดอายุหรือย้ายระบบ
- การพัฒนาแอป Windows
- การพัฒนาคอมโพเนนต์ COM
- การแทนที่แอป Windows
- ที่ปรึกษาเทคนิคและรีวิวการออกแบบ
- ติดต่อเรา
อ้างอิง
- Ada 2022 Language Reference Manual, Section 12: Generic Units — ตัวบทที่กำหนด generic unit เอง 12.1 คือ generic declaration, 12.3 คือ instantiation, 12.4 คือ formal object (value parameter), 12.5 คือ formal type (หมวดอย่าง
private,range <>,digits <>), 12.6 คือ formal subprogram, 12.7 คือ formal package ถ้าหลงในแต่ละบทของบทความนี้ เปิดวรรคที่ตรงกันจะชัวร์ที่สุด - Ada 2022 Language Reference Manual, 2.2: Lexical Elements, Separators, and Delimiters — จุดที่นิยามว่า
<>ถูกเรียกว่า compound delimiter ชื่อ “box” เป็นหลักฐานของคำว่าboxในรูป - GNAT User’s Guide for Native Platforms — วิธีใช้
gnatmake,gnatchopและรายการออปชันคอมไพล์รวม-gnataถ้าติดขั้นตอนในบทที่ 3 ให้ดูที่นี่ - Alire Documentation — วิธีติดตั้ง
alrการจัดการ toolchain (GNAT / gprbuild) และการสร้าง crate ถ้าต้องการแค่จัดสภาพแวดล้อม เอกสารนี้พอ - โค้ดตัวอย่าง (GitHub) — ตัวอย่างในบทความนี้ที่แยกเป็นไฟล์ตามโครงบทที่ 18
บทความที่เกี่ยวข้อง
บทความล่าสุดที่มีแท็กเดียวกัน เพื่อเจาะลึกหัวข้อใกล้เคียง
การทำงานแบบ concurrent ที่ปลอดภัยใน Ada ── คู่มือปฏิบัติเรื่อง task และ protected object
บทความแนะนำ concurrency ที่ฝังในภาษา Ada เอง ได้แก่ task และ protected object จัดระเบียบตั้งแต่ rendezvous (entry/accept), selective acce...
โปรแกรมมิงระบบเรียลไทม์ด้วย Ada — ลำดับความสำคัญ คาบ และการควบคุมเวลาประมวลผลในทางปฏิบัติ
เรียนรู้ Annex D (ระบบเรียลไทม์) ของ Ada ผ่านตัวอย่างโค้ดเชิงปฏิบัติ 8 ชุด จัดเรียงทีละขั้นตั้งแต่ลำดับความสำคัญของ task, Ceiling_Locking...
เชิงลึกของการจำลองเสมือนบน Windows (ตอนที่ 3) — เครื่องเสมือนที่บูตในไม่กี่วินาที: ทำไม WSL2, Windows Sandbox และคอนเทนเนอร์จึงเบา
ทำไม WSL2 และ Windows Sandbox จึงเริ่มในไม่กี่วินาทีและรู้สึกเบา? บทความนี้อธิบายกลไก ตั้งแต่ dynamic base image และ direct map ไปจนถึงกา...
เชิงลึกของการจำลองเสมือนบน Windows (ตอนที่ 2) — หน่วยความจำที่แม้เคอร์เนลก็มองไม่เห็น: กลไกของ VBS, HVCI และ Credential Guard
เมื่อติดตั้งใหม่บนฮาร์ดแวร์ที่รองรับ VBS จะเปิดตามค่าเริ่มต้น และใช้ hypervisor กับ SLAT สร้างการแยกที่แข็งกว่าเคอร์เนล บทความนี้อธิบายโค...
เชิงลึกของการจำลองเสมือนบน Windows (ตอนที่ 1) — Windows ของคุณรันอยู่ที่ไหนจริง ๆ? Hypervisor และพาร์ติชัน
เมื่อเปิด Hyper-V แล้ว Windows ฝั่งโฮสต์เองก็รันบน hypervisor ในฐานะ root partition บทความนี้อธิบายรากฐานของการจำลองเสมือนผ่านบทบาทของ VT...
หัวข้อที่เกี่ยวข้อง
หน้าเหล่านี้วางหัวข้อไว้ในบริบทที่กว้างขึ้นของบริการและการตัดสินใจ
หัวข้อเทคนิคของ Windows
ประตูสู่การพัฒนา Windows การตรวจสอบบั๊ก และการใช้ประโยชน์จากสินทรัพย์เดิม
คำถามที่พบบ่อย
คำถามที่มักพบในการปรึกษาเกี่ยวกับหัวข้อของบทความนี้
- เจเนอริกของ Ada คืออะไร?
- เป็นกลไกการนำโค้ดกลับมาใช้ใหม่ที่รับชนิดข้อมูล ค่า ซับโปรแกรม และแม้แต่ package ทั้งก้อนเป็น formal parameter แล้วตรวจชนิดแบบสแตติกตอน instantiate ด้วย new ไม่ใช่การแทนที่ข้อความล้วน ๆ แต่เป็นการยืนยันตอนคอมไพล์ว่าชิ้นส่วนนี้ทำตามสัญญาหรือไม่ Generic subprogram แค่ประกาศยังเรียกใช้ไม่ได้ ต้องให้ชนิดข้อมูลจริงแล้ว instantiate จึงจะกลายเป็น procedure หรือ function ปกติ
- เจเนอริกของ Ada ต่างจาก template ของ C++ อย่างไร?
- Ada ใช้ contract model ที่เขียนสัญญาให้ชัดตั้งแต่ต้น ตัว body ของ generic ใช้ได้เฉพาะปฏิบัติการที่ formal parameter สัญญาไว้ และถูกตรวจชนิดได้ด้วยตัว body เอง Template ของ C++ ตามประวัติจะเพิ่งเห็นข้อผิดพลาดตอน instantiate ซึ่ง C++20 concepts ทำให้ดีขึ้นมาก นอกจากนี้ Ada รับได้ทั้ง value parameter กับ subprogram parameter และรับ package ทั้งก้อนเป็น formal parameter ได้ จึงประกอบชิ้นส่วน generic เข้าด้วยกันได้
- ทำไม formal type parameter ของ Ada ต้องระบุ type category?
- เพื่อเขียนสัญญาว่า body ใช้ปฏิบัติการอะไรได้บ้าง type T is private สมมติได้แค่ปฏิบัติการพื้นฐานอย่างการกำหนดค่าและการเทียบเท่า เปรียบเทียบมากน้อยหรือคำนวณเลขใช้ไม่ได้ ถ้าต้องการเลขคณิตจำนวนเต็มใช้ range <> ถ้าต้องการเลขทศนิยมลอยตัวใช้ digits <> ถ้าต้องการบิตใช้ mod <> การระบุหมวดยังทำหน้าที่เป็นสเปกระดับชนิดข้อมูล ที่บอกผู้อ่านว่าชิ้นส่วนนี้เรียกร้องอะไร
- มีจุดที่ต้องระวังเมื่อใช้เจเนอริกของ Ada หรือไม่?
- เพราะสร้าง instance ต่อชนิด ถ้ามีจำนวน instance มาก ขนาดโค้ดอาจโตได้ นี่คือ trade-off เดียวกับ template ของ C++ และการ monomorphize ของ Rust นอกจากนี้ exception ที่ประกาศใน specification ของ generic package จะกลายเป็นคนละ exception ในแต่ละ instance แนวทางในงานจริงคือเริ่มด้วย formal parameter น้อยที่สุด เพิ่มเมื่อมีเหตุที่ต้องสลับจริง และตอน instantiate ที่อาร์กิวเมนต์เยอะให้ใช้ named association