Generic programming ใน Ada — เขียนสัญญาด้วยชนิดข้อมูล เพื่อนำกลับมาใช้ใหม่แบบ zero-cost

· อัปเดตเมื่อ: · · 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 นั่นคือไม่ไปตรวจตอนรันว่าชนิดนี้ใช้ได้จริงหรือไม่ แต่ยืนยันตอนคอมไพล์ว่าชิ้นส่วนนี้ทำตามสัญญาหรือไม่

ทางเลือกเมื่อจะนำโค้ดกลับมาใช้ใหม่เปรียบเทียบการคัดลอกวาง การรับผ่าน void* หรือ cast กับ generic ของ Ada ที่ type-safe ตรวจตอนคอมไพล์ และไม่มี dispatch ส่วนเกินตอนรันตรรกะที่อยากนำกลับมาใช้ใหม่จะนำกลับมาใช้ใหม่อย่างไรคัดลอกแล้ววางvoid* / Object / castgeneric ของ Adaพลาดแก้ไม่ครบได้ง่ายผิดพลาดตอนรันหรือเสีย type safety ได้ง่ายtype-safeตรวจตอนคอมไพล์ไม่มี 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 ก็ทดลองกลไกนี้ได้ในสภาพแวดล้อมฟรี

แผนที่ความรู้ generic programming ของ Adaแผนภาพที่แสดงว่า generic subprogram และ generic package ตั้งอยู่บนสัญญา (contract model) ของ formal parameter ที่เป็นชนิดข้อมูล ค่า ซับโปรแกรม และ package ว่าสัญญาถูกตรวจตอน instantiate ด้วย new และความต่างจากปัญหาข้อผิดพลาดตอน instantiate ที่ template ของ C++ มีมาตามประวัติอิมพลีเมนต์ใช้ใช้ต้องมีใช้ใช้ต้องมีกำหนดค่าด้วยต้องมีต้องมีอิมพลีเมนต์อิมพลีเมนต์ป้องกันอาจก่อให้เกิดต้องมีอิมพลีเมนต์กำหนดค่าด้วยใช้ใช้ต้องมีอิมพลีเมนต์generic ของ Ada (generic unit)contract model ของ Ada genericAda (ภาษาโปรแกรม)ซับโปรแกรม generic (Generic Subprogram)แพ็กเกจ generic (Generic Package)พารามิเตอร์ Formal Typeพารามิเตอร์ Formal Subprogramพารามิเตอร์ Formal Packageการสร้างอินสแตนซ์ (instantiation)พารามิเตอร์ Formal Object (ค่า)ข้อผิดพลาดเทมเพลตที่รู้ได้ตอนสร้างอินสแตนซ์เท่านั้นเทมเพลต C++GNATAlire

ในแผนภาพ เส้นทึบหมายถึงความสัมพันธ์ที่ถือเสมอ และเส้นประหมายถึงความสัมพันธ์ที่มีเงื่อนไข (เงื่อนไขอยู่ในการอธิบายของแต่ละความสัมพันธ์ในหน้าละเอียด) รายการความสัมพันธ์ทั้งหมด (รวม 21 รายการ พร้อมหลักฐานและระดับความเชื่อมั่น) และนิยามของแนวคิดหลักรวบรวมไว้ที่ หน้าละเอียดของแผนที่ความรู้ (เป็นภาษาญี่ปุ่น) ข้อมูล: JSON-LD / Turtle

2. แผนที่ของบทความนี้

ก่อนอื่นจับภาพรวมด้วยแผนภาพ ถ้าเข้าใจเจเนอริกของ Ada แค่ว่า “ฟีเจอร์ที่รับชนิดข้อมูลเป็นอาร์กิวเมนต์” จะแคบเกินไป ในทางปฏิบัติเราประกอบ subprogram, package, subprogram parameter, value parameter และ formal package parameter ตามหน่วยที่อยากนำกลับมาใช้ใหม่

Ada GenericsGeneric SubprogramSwapCount_IfSortGeneric PackageStackStatisticsKV StoreFormal ParametersTypeprivatelimited privaterange boxmod boxdigits boxdelta boxdiscrete boxObjectMax_SizeThresholdSubprogramLess functionEquals functionPredicatePackagewith package P is newGenericแนวคิดออกแบบContract ModelStatic CheckingZero-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

ตัวอย่างในบทความนี้สมมติวางในรีโพสิทอรีดังนี้

โครงไฟล์ตัวอย่างตัวอย่างอยู่ใน ada-generic-programming/src/snippets เป็นไฟล์ .ada ทีละหัวข้อ พร้อม README.md ที่รากada-generic-programming/src/snippets/01_swap.ada02_stack.ada03_sort.ada04_statistics.ada05_filter.ada06_kv_store.adaREADME.md

ตัวอย่างที่รวมหลายหน่วยคอมไพล์ไว้ในไฟล์เดียว ให้แยกด้วย gnatchop แล้วค่อย gnatmake

mkdir work
cd work
gnatchop ../src/snippets/01_swap.ada
gnatmake -gnata swap_demo
./swap_demo

-gnata เป็นออปชันเปิด assertion ไม่จำเป็นต่อการใช้ generic เอง แต่ในตัวอย่างสำหรับเรียน ช่วยตรวจสัญญาและเงื่อนไขขอบได้ง่ายขึ้น

ลำดับ gnatchop แล้ว gnatmakeผู้พัฒนาส่งไฟล์ .ada ไฟล์เดียว ให้ gnatchop แยกเป็น .ads .adb และ main แล้ว gnatmake bind กับ link จนได้ไฟล์ executableไฟล์ executablegnatmakegnatchopผู้พัฒนาไฟล์ executablegnatmakegnatchopผู้พัฒนาส่งไฟล์ .ada ไฟล์เดียวแยกเป็น .ads / .adb / maingnatmake -gnata mainbind และ link จนจบ./mainผลลัพธ์การรัน

4. โมเดลพื้นฐานของ generic ใน Ada

เจเนอริกของ Ada คิดเป็นสามอย่างนี้จะเข้าใจง่าย

  1. เขียน generic unit
  2. เขียน formal parameter ในส่วน generic
  3. ฝั่งผู้ใช้ instantiate ด้วย new
โมเดลสามขั้นของ generic ใน Adaจากประกาศ generic ผ่าน formal parameter และ body ไปสู่ instantiation ด้วย new แล้วใช้เป็น subprogram หรือ package ปกติประกาศ genericformal parametergeneric bodyinstantiation ด้วย newใช้เป็น subprogram หรือ package ปกติชนิดข้อมูลค่าซับโปรแกรม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);

วาดเป็นความสัมพันธ์ได้แบบนี้

Instantiation ของ Generic_Swap ตามชนิดGeneric_Swap ที่รับ Element เป็น private ถูก instantiate เป็น Swap ของ Integer Character และ My_Record แล้วสลับตัวแปรชนิดนั้นส่ง Integerส่ง Characterส่ง My_RecordGeneric_Swaptype Element is privateSwap_IntegerSwap_CharacterSwap_My_Recordสลับตัวแปร Integerสลับตัวแปร Characterสลับตัวแปร My_Record

จุดสำคัญคือ body ของแม่แบบถูกเขียนด้วยปฏิบัติการที่ใช้กับ Element ได้เท่านั้น ถ้าประกาศ type Element is private; ใช้การกำหนดค่าและการเทียบเท่าได้ แต่เปรียบเทียบมากน้อยหรือคำนวณเลขใช้ไม่ได้ นั่นคือตัวประกาศ generic เองบอกว่า “ชิ้นส่วนนี้สมมติอะไรได้บ้าง”

5. ชนิดของ formal parameter — คำศัพท์ของ generic ใน Ada

สิ่งที่เจเนอริกของ Ada รับได้ไม่ใช่แค่ชนิดข้อมูล จุดนี้ต่างจาก generic ทั่วไปของ C# หรือ Java อย่างชัด

ชนิดของ formal parameterFormal parameter ของ Ada มีชนิดข้อมูล ค่าวัตถุ ซับโปรแกรม และ package พร้อมตัวอย่างเช่น private digits box และ with functiongeneric formal parameterstype parameterobject / value parametersubprogram parameterpackage parametertype Element is privatetype Index is boxtype Real is digits boxMax_Size : PositiveDefault_Value : Elementwith function Less...with procedure Put ...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 จึงใช้ได้อย่างเป็นธรรมชาติ

การสลับค่าด้วยตัวแปรชั่วคราวก่อนเรียก A เป็น 10 และ B เป็น 20 หลังคัดลอกผ่าน Temp ค่าของ A และ B สลับกันหลังเรียกก่อนเรียกA = 20B = 10A = 10B = 20Temp = A

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 ตัวอย่างคลาสสิกคือสแตก

สแตกเปลี่ยนแค่ชนิดสมาชิกกับขนาดสูงสุด ตรรกะพื้นฐานก็เหมือนกัน

Generic_Stack รับชนิดกับขนาดGeneric_Stack รับ Element_Type กับ Max_Size แล้วให้ Push Pop Size และถูก instantiate เป็นสแตก Integer Float และสตริงGeneric_StackElement_TypeMax_SizePush / Pop / Size / Is_Empty / Is_FullInt_StackElement=IntegerMax_Size=5Float_StackElement=FloatMax_Size=3String_StackElement=Unbounded_StringMax_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 parameter
  • Max_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);

สองตัวนี้มาจากแม่แบบเดียวกัน แต่ไม่แชร์สถานะภายใน

สถานะของ Int_Stack กับ Float_Stack แยกกันGeneric_Stack สร้าง Int_Stack และ Float_Stack แต่ละตัวมี Top และ Data ของตัวเอง ไม่แชร์สถานะสถานะของ Float_StackTopData : อาร์เรย์ Floatสถานะของ Int_StackTopData : อาร์เรย์ IntegerGeneric_StackInt_StackFloat_Stack

7.3 การเปลี่ยนสถานะของสแตก

สแตกดูเป็นเครื่องจักรสถานะจะเข้าใจง่าย

การเปลี่ยนสถานะของสแตกสแตกเคลื่อนระหว่าง Empty NonEmpty และ Full ด้วย Push กับ Pop และไป Overflow หรือ Underflow เมื่อทำนอกสัญญาPushPush / PopPop ตัวสุดท้ายออกPush จนถึง Max_SizePopPushPopEmptyNonEmptyFullOverflowUnderflow

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 ได้

ฉีดฟังก์ชันเปรียบเทียบเข้า Generic_Insertion_SortGeneric_Insertion_Sort รับชนิดสมาชิก ดัชนี อาร์เรย์ และฟังก์ชันเปรียบเทียบ เพื่อเรียงน้อยไปมาก มากไปน้อย หรือลำดับที่กำหนดเองGeneric_Insertion_SortItem_TypeIndexItem_Arrayฟังก์ชันเปรียบเทียบใช้การเปรียบเทียบมาตรฐานส่ง Greater เพื่อเรียงมากไปน้อยส่งลำดับที่กำหนดเอง

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 สี่ตัว

  1. Item_Type: ชนิดสมาชิกของอาร์เรย์
  2. Index: ชนิดดัชนีของอาร์เรย์
  3. Item_Array: ชนิดอาร์เรย์จริง
  4. "<": ฟังก์ชันเปรียบเทียบ

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 เพราะแค่สลับฟังก์ชันเปรียบเทียบ ก็ใช้โครงลูปเดียวกันได้ทั้งเรียงน้อยไปมากและมากไปน้อย

ขั้นตอน insertion sortหยิบ Key จากซ้าย เลื่อนสมาชิกก่อนหน้าไปทางขวาจนกว่าจะเจอที่แทรก แล้วทำซ้ำจนอาร์เรย์เรียงแล้วYesNoNoYesอาร์เรย์ยังไม่เรียงหยิบ Key จากซ้ายไปขวาKey มาก่อนสมาชิกก่อนหน้าหรือไม่?เลื่อนสมาชิกก่อนหน้าไปทางขวาแทรก Keyทำจนตัวสุดท้ายแล้วหรือยัง?อาร์เรย์เรียงแล้ว

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

เรียงน้อยไปมากและมากไปน้อยจาก body เดียวกันข้อมูลชุดเดียวกันถูก Sort_Asc ด้วยการเปรียบเทียบมาตรฐาน และ Sort_Desc ด้วย Greater ได้ลำดับกลับกัน99, 3, 47, 12Sort_AscการเปรียบเทียบมาตรฐานSort_Descส่ง Greater เป็นฟังก์ชันเปรียบเทียบ3, 12, 47, 9999, 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 ได้

หมวดของ formal typeFormal type แยกเป็น private limited private discrete range mod digits delta และ access พร้อมตัวอย่างชนิดที่เข้าแต่ละหมวดFormal Typeprivatelimited privatediscrete box: discrete typerange box: signed integermod box: modular integerdigits box: floating-pointdelta box: fixed-pointชนิด accessenumerationชนิดจำนวนเต็มFloatLong_Floatชนิดทศนิยมลอยตัวที่ผู้ใช้กำหนด

คำว่า 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);

สถิติชุดเดียวกันนำกลับมาใช้ใหม่กับชนิดทศนิยมลอยตัวที่ความละเอียดต่างกันได้

Generic_Statistics ใช้กับความละเอียดต่างกันGeneric_Statistics ที่ Real เป็น digits box ถูก instantiate เป็น Float_Stats Long_Stats และ My_Real_Stats เพื่อคำนวณ Mean กับ VarianceGeneric_StatisticsReal is digits boxFloat_StatsLong_StatsMy_Real_StatsMean / Variance ด้วย FloatMean / Variance ด้วย Long_FloatMean / 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;

ลำดับการทำงานตรงไปตรงมา

การฉีด predicate เข้า Generic_Count_Ifไล่สมาชิกอาร์เรย์ ถ้า Predicate เป็นจริงเพิ่ม Count แล้วคืน CountTrueFalseอาร์เรย์ไล่แต่ละสมาชิกPredicate(Item)?เพิ่ม Countไม่ทำอะไรไปสมาชิกถัดไปคืน 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);

จากตรรกะไล่ชุดเดียวกัน สร้างฟังก์ชันสองตัวที่ต่างกันแค่เงื่อนไขได้

นับเลขคู่กับนับเกินเกณฑ์จากตัวเดียวกันGeneric_Count_If ถูก instantiate เป็น Count_Even ด้วย Is_Even และ Count_Large ด้วย Is_Large บนข้อมูลชุดเดียวกันGeneric_Count_IfCount_EvenPredicate = Is_EvenCount_LargePredicate = Is_Large12, 7, 88, 3, 56, 91, 44, 19, 62จำนวนเลขคู่จำนวนที่มากกว่า 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 แบบความยาวคงที่ง่าย ๆ เป็นตัวอย่าง

Generic_KV_Store ประกอบหลายพารามิเตอร์Generic_KV_Store รับชนิดคีย์ ชนิดค่า ฟังก์ชันเทียบคีย์ และ Max_Entries แล้วให้ Put Get Contains สำหรับ settings store แคชเล็ก และพจนานุกรมความยาวคงที่งานฝังตัวGeneric_KV_StoreKey_TypeValue_Typeฟังก์ชันเทียบคีย์Max_EntriesPut / Get / Containssettings storeแคชขนาดเล็กพจนานุกรมความยาวคงที่งานฝังตัว

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 จึงไม่เหมาะกับข้อมูลปริมาณมาก แต่ในสถานการณ์ที่ความยาวคงที่ ขนาดเล็ก และไม่จองหน่วยความจำแบบไดนามิกเป็นเรื่องสำคัญ รูปนี้ใช้ง่าย

ลำดับ Put ของ KV storeฝั่งที่เรียก Put แล้วสโตร์หาดัชนีคีย์ ถ้ามีอยู่แล้วเขียนทับค่า ถ้าเป็นคีย์ใหม่หาช่องว่างแล้วบันทึก Keys Values Used จากนั้น Get คืนค่าที่ PosKeys/Values/Usedอินสแตนซ์ Generic_KV_Storeฝั่งที่เรียกKeys/Values/Usedอินสแตนซ์ Generic_KV_Storeฝั่งที่เรียกalt[มีคีย์อยู่แล้ว][คีย์ใหม่]Put(Key, Value)Find_Index(Key)Values(Pos) := ValueFind_FreeKeys(Pos) := KeyValues(Pos) := ValueUsed(Pos) := TrueGet(Key)Find_Index(Key)PosValues(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 อีกตัวได้

สแตกถูกส่งต่อเข้า loggerGeneric_Stack ถูก instantiate เป็น Int_Stack แล้วส่งต่อเข้า Generic_Stack_Logger จนได้ Int_Stack_Logger_InstanceGeneric_StackInt_StackGeneric_Stack_LoggerInt_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 เข้าด้วยกันได้

ประกอบ Generic_Stack กับ Generic_Stack_Loggerขั้นที่ 1 สร้าง Int_Stack จาก Generic_Stack ขั้นที่ 2 ส่ง Int_Stack เข้า Generic_Stack_Logger เพื่อได้ Print_Sizeขั้นที่ 2ขั้นที่ 1Int_Stack_LoggerGeneric_Stack_LoggerInt_StackGeneric_StackPrint_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 เจาะจงขึ้น

Contract model ของ generic ใน AdaFormal part เป็นสัญญา body ถูกตรวจชนิดภายในสัญญานั้น และตอน instantiate จะตรวจว่า actual parameter ทำตามสัญญา แล้วได้ package หรือ subprogram ปกติgeneric formal partสัญญาgeneric bodyอิมพลีเมนต์ภายในสัญญาตรวจชนิดด้วยตัว body เองinstantiationactual parametersชนิดจริง ฟังก์ชันจริง ค่าจริงตรวจว่า actual parameter ทำตามสัญญาหรือไม่package / subprogram ปกติ

การออกแบบนี้คุ้มครองทั้งผู้ใช้ generic และผู้เขียน generic

13.1 ความต่างจากมุมมองของ C++ template

Template ของ C++ ทรงพลัง แต่ตามประวัติมีลักษณะว่า “ต้อง instantiate ตัว template body ก่อนจึงจะเห็นข้อผิดพลาด” C++20 concepts ทำให้ดีขึ้น แต่เจเนอริกของ Ada เป็นโมเดลที่เขียนสัญญาให้ชัดตั้งแต่ต้น

Ada กับ C++ template ต่างกันตรงไหนAda เขียนสัญญาใน formal part แล้วตรวจ body ภายในสัญญา ก่อนตรวจ actual ตอน instantiate ส่วน C++ template ตามประวัติ concretize ข้อกำหนดตอน instantiate และระบุข้อจำกัดด้วย concepts ได้C++ templatesตอน instantiate ข้อกำหนดถูกทำให้เป็นรูปธรรมเขียน template bodyระบุข้อจำกัดด้วย concepts ได้Adaตรวจ body ภายในสัญญาเขียนสัญญาใน formal partตรวจ actual ตอน instantiation

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 ในงานจริงตัดสินใจแบบนี้จะพลาดยากลง

จะทำให้ส่วนไหนเป็น genericแยกกรณีที่ต่างกันแค่ชนิด ขนาดหรือเกณฑ์ พฤติกรรมเปรียบเทียบ หรือสถานะภายใน API เพื่อเลือก type parameter value parameter formal subprogram หรือ generic packageYesNoYesNoYesNoYesNoมีตรรกะที่อยากนำกลับมาใช้ใหม่ต่างกันแค่ชนิด?พิจารณา type parameterขนาดหรือเกณฑ์ก็ต่าง?เพิ่ม value parameterพฤติกรรมเปรียบเทียบหรือตัดสินต่าง?เพิ่ม formal subprogramอยากรวมสถานะภายในหรือ API?generic packagesubprogram ปกติก็พอ

14.1 กรณีที่ generic subprogram เหมาะ

Generic subprogram เหมาะกับอัลกอริทึมที่ไม่มีสถานะ

  • Swap
  • Sort
  • Count_If
  • Find
  • การแปลงแบบ 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” ได้

แยก spec กับ body ของ generic packageFormal part เป็นสัญญาของชนิด ค่า และฟังก์ชัน ไปเลี้ยงทั้ง spec ที่ผู้ใช้เห็น และ body ที่ซ่อนจากผู้ใช้ซ่อนpackage specAPI สาธารณะฝั่งผู้ใช้package bodyอิมพลีเมนต์ภายในgeneric formal partสัญญาของชนิด ค่า ฟังก์ชัน

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;
ต้องใส่ฟังก์ชันเปรียบเทียบในสัญญาถ้า body ต้องการเปรียบเทียบต้องเขียนฟังก์ชันเปรียบเทียบใน formal part เพื่อตรวจตอน instantiate ถ้ามีแค่ private จะคอมไพล์ body ไม่ผ่านอยากใช้การเปรียบเทียบใน bodyเขียนฟังก์ชันเปรียบเทียบใน formal partยืนยันว่าเปรียบเทียบได้ตอน instantiateมีแค่ privateคอมไพล์ 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

exception ของแต่ละ instance เป็นคนละตัวStack_Overflow ที่ประกาศใน Generic_Stack กลายเป็น Int_Stack.Stack_Overflow และ Float_Stack.Stack_Overflow ซึ่งเป็นคนละ exceptionคนละตัวGeneric_Stackประกาศ Stack_OverflowInt_Stack.Stack_OverflowFloat_Stack.Stack_Overflow

15.4 ขนาดโค้ดอาจเพิ่มขึ้น

เจเนอริกเลี่ยงการอ้างอิงทางอ้อมที่เกินจำเป็นตอนรันได้ง่าย ในทางกลับกัน เพราะสร้าง instance ต่อชนิด ถ้าจำนวน instance มาก ขนาดโค้ดอาจโต

นี่คือ trade-off เดียวกับ template ของ C++ และการ monomorphize ของ Rust ในงานความน่าเชื่อถือสูง งานฝังตัว และงานใกล้เรียลไทม์ มักแลกด้วยการลดความไม่แน่นอนตอนรัน แล้วไปจัดการขนาดสิ่งที่ generate ตอน build

ขนาดโค้ดเมื่อมีหลาย instanceGeneric body เดียว generate โค้ด Integer Float Long_Float และ My_Type ข้อดีคือเลี่ยงการตรวจชนิดตอนรัน ข้อเสียคือ instance เยอะแล้วขนาดโตgeneric body เดียวฉบับ Integerฉบับ Floatฉบับ Long_Floatฉบับ My_Typeโค้ดที่ถูก generateเลี่ยงการตรวจชนิดตอนรันและการ boxing ได้ง่ายถ้า 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;
Generic_Min ต้องการฟังก์ชันเปรียบเทียบชนิด Element ต้องมีฟังก์ชันเปรียบเทียบ จึงให้ Generic_Min คืนค่าที่เล็กกว่าได้Elementต้องการฟังก์ชันเปรียบเทียบGeneric_Minคืนตัวที่เล็กกว่า

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, และบัฟเฟอร์สำหรับทดสอบ

ฉีด Put เพื่อสลับช่องทางส่งออกGeneric_Print_Twice รับ Put เป็น formal subprogram แล้วชี้ไปคอนโซล log หรือบัฟเฟอร์ทดสอบได้Generic_Print_Twiceรับ Put เป็น formal subprogramส่งออกคอนโซลส่งออกเป็น logบัฟเฟอร์สำหรับทดสอบ

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

ไม่ตรึงชนิดดัชนีของอาร์เรย์Index ที่เป็น discrete ใช้ได้ทั้งช่วง Positive enumeration ชื่อ Day หรือ State และชนิดจำนวนเต็มที่กำหนดเองIndex is discretePositive rangeenumeration Dayenumeration Stateชนิดจำนวนเต็มที่กำหนดเอง

17. เช็กลิสต์ให้ API เป็นแบบ Ada

ตอนเขียน generic ทบทวนด้วยมุมนี้ท้ายสุด จะอ่านง่ายขึ้น

เช็กลิสต์ออกแบบ genericตรวจว่า formal parameter น้อยที่สุด ปฏิบัติการที่ต้องใช้ถูกเขียนใน formal part หมวดชนิดเหมาะสม instantiate อ่านง่าย และรับได้ทั้งสถานะ exception และขนาดโค้ดเช็คการออกแบบ genericformal parameter น้อยที่สุดหรือยังเขียนปฏิบัติการที่ต้องใช้ใน formal part แล้วหรือยังหมวดอย่าง private / range / digits เหมาะหรือยังinstantiate อ่านง่ายด้วย named association ได้หรือยังคิดถึงสถานะและ exception ต่อ instance หรือยังรับการโตของขนาดโค้ดได้หรือยังมี instance สำหรับทดสอบหรือยัง

สรุปเป็นข้อความได้ดังนี้

  • ปฏิบัติการที่ body ใช้ ต้องมองเห็นได้เป็นสัญญาของ formal parameter เสมอ
  • ถ้า private พอก็ใช้ private ถ้าต้องการเลขคณิต ให้ใช้ range <> หรือ digits <>
  • พฤติกรรมที่เปลี่ยนตามชนิด เช่น เปรียบเทียบ แฮช ส่งออก แปลง ให้เป็น formal subprogram
  • ถ้าขนาดหรือเกณฑ์เป็นสมบัติของ instance ให้เป็น value parameter
  • ถ้ามีสถานะ ให้คิด generic package ก่อน ถ้าไม่มีสถานะ ให้คิด generic subprogram ก่อน
  • ตอน instantiate อาร์กิวเมนต์ยิ่งเยอะ ยิ่งใช้ named association
  • ออกแบบโดยสมมติว่า exception และสถานะภายในเป็นอิสระต่อ instance

18. ตัวอย่างโครงสร้างทั้งชุดของตัวอย่าง

ถ้าแยกตัวอย่างในบทความเป็นไฟล์ โครงแบบนี้จะอ่านง่าย

โครงไฟล์ตัวอย่างทั้งชุดราก ada-generic-programming มี src/generics สำหรับ spec กับ body และ src/demos สำหรับโปรแกรมสาธิตแต่ละหัวข้อada-generic-programmingsrcgenericsdemosgeneric_swap.adsgeneric_swap.adbgeneric_stack.adsgeneric_stack.adbgeneric_insertion_sort.adsgeneric_insertion_sort.adbgeneric_statistics.adsgeneric_statistics.adbgeneric_count_if.adsgeneric_count_if.adbgeneric_kv_store.adsgeneric_kv_store.adbswap_demo.adbstack_demo.adbsort_demo.adbstatistics_demo.adbcount_if_demo.adbkv_demo.adb

ตัวอย่างเล็กสำหรับบทความ รวมในไฟล์เดียวแล้ว gnatchop ก็สะดวก แต่ถ้าคิดงานจริงและการบำรุงระยะยาว การแยก specification .ads กับ body .adb จะเป็นโครงแบบ Ada กว่า

19. สรุป — กำหนดขอบเขตการนำกลับมาใช้ใหม่ด้วยชนิดข้อมูล

Generic programming ของ Ada ไม่ใช่แค่ “ฟีเจอร์เขียนโค้ดที่ไม่ผูกกับชนิด” สาระจริงคือ เขียนให้ชัดว่าชิ้นส่วนที่นำกลับมาใช้ใหม่ได้เรียกร้องอะไร เป็นสัญญาของชนิดข้อมูล ซับโปรแกรม และค่า

จากสัญญาถึงการนำกลับมาใช้ใหม่เขียนสัญญา แล้วเขียน generic body ส่งชนิด ค่า ฟังก์ชันเพื่อ instantiate ใช้แบบ type-safe แล้วนำกลับมาใช้ใหม่โดยไม่คัดลอกเขียนสัญญาเขียน generic bodyส่งชนิด ค่า ฟังก์ชันแล้ว instantiateใช้แบบ type-safeนำกลับมาใช้ใหม่โดยไม่คัดลอก

อย่างที่เห็นในบทความนี้ เจเนอริกของ 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 เดิมอย่างไร เพื่อยืดอายุหรือย้ายระบบ

อ้างอิง

  • 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

บทความล่าสุดที่มีแท็กเดียวกัน เพื่อเจาะลึกหัวข้อใกล้เคียง

โปรแกรมมิงระบบเรียลไทม์ด้วย 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...

หน้าเหล่านี้วางหัวข้อไว้ในบริบทที่กว้างขึ้นของบริการและการตัดสินใจ

คำถามที่พบบ่อย

คำถามที่มักพบในการปรึกษาเกี่ยวกับหัวข้อของบทความนี้

เจเนอริกของ 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

โปรไฟล์ผู้เขียน

หน้าแนะนำผู้เขียนบทความ

Go Komura

ผู้แทนของ KomuraSoft LLC

เชี่ยวชาญด้านการพัฒนาซอฟต์แวร์ Windows ที่ปรึกษาเทคนิค และการตรวจสอบบั๊ก โดยเฉพาะโปรเจกต์ที่มีระบบเดิมและบั๊กที่ทำซ้ำได้ยาก

ลิงก์สาธารณะ

กลับไปยังบล็อก