Lập trình generic trong Ada — Viết hợp đồng bằng kiểu, tái sử dụng với chi phí zero

· Cập nhật ngày: · · Ada, Ngôn ngữ lập trình, Generics, Hệ thống kiểu, Static typing, Contract model, Zero-cost abstraction, GNAT, Alire, Độ tin cậy cao, Tái sử dụng mã

1. Mở đầu — Không phải “nhận mọi thứ”, mà là “hứa điều gì”

Khi bạn cố viết mã tái sử dụng trên ngôn ngữ static typing, bạn sớm đụng cùng một khó khăn. Stack viết cho số nguyên thì cũng muốn dùng với chuỗi. Muốn dùng cùng xử lý thống kê cho mảng floating-point. Muốn dùng logic sort tăng dần cho sort giảm dần nữa. Thế nhưng copy cùng mã theo từng kiểu thì sẽ sót chỗ sửa. Ngược lại, thiết kế nhận mọi thứ qua void* hay cast thì an toàn kiểu sụp.

Câu trả lời của Ada là generic (generic units).

Generic Ada không phải thay thế văn bản. Nó nhận kiểu, giá trị, subprogram, thậm chí cả package làm formal parameter, rồi kiểm tra kiểu tĩnh tại thời điểm instantiation. Nói cách khác, không phải lúc runtime mới hỏi “kiểu này có ổn không”, mà là cơ chế chốt lúc biên dịch “linh kiện này có thỏa hợp đồng này không”.

Ba cách tái sử dụng mãSo sánh copy-paste, void-star hoặc cast, và generic Ada về sót sửa, lỗi runtime, và kiểm tra lúc biên dịchLogic muốn tái sử dụngTái sử dụng thế nào?Copy và pastevoid* / Object / castGeneric AdaDễ sót chỗ sửaDễ lỗi runtime hoặc mất an toàn kiểuAn toàn kiểuKiểm tra lúc biên dịchKhông có dispatch thừa lúc runtime

Bài viết này sắp xếp lập trình generic trong Ada theo luồng sau.

  • Generic subprogram
  • Generic package
  • Type parameter, value parameter, subprogram parameter
  • Type category như private, range <>, digits <>
  • Ví dụ triển khai: sort, stack, thống kê, Count_If, key-value store
  • Higher-order generic qua formal package parameter
  • Contract model của Ada và cách nghĩ thiết kế thực tiễn

1.1 Đối tượng đọc và những gì bạn mang về được

Bài viết hướng tới những người như sau.

  • Đã dùng template C++, generic C# hoặc Java, hoặc generic Rust
  • Chưa thuộc cú pháp Ada nhưng quan tâm thiết kế “viết hợp đồng bằng kiểu”
  • Đang cân nhắc chính sách thiết kế linh kiện tái sử dụng trong bảo trì dài hạn, embedded, hoặc độ tin cậy cao

Về cú pháp Ada, biết package (tách spec .ads và body .adb) cùng parameter mode in / out / in out là đủ để đọc tiếp. Nếu hai điểm đó còn lung túng, đọc trước “Sức hấp dẫn của ngôn ngữ Ada” sẽ nhanh hơn.

Điều mang về không phải bản thân cú pháp Ada, mà là cách làm thiết kế: viết ra bằng kiểu, trước khi triển khai, rằng linh kiện tái sử dụng đòi gì. Cách nghĩ này áp dụng nguyên vẹn khi bạn quyết viết interface constraint của C# hay concepts của C++20 tới mức nào.

1.2 Hướng dẫn đọc — Không cần đọc hết

Bài có 20 chương. Không cần đọc thẳng từ đầu; có thể nhặt theo mục đích như sau.

Mục đích Đọc chương
Chỉ nắm cách nghĩ trong thời gian ngắn Chương 4 (mô hình cơ bản) → chương 6 (generic subprogram tối thiểu) → chương 13 (contract model)
Muốn tự viết linh kiện Chương 4 → chương 6 → chương 7 (generic package) → chương 8 (tiêm hành vi) → chương 9 (type category)
Chỉ cần kim chỉ nam quyết định thiết kế Chương 13 → chương 14 (cái gì nên thành generic) → chương 15 (chỗ dễ vấp) → chương 17 (danh sách kiểm tra)
Muốn chạy tay Chương 3 dựng môi trường, rồi sample hoàn chỉnh ở chương 6 và 7

Lộ trình ngắn nhất là ba chương 4, 6 và 13. Ba chương đó đủ formal parameter, instantiation, và contract model. Chương 5 và 9 là danh sách formal parameter; khi cần thì tra như từ điển.

Chủ đề này nối tiếp loạt bài “Sức hấp dẫn của ngôn ngữ Ada”, “Nhập môn formal verification với SPARK”, “Xử lý đồng thời an toàn”, “Hệ thống thời gian thực”. Ada “nói thiết kế bằng kiểu”; bài này đào góc generic.

Bản đồ tri thức của bài viết này

Generic của Ada là cơ chế tái sử dụng nhận kiểu, giá trị, subprogram và package làm formal parameter, rồi kiểm tra kiểu tĩnh tại thời điểm instantiation bằng new. Generic Subprogram và Generic Package là hai trụ cột của cơ chế này: Formal Type nêu tường minh hợp đồng các thao tác mà body được phép dùng, qua các category như private, range<> và digits<>; Formal Subprogram đưa vào hành vi như so sánh hay predicate; Formal Package nhận generic package đã được instantiate làm linh kiện. Nhờ contract model kiểm body độc lập chỉ trong phạm vi các formal parameter, vấn đề lỗi chỉ lộ khi instantiate mà template C++ từng gặp trong lịch sử, ở Ada thì về cấu trúc không xảy ra. Có GNAT và Alire thì có thể thử cơ chế này trong môi trường miễn phí.

Bản đồ tri thức lập trình generic của AdaSơ đồ cho thấy Generic Subprogram và Generic Package được xây trên hợp đồng (contract model) của các formal parameter kiểu, giá trị, subprogram và package, việc kiểm hợp đồng đó lúc instantiation bằng new, cùng khác biệt với vấn đề lỗi chỉ lộ khi instantiate mà template C++ từng gặp trong lịch sửtriển khaisử dụngsử dụngyêu cầusử dụngsử dụngyêu cầucấu hình bằngyêu cầuyêu cầutriển khaitriển khaingăn chặncó thể gâyyêu cầutriển khaicấu hình bằngsử dụngsử dụngyêu cầutriển khaiGeneric của Ada (generic unit)Contract model của generic AdaAda (ngôn ngữ lập trình)Generic SubprogramGeneric PackageFormal Type (tham số kiểu)Formal Subprogram (tham số subprogram)Formal Package (tham số package)Instantiation (khởi tạo generic)Formal Object (tham số giá trị)Lỗi template chỉ phát hiện lúc instantiationTemplate C++GNATAlire

Trong sơ đồ, đường liền nét biểu thị quan hệ luôn đúng và đường nét đứt biểu thị quan hệ có điều kiện (điều kiện nằm trong phần giải thích từng quan hệ trên trang chi tiết). Danh sách đầy đủ các quan hệ (tổng 21, kèm bằng chứng và mức chắc chắn) cùng định nghĩa các khái niệm chính được tập hợp tại trang chi tiết bản đồ tri thức (bằng tiếng Nhật). Dữ liệu: JSON-LD / Turtle

2. Bản đồ bài viết này

Trước hết nắm toàn cảnh bằng hình. Nếu hiểu generic Ada chỉ là “tính năng nhận kiểu làm đối số”, góc nhìn đó hẹp. Thực tế, tùy đơn vị muốn tái sử dụng, bạn kết hợp subprogram, package, subprogram parameter, value parameter, và formal package parameter.

Ada GenericsGeneric SubprogramSwapCount_IfSortGeneric PackageStackStatisticsKV StoreFormal ParametersTypeprivatelimited privaterange boxmod boxdigits boxdelta boxdiscrete boxObjectMax_SizeThresholdSubprogramLess functionEquals functionPredicatePackagewith package P is newGenericDesign IdeasContract ModelStatic CheckingZero-Cost AbstractionSeparate Specification andBody

Trong hình, box chính là <> (box compound delimiter) của Ada. Mermaid không vẽ <> nguyên si nên trong hình thôi ta viết box.

Cách đọc bài này đơn giản. Nửa đầu đi theo cú pháp; nửa sau xử lý phán đoán thiết kế. Nếu mới đọc Ada, đừng cố nhớ cú pháp nhỏ: hãy chú ý “đang lấy gì làm formal parameter”“formal parameter đó cho phép thao tác nào”.

2.1 Từ điển thuật ngữ ngắn

Những từ sẽ lặp lại được đối chiếu Anh–Việt trước. Tài liệu Ada tiếng Nhật thường dịch generic bằng một từ Hán-Việt nghĩa là “tổng quát”; bài tiếng Việt giữ từ generic và nói generic subprogram, generic package — nghĩa vẫn là “mang tính generic”.

Ký hiệu trong bài Tiếng Anh Nghĩa
generic unit generic unit Khai báo bắt đầu bằng generic. Tên chung cho generic subprogram và generic package
formal parameter generic formal parameter Đối số phía nhận, viết giữa generic và thân khai báo. Là kiểu, giá trị, subprogram, hoặc package
formal part generic formal part Chính đoạn liệt kê formal parameter. Có thể đọc là “chỗ viết hợp đồng”
actual parameter generic actual parameter Kiểu, giá trị, subprogram, package thực sự truyền lúc instantiate
instantiation instantiation Dùng new để tạo subprogram hoặc package thường từ generic unit
box <> Tên gọi ký hiệu <> trong Ada. Như range <>, nghĩa là “kiểu cụ thể chốt lúc instantiate”
contract model contract model Cách của Ada: viết body chỉ trong phạm vi lời hứa ở formal parameter, rồi kiểm kiểu body độc lập (chương 13)

Trong văn bản ta viết đúng ký pháp Ada range <>, digits <>, nhưng trong hình thôi ta viết range box, digits box. Mermaid không vẽ nhãn chứa <> nguyên si; nghĩa vẫn như văn bản. Như bảng trên, chính Ada gọi <> là box, nên ký hiệu trong hình không lệch thuật ngữ Ada.

3. Môi trường chạy và cách biên dịch

Mã trong bài giả định GNAT 15.x trở lên. GNAT là trình biên dịch đại diện của Ada, cài được từ Alire. Alire là package manager của Ada / SPARK, cũng dùng để quản lý toolchain và build.

gnat --version
# GNAT 15.2.1

Cài GNAT từ Alire (package manager của Ada) bằng alr install gnat_native gprbuild, rồi đưa binary vào PATH.

Sample trong bài được đặt trong repo như sau.

Bố cục sample của bài viếtThư mục ada-generic-programming chứa src/snippets với sáu file .ada và README.mdada-generic-programming/src/snippets/01_swap.ada02_stack.ada03_sort.ada04_statistics.ada05_filter.ada06_kv_store.adaREADME.md

Sample gộp nhiều compilation unit trong một file thì tách bằng gnatchop rồi mới gnatmake.

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

-gnata bật assertion. Không bắt buộc chỉ để dùng generic, nhưng với sample học thì dễ kiểm hợp đồng và điều kiện biên.

Quy trình biên dịch samplegnatchop tách file .ada rồi gnatmake biên dịch và chạy file thực thiFile thực thignatmakegnatchopNhà phát triểnFile thực thignatmakegnatchopNhà phát triểnĐưa một file .adaTách thành .ads / .adb / maingnatmake -gnata mainBind và link xong./mainKết quả chạy

4. Mô hình cơ bản của generic Ada

Generic Ada dễ nắm nếu nghĩ thành ba bước.

  1. Viết generic unit
  2. Viết formal parameter ở phần generic
  3. Phía dùng instantiate bằng new
Mô hình cơ bản của generic AdaKhai báo generic, formal parameter, body, rồi instantiation bằng new để dùng như subprogram hoặc package thườngkhai báo genericformal parametergeneric bodyinstantiation bằng newDùng như subprogram hoặc package thườngKiểuGiá trịSubprogramPackage

Ví dụ, biến thao tác hoán đổi hai giá trị thành generic thì chỉ cần kiểu làm formal parameter.

generic
   type Element is private;
procedure Generic_Swap (A, B : in out Element);

Ở thời điểm này Generic_Swap chưa gọi được. Đây là “template hoán đổi dùng được với mọi kiểu Element”. Chỉ khi đưa kiểu cụ thể, nó mới thành procedure thường.

procedure Swap_Integer is new Generic_Swap (Integer);

Vẽ quan hệ thì như sau.

Instantiation Generic_Swap theo từng kiểuCùng template Generic_Swap tạo Swap_Integer, Swap_Character, Swap_My_Record tùy kiểu truyền vàoTruyền IntegerTruyền CharacterTruyền My_RecordGeneric_Swaptype Element is privateSwap_IntegerSwap_CharacterSwap_My_RecordHoán đổi biến IntegerHoán đổi biến CharacterHoán đổi biến My_Record

Điểm quan trọng: template body chỉ viết bằng thao tác được phép trên Element. Khi khai type Element is private;, gán và so sánh bằng thì dùng được, nhưng so sánh lớn/nhỏ hay số học thì không. Bản thân khai báo generic đã nói “linh kiện này được phép giả định gì”.

5. Các loại formal parameter — Từ vựng của generic Ada

Generic Ada không chỉ nhận kiểu. Đây là điểm khác lớn so với generic thông thường của C# hay Java.

Bốn loại formal parameter của generic AdaGeneric Ada nhận formal parameter kiểu, giá trị, subprogram, và package chứ không chỉ kiểugeneric 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 ...

Trong hình, box chính là <> (box compound delimiter) của Ada. Mermaid không vẽ <> nguyên si nên trong hình thôi ta viết box.

Các formal parameter tiêu biểu, xếp bảng thì như sau.

Loại Ví dụ Nghĩa
Type parameter type Element is private; Dạng cơ bản nhận mọi kiểu definite, không limited
Limited type parameter type Element is limited private; Cũng nhận kiểu không copy được
Discrete type type Index is (<>); Kiểu dùng làm chỉ số mảng, như integer hay enumeration
Signed integer type Count is range <>; Được giả định +, -, so sánh lớn/nhỏ, số học nguyên
Modular integer type Word is mod <>; Bit và số nguyên theo modulo
Floating-point type Real is digits <>; Float, Long_Float, floating-point do người dùng định nghĩa, v.v.
Fixed-point type Money is delta <>; Số học fixed-point
Value parameter Max_Size : Positive; Cố định kích thước, ngưỡng, v.v. theo từng instance
Subprogram with function Predicate (...) return Boolean; Tiêm hành vi như hàm so sánh hay predicate
Package with package P is new Some_Generic (<>); Nhận generic package đã instantiate như một linh kiện

Vì có bộ từ vựng này, Ada viết tự nhiên theo kiểu “chỉ nhận kiểu làm được thao tác này”, chứ không phải “nhận mọi thứ rồi làm điều nguy hiểm bên trong”.

6. Generic subprogram — Hiểu cấu trúc tối thiểu với Generic_Swap

Ví dụ đầu: Generic_Swap hoán đổi hai biến thuộc kiểu bất kỳ.

6.1 Phần spec

generic
   type Element is private;
procedure Generic_Swap (A, B : in out Element);

Phần sau generic là formal parameter. Ở đây nhận một kiểu tên Element. is private nghĩa là, nhìn từ generic body, không biết biểu diễn nội bộ của kiểu đó.

Khai báo này cho biết hai điều.

  • Generic_Swap dùng được với mọi kiểu Element
  • Body không phụ thuộc cấu trúc nội bộ hay so sánh lớn/nhỏ của Element

6.2 Phần body

procedure Generic_Swap (A, B : in out Element) is
   Temp : constant Element := A;
begin
   A := B;
   B := Temp;
end Generic_Swap;

Body này chỉ dùng gán trên Element. Không có A < B hay A + B. Vì thế mọi kiểu gán được — Integer, Character, record, enumeration — đều dùng tự nhiên.

Luồng hoán đổi Generic_SwapTemp nhận A, A nhận B, rồi B nhận Temp, hai biến đổi chỗ sau lời gọiSau khi gọiTrước khi gọiA = 20B = 10A = 10B = 20Temp = A

6.3 Instantiation

Phía dùng thì dùng new.

procedure Swap_Int  is new Generic_Swap (Integer);
procedure Swap_Char is new Generic_Swap (Character);

Swap_IntSwap_Char giờ gọi được như procedure thường.

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;

Kết quả chạy như sau.

Before: X= 10, Y= 20
After : X= 20, Y= 10

Không thể đưa biến Float vào Swap_Int (X, Y);. Swap_Int đã là procedure thường, instantiate cho Integer. Generic không phải “lỗ nhận mọi thứ”, mà là “cơ chế tạo ra thứ cụ thể, an toàn, theo từng kiểu”.

7. Generic package — Tham số hóa kiểu và giá trị

Khi muốn tái sử dụng không chỉ một subprogram mà cả cụm thao tác cùng trạng thái nội bộ, dùng generic package. Ví dụ điển hình là stack.

Với stack, chỉ cần đổi kiểu phần tử và kích thước tối đa; logic cơ bản vẫn giống.

Generic_Stack nhận kiểu và kích thướcGeneric_Stack lấy Element_Type và Max_Size rồi sinh các instance stack Integer, Float, Unbounded_StringGeneric_StackElement_TypeMax_SizePush / Pop / Size / Is_Empty / Is_FullInt_StackElement=IntegerMax_Size=5Float_StackElement=FloatMax_Size=3String_StackElement=Unbounded_StringMax_Size=20

7.1 Phần spec

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;

Ở đây dùng hai loại formal parameter.

  • Element_Type là type parameter
  • Max_Size là value parameter

Max_Size thuộc Positive, nên không instantiate được với kích thước 0 trở xuống. Value parameter cũng mang ràng buộc từ kiểu như vậy.

7.2 Phần 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;

Điểm quan trọng của package body: DataTop được tạo riêng từng instance.

package Int_Stack   is new Generic_Stack (Integer, 5);
package Float_Stack is new Generic_Stack (Float,   3);

Hai cái này sinh từ cùng template nhưng không chia sẻ trạng thái nội bộ.

Mỗi instance stack có trạng thái riêngInt_Stack và Float_Stack sinh từ cùng Generic_Stack nhưng không chia sẻ Top hay DataTrạng thái của Float_StackTopData : mảng FloatTrạng thái của Int_StackTopData : mảng IntegerGeneric_StackInt_StackFloat_Stack

7.3 Chuyển trạng thái của stack

Stack dễ hiểu nếu nhìn như máy trạng thái.

Máy trạng thái của stackStack chuyển Empty, NonEmpty, Full, Overflow, Underflow theo Push và PopPushPush / PopPop lấy phần tử cuốiPush tới Max_SizePopPushPopEmptyNonEmptyFullOverflowUnderflow

7.4 Ví dụ dùng

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 và chạy theo cùng bước ở chương 3.

gnatchop ../src/snippets/02_stack.ada
gnatmake -gnata stack_demo
./stack_demo
Size= 3
Pop = 30
Pop = 20
Size= 1

Khoảng trắng một ký tự bên phải = là vì 'Image của kiểu nguyên đặt một khoảng trắng trước giá trị không âm. Push ba lần, Pop hai lần, nên Size cuối là 1. Nếu stack đầy (trong ví dụ này là 5 phần tử, Max_Size) mà vẫn Push thì Int_Stack.Stack_Overflow được raise; nếu rỗng mà Pop thì Int_Stack.Stack_Underflow.

Generic package trong thực tế hay phát huy ở “container nhỏ”, “buffer độ dài cố định”, “ring buffer”, “hàng đợi log”, “hardware abstraction layer”. Đặc biệt với Ada, thiết kế cố định kích thước tĩnh bằng type hoặc value parameter, hơn là đẩy kích thước lúc runtime, hợp với hệ thống độ tin cậy cao.

8. Formal subprogram parameter — Tiêm hành vi

Chỉ nhận kiểu thì vẫn còn điều chưa biểu đạt được. Sort chẳng hạn không chỉ cần kiểu phần tử mà còn cần logic so sánh: “cái nào đứng trước”.

Trong Ada, hàm so sánh đó có thể là formal parameter của generic.

Formal parameter của Generic_Insertion_SortSort generic nhận kiểu phần tử, index, mảng, và hàm so sánh để đổi thứ tự tăng hoặc giảmGeneric_Insertion_SortItem_TypeIndexItem_ArrayHàm so sánhDùng so sánh chuẩnTruyền Greater để giảm dầnTruyền thứ tự riêng

8.1 Phần spec

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);

Có bốn formal parameter.

  1. Item_Type: kiểu phần tử mảng
  2. Index: kiểu chỉ số mảng
  3. Item_Array: kiểu mảng thực sự
  4. "<": hàm so sánh

type Index is (<>); nhận discrete type. Không chỉ integer mà cả enumeration. Chỗ mang dấu Ada: chỉ số mảng không chỉ Positive mà còn enumeration như Day.

is <> trong with function "<" ... is <>; nghĩa là, nếu bỏ actual parameter, dùng toán tử chuẩn đang visible hoặc hàm khớp. Kiểu như Integer vốn đã có < thì không cần nêu hàm so sánh.

8.2 Phần 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 không hợp mảng lớn, nhưng hợp để giải thích generic: chỉ thay hàm so sánh là cùng cấu trúc vòng lặp dùng được cho cả tăng dần và giảm dần.

Luồng insertion sortLấy Key từ trái sang, dịch phần tử đứng trước khi Key đứng trước, rồi chèn Key cho đến hết mảngYesNoNoYesMảng chưa sắpLần lượt lấy Key từ tráiKey đứng trước phần tử ngay trước?Dịch phần tử ngay trước sang phảiChèn KeyXử lý hết chưa?Mảng đã sắp

8.3 Tạo tăng dần và giảm dần từ cùng một 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 dùng < chuẩn. Sort_Desc thì "<" => Greater, thay hàm so sánh.

Cùng body, hai thứ tự nhờ đổi hàm so sánhSort_Asc dùng so sánh chuẩn, Sort_Desc truyền Greater, nên cùng dữ liệu ra hai thứ tự ngược nhau99, 3, 47, 12Sort_Ascso sánh chuẩnSort_Desctruyền Greater làm hàm so sánh3, 12, 47, 9999, 47, 12, 3

Cơ chế này gần với việc truyền comparison function object làm template argument trong C++, hoặc đòi thứ tự bằng trait bound trong Rust. Khác là Ada nêu tường minh, như formal subprogram parameter, rằng “truyền hàm dạng này”.

8.4 Ví dụ chạy

Theo cấu trúc chương 18, nếu đặt Generic_Insertion_Sort ở file riêng (generic_insertion_sort.ads / .adb), phía gọi như sau.

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

Hai procedure sinh từ cùng body Generic_Insertion_Sort đảo thứ tự chỉ vì thay hàm so sánh. Khoảng trắng trước mỗi phần tử là vì 'Image của kiểu nguyên đặt một khoảng trắng trước giá trị không âm.

9. Type category — Viết hợp đồng cụ thể hơn private

type T is private; tiện, nhưng không làm được mọi thứ. Trên kiểu private, không mặc nhiên dùng bốn phép tính hay so sánh lớn/nhỏ. Vì thế Ada cho phép chỉ định category trên formal type parameter.

Các category của formal typeFormal type có thể là private, limited private, discrete, range, mod, digits, delta, hoặc accessFormal Typeprivatelimited privatediscrete box: discrete typerange box: signed integermod box: modular integerdigits box: floating-pointdelta box: fixed-pointkiểu accessEnumerationIntegerFloatLong_FloatFloating-point do người dùng định nghĩa

Trong hình, box chính là <> (box compound delimiter) của Ada. Mermaid không vẽ <> nguyên si nên trong hình thôi ta viết box.

9.1 Chỉ định category thì được gì

Ví dụ tính trung bình hay phương sai cần cộng, trừ, nhân, chia. Kiểu private không cho giả định các phép đó. Vì thế ta giới hạn ở floating-point.

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 <>; cho biết Real là floating-point. Generic body vì thế dùng được +, -, *, /.

9.2 Phần 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 Dùng với FloatLong_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);

Cùng xử lý thống kê tái sử dụng được trên floating-point khác độ chính xác.

Generic_Statistics cho nhiều độ chính xácCùng statistics package instantiate cho Float, Long_Float, và Real do người dùng định nghĩaGeneric_StatisticsReal is digits boxFloat_StatsLong_StatsMy_Real_StatsMean / Variance trên FloatMean / Variance trên Long_FloatMean / Variance trên Real do người dùng định nghĩa

9.4 Ví dụ chạy

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 là thư viện chuẩn instantiate Ada.Text_IO.Float_IO cho Float. Exp => 0 in dạng thập phân thường chứ không phải dạng mũ; Aft => 3 là số chữ số phần thập phân. Lưu ý Variance này là phương sai tổng thể (chia cho Values'Length), không phải phương sai mẫu.

9.5 Chỉ định category là “đặc tả cấp kiểu”

Category không chỉ là cú pháp để khiến compiler im lặng. Với người đọc, nó còn là đặc tả: linh kiện này đòi gì.

Xử lý muốn viết Formal type hợp Lý do
Hoán đổi, lưu, lấy ra private Gán được là đủ
Quản lý tài nguyên không copy được limited private Không giả định gán
Chỉ số mảng, duyệt trạng thái enumeration (<>) Dùng được First, Last, Succ, Pred
Tổng nguyên, bộ đếm range <> Giả định số học nguyên
Bitmask, bộ đếm vòng mod <> Giả định phép modular
Trung bình, phương sai, tính số digits <> Giả định số học floating-point
Tiền, đại lượng điều khiển với độ chính xác cố định delta <> Giả định số học fixed-point

10. Tiêm predicate — Viết Count_If theo cách Ada

Formal subprogram parameter không chỉ dùng cho hàm so sánh mà còn cho predicate. Predicate là hàm nhận giá trị rồi trả Boolean.

Vai trò gần Func<T, bool> của C#, Predicate<T> của Java, lambda hay function object của C++ — trong Ada được viết như formal subprogram của generic.

10.1 Phần spec

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;

Ở đây Predicate không gắn is <>. Không có hàm predicate chuẩn nào đang visible, nên thiết kế bắt phía dùng phải truyền.

10.2 Phần 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;

Luồng xử lý đơn giản.

Luồng Generic_Count_IfDuyệt mảng, tăng Count khi Predicate đúng, bỏ qua khi sai, rồi trả CountTrueFalseMảngDuyệt từng phần tửPredicate(Item)?Tăng CountKhông làm gìSang phần tử tiếpTrả Count

10.3 Đếm số chẵn và đếm theo ngưỡng

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);

Từ cùng logic duyệt, tạo hai hàm chỉ khác điều kiện.

Cùng vòng lặp, hai predicate khác nhauCount_Even đếm số chẵn, Count_Large đếm phần tử lớn hơn 50, cùng Generic_Count_IfGeneric_Count_IfCount_EvenPredicate = Is_EvenCount_LargePredicate = Is_Large12, 7, 88, 3, 56, 91, 44, 19, 62Số phần tử chẵnSố phần tử lớn hơn 50

Trong ví dụ này, duyệt mảng, quản lý bộ đếm, trả kết quả đều chung. Chỉ “đếm cái gì” được tiêm vào như hàm. Đó là dạng cơ bản của thiết kế higher-order trong Ada.

10.4 Ví dụ chạy

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

Trong 9 phần tử của Data, số chẵn là 12, 88, 56, 44, 62 — 5 cái; lớn hơn 50 là 88, 56, 91, 62 — 4 cái.

11. Ghép nhiều tham số — Key-value store tổng quát

Linh kiện thực tế ít khi xong với đúng một type parameter. Kiểu khóa và giá trị, cách so sánh khóa, số mục tối đa — nhiều điều kiện phải ghép.

Ở đây lấy key-value store cố định độ dài, đơn giản, làm ví dụ.

Generic_KV_Store ghép nhiều formal parameterStore nhận Key_Type, Value_Type, hàm so khớp khóa, và Max_Entries cho Put, Get, ContainsGeneric_KV_StoreKey_TypeValue_TypeHàm khớp khóaMax_EntriesPut / Get / ContainsStore giá trị cấu hìnhCache quy mô nhỏTừ điển độ dài cố định cho embedded

11.1 Phần spec

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 này có bốn formal parameter.

Tham số Loại Vai trò
Key_Type Kiểu Kiểu khóa
Value_Type Kiểu Kiểu giá trị
"=" Subprogram Xác định khóa có khớp
Max_Entries Giá trị Số mục tối đa

Max_Entries có mặc định := 50. Không chỉ định thì thành store 50 mục.

11.2 Phần 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;

Cài đặt này là tìm tuyến tính, không dành cho dữ liệu lớn. Nhưng ở chỗ độ dài cố định, quy mô nhỏ, không cấp phát bộ nhớ động là tính chất quan trọng, dạng này dễ dùng.

Luồng Put và Get của KV storePut ghi đè khi khóa đã có, cấp slot mới khi khóa mới, Get tìm index rồi trả giá trịKeys/Values/UsedInstance Generic_KV_StorePhía gọiKeys/Values/UsedInstance Generic_KV_StorePhía gọialt[Khóa đã có][Khóa mới]Put(Key, Value)Find_Index(Key)Values(Pos) := ValueFind_FreeKeys(Pos) := KeyValues(Pos) := ValueUsed(Pos) := TrueGet(Key)Find_Index(Key)PosValues(Pos)

11.3 Ví dụ 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 trên khóa đã có sẽ ghi đè
   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)

Gọi Get mà không Contains trước thì, khi không có khóa, Key_Not_Found được raise. Chọn một trong hai: rẽ nhánh bằng Contains như trên, hoặc viết exception handler.

"=" được lược bỏ. Integer có toán tử bằng chuẩn, và is <> dùng toán tử đó.

Nếu khóa là chuỗi không phân biệt hoa thường chẳng hạn, truyền hàm bằng riêng.

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 — Biến generic thành linh kiện nhỏ hơn

Generic Ada cho phép chính package làm formal parameter. Nhờ đó, “một instance sinh từ generic package nào đó” trở thành đầu vào của generic khác.

Logger nhận instance của Generic_StackInt_Stack instantiate từ Generic_Stack rồi được truyền vào Generic_Stack_LoggerGeneric_StackInt_StackGeneric_Stack_LoggerInt_Stack_Logger_Instance

12.1 Logger nhận stack

Ví dụ: nhận instance của Generic_Stack ở trên, rồi làm logger in kích thước.

generic
   with package Stack is new Generic_Stack (<>);
package Generic_Stack_Logger is
   procedure Print_Size;
end Generic_Stack_Logger;

Body như sau.

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;

Phía dùng thì tạo stack trước, rồi đưa stack đó cho 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);

Thiết kế này cho phép ghép các linh kiện generic với nhau.

Generic lồng nhau qua formal packageTầng một tạo Int_Stack, tầng hai nhận stack đó để tạo logger và API Print_SizeTầng 2Tầng 1Int_Stack_LoggerGeneric_Stack_LoggerInt_StackGeneric_StackPrint_Size

Công dụng gần template template parameter của C++, nhưng Ada nêu được tường minh: “nhận instance của generic package này”. Trong mã Ada lớn, tiện khi tách rồi ghép container, thuật toán, log, kiểm tra, hỗ trợ kiểm thử.

13. Contract Model — Ý tưởng quan trọng nhất của generic Ada

Để hiểu generic Ada, điểm then chốt là contract model.

Generic body chỉ được viết bằng thao tác mà formal parameter đã hứa. Ví dụ chỉ khai type Element is private; thì không được dùng < trên Element. Muốn dùng < thì phải nêu như formal subprogram, hoặc làm type category cụ thể hơn.

Contract model của generic AdaFormal part là hợp đồng, body được kiểm kiểu độc lập, instantiation kiểm actual có thỏa hợp đồng khônggeneric formal parthợp đồnggeneric bodycài trong phạm vi hợp đồngKiểm kiểu body độc lậpinstantiationactual parameterskiểu, hàm, giá trị thựcKiểm actual có thỏa hợp đồngPackage/subprogram thường

Thiết kế này bảo vệ cả người dùng generic lẫn người viết generic.

13.1 Khác biệt bề ngoài với template C++

Template C++ mạnh, nhưng trước đây có tính chất “chỉ khi instantiate template body thì mới ra lỗi”. C++20 concepts đã cải thiện, còn generic Ada từ đầu là mô hình nêu hợp đồng tường minh.

Ada viết hợp đồng trước, template C++ cụ thể hóa lúc instantiateAda kiểm body trong phạm vi formal part rồi kiểm actual lúc instantiate; C++ trước đây cụ thể hóa lúc instantiate dù concepts đã cải thiệnC++ templatesYêu cầu cụ thể hóa lúc instantiateViết template bodyCó thể nêu ràng buộc bằng conceptsAdabody được kiểm trong hợp đồngViết hợp đồng ở formal partinstantiation kiểm actual

Generic Java hay C# lấy quan hệ với reference type, constraint, type erasure, biểu diễn lúc runtime làm trung tâm thiết kế. Generic Ada nghiêng về việc tạo instance cụ thể lúc biên dịch.

Góc nhìn Ada C++ Java Rust
Cách viết hợp đồng Viết kiểu, giá trị, hàm, package ở formal part templates / concepts Type parameter và bounds trait bounds
Kiểm body Kiểm trong hợp đồng của formal parameter Lấy cụ thể hóa lúc instantiate làm trung tâm Kiểm trong bounds Kiểm trong trait bounds
Chi phí runtime Phân giải tĩnh là mặc định Sinh tĩnh là mặc định Chịu ảnh hưởng type erasure Monomorphization là mặc định
Value parameter Hạn chế const generics
Subprogram làm formal parameter Biểu đạt bằng function object, v.v. Lambda / functional interface Closure / hàm / trait
Package làm formal parameter Template template, v.v. Không Tách khỏi cấu trúc module

Từng ngôn ngữ khác nhau ở chi tiết, nhưng đặc trưng Ada là “viết hợp đồng bằng cú pháp trước”.

14. Quyết định thiết kế thực tiễn — Cái gì nên thành generic

Generic tiện, nhưng không phải cái gì cũng nên biến thành generic. Trong thực tế, phán đoán như sau thì ít thất bại.

Quyết định cái gì nên thành genericKhác mỗi kiểu thì type parameter, khác kích thước thì value parameter, khác hành vi thì formal subprogram, có trạng thái thì generic packageYesNoYesNoYesNoYesNoCó xử lý muốn tái sử dụngChỉ khác kiểu?Xem type parameterKích thước hay ngưỡng cũng khác?Thêm value parameterHành vi so sánh hay kiểm điều kiện cũng khác?Thêm formal subprogramMuốn gom trạng thái nội bộ và API?Generic packageSubprogram thường là đủ

14.1 Chỗ hợp generic subprogram

Generic subprogram hợp thuật toán không giữ trạng thái.

  • Swap
  • Sort
  • Count_If
  • Find
  • Biến đổi kiểu Map
  • Min / Max

Khi body thuật toán ngắn, đầu vào và đầu ra rõ, subprogram dễ đọc hơn package.

14.2 Chỗ hợp generic package

Generic package hợp khi muốn mang nhiều thao tác và trạng thái nội bộ cùng với kiểu.

  • Stack độ dài cố định
  • Ring buffer
  • Từ điển quy mô nhỏ
  • Bộ xử lý thống kê
  • Trừu tượng hóa I/O theo thiết bị
  • Bộ phép toán trên kiểu số có hệ đơn vị

Đặc biệt Ada tách package spec là public API, package body là cài đặt, nên generic package dùng được như “module template an toàn kiểu”.

Spec công khai, body ẩn, formal part là hợp đồngFormal part ràng spec và body, phía dùng chỉ thấy public API của package specẩnpackage specpublic APIPhía dùngpackage bodycài đặt nội bộgeneric formal parthợp đồng kiểu, giá trị, hàm

14.3 Bắt đầu với ít formal parameter

Formal parameter quá nhiều thì instantiation khó đọc. An toàn là bắt đầu tối thiểu, chỉ tăng khi xuất hiện lý do cần thay.

-- Ví dụ dễ trở nên khó đọc
package X is new Generic_Foo
  (A, B, C, D, E, F, G);

-- Giữ ý định bằng named association
package X is new Generic_Foo
  (Element_Type => Integer,
   Index_Type   => Positive,
   Buffer_Size  => 128,
   "<"          => Less);

Ada cho named association lúc instantiate. Điểm thiết kế quan trọng của generic hiện ra ở instantiation, nên mã thực tế thường dễ bảo trì hơn nếu viết theo tên.

15. Những chỗ dễ vấp

Generic Ada mạnh, nhưng có điểm dễ vấp lúc mới dùng.

15.1 Kiểu private không so sánh lớn/nhỏ được

Body như sau không viết được.

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      -- lỗi tại đây
      return A;
   else
      return B;
   end if;
end Bad_Min;

Element chỉ được khai là private, nên không chắc đã có <. Muốn so sánh thì thêm vào hợp đồng như sau.

generic
   type Element is private;
   with function "<" (Left, Right : Element) return Boolean is <>;
function Generic_Min (A, B : Element) return Element;
So sánh trên private phải được ghi vào formal partMuốn so sánh trong body thì viết hàm so sánh vào formal part; chỉ private sẽ lỗi biên dịchMuốn so sánh trong bodyViết hàm so sánh ở formal partLúc instantiate xác nhận có so sánh đượcChỉ privateLỗi biên dịch trong generic body

15.2 is <> không phải “suy luận tự động mọi thứ”

is <> tiện nhưng không phải phép màu. Tại điểm instantiate, toán tử hoặc subprogram khớp phải đang visible. Nếu hàm so sánh riêng nằm ở package khác, with / use cho đúng, hoặc truyền tường minh theo tên, thì an toàn.

procedure Sort_By_Age is new Generic_Insertion_Sort
  (Item_Type  => Person,
   Index      => Positive,
   Item_Array => Person_Array,
   "<"        => Younger_Than);

15.3 Exception cũng khác nhau theo từng instance

Exception khai báo trong spec của generic package trở thành exception riêng từng instance.

package Int_Stack   is new Generic_Stack (Integer, 5);
package Float_Stack is new Generic_Stack (Float, 3);

Khi đó Int_Stack.Stack_OverflowFloat_Stack.Stack_Overflow được coi là hai exception khác nhau. Nếu muốn một exception chung, cân nhắc định nghĩa exception bên ngoài generic.

Exception khai báo trong generic là riêng từng instanceInt_Stack.Stack_Overflow và Float_Stack.Stack_Overflow là hai exception khác nhauexception khácGeneric_Stackkhai báo Stack_OverflowInt_Stack.Stack_OverflowFloat_Stack.Stack_Overflow

15.4 Kích thước mã có thể tăng

Generic dễ tránh indirection thừa lúc runtime, nhưng vì mỗi kiểu sinh một instance, nhiều instantiation có thể làm tăng kích thước mã.

Đây là trade-off cũng thấy ở template C++ và monomorphization của Rust. Phát triển nghiêng độ tin cậy cao, embedded, real-time thì chấp nhận quản kích thước sản phẩm lúc build, đổi lấy việc giảm bất định lúc runtime.

Mỗi instantiation sinh mã riêngMột generic body sinh bản Integer, Float, Long_Float, My_Type, tránh boxing lúc runtime nhưng có thể tăng kích thướcMột generic bodyBản IntegerBản FloatBản Long_FloatBản My_TypeMã sinh raDễ tránh kiểm kiểu hay boxing lúc runtimeNhiều instance thì chú ý tăng kích thước

15.5 Chỗ nên dùng limited private

type Element is private; giả định gán. File handle, lock, device handle — thứ không muốn bị copy — thì cân nhắc limited private.

generic
   type Resource is limited private;
   with procedure Close (R : in out Resource);
procedure Generic_Use_And_Close (R : in out Resource);

Thiết kế kiểu không copy được thì, hơn là container lưu giá trị, thuật toán áp dụng procedure hoặc thiết kế nêu tường minh tham chiếu thường an toàn hơn.

16. Tập mẫu thiết kế nhỏ

Từ đây, sắp xếp ngắn các dạng hay dùng trong thực tế.

16.1 Chỉ cung cấp Min cho giá trị so sánh được

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 đòi hàm so sánhElement cộng hàm so sánh tạo Generic_Min, trả về giá trị nhỏ hơnElementCần hàm so sánhGeneric_MinTrả về cái nhỏ hơn

16.2 Lấy ngưỡng làm 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 hợp giá trị muốn cố định như tính chất của instance, không phải giá trị cấu hình lúc runtime.

16.3 Tiêm phương tiện xuất

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;

Dạng này cho phép thay đích xuất: stdout, log, buffer kiểm thử, v.v.

Tiêm thủ tục Put để đổi đích xuấtGeneric_Print_Twice nhận Put rồi có thể in console, log, hoặc buffer kiểm thửGeneric_Print_TwiceNhận Put như formal subprogramXuất consoleXuất logBuffer kiểm thử

16.4 Không cố định kiểu chỉ số mảng

Trong Ada, kiểu chỉ số mảng cũng là thông tin kiểu quan trọng. Đừng cố định Positive; khi cần, biến cả kiểu chỉ số thành formal parameter thì tính tái sử dụng tăng.

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);

Thiết kế này không chỉ mảng chỉ số Positive mà còn mảng chỉ số enumeration.

Không cố định kiểu index của mảngFormal Index kiểu discrete nhận Positive, enumeration Day hoặc State, và kiểu số nguyên riêngIndex is discretePositive rangeEnumeration DayEnumeration StateKiểu nguyên riêng

17. Danh sách kiểm tra để API mang phong cách Ada

Khi viết generic, soi lại theo các góc sau thì dễ đọc hơn.

Danh sách kiểm tra thiết kế generic AdaKiểm formal tối thiểu, phép toán đã nêu trong formal part, category, named association, exception theo instance, kích thước mã, và instance kiểm thửKiểm thiết kế genericFormal parameter đã tối thiểu?Phép toán cần thiết đã nêu ở formal part?Category private / range / digits đã phù hợp?Instantiate được dễ đọc bằng named association?Đã ý thức trạng thái và exception theo từng instance?Có chấp nhận tăng kích thước mã?Đã chuẩn bị instance cho kiểm thử?

Viết thành câu thì như sau.

  • Thao tác dùng trong body phải hiện ra như hợp đồng của formal parameter.
  • private đủ thì để private. Cần số học thì dùng range <> hoặc digits <>.
  • Hành vi đổi theo kiểu — so sánh, hash, xuất, chuyển đổi — thì để formal subprogram.
  • Kích thước hay ngưỡng là tính chất của instance thì để value parameter.
  • Có trạng thái thì nghĩ generic package trước; không trạng thái thì generic subprogram.
  • Instantiation càng nhiều đối số càng nên named association.
  • Thiết kế trên giả định exception và trạng thái nội bộ độc lập theo từng instance.

18. Ví dụ cấu trúc tổng thể của sample

Nếu tách sample trong bài thành file, cấu trúc như sau thì dễ đọc.

Tách spec, body, và demo thành fileThư mục src/generics giữ ads và adb, src/demos giữ các chương trình demoada-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

Sample nhỏ cho bài viết thì gộp một file rồi gnatchop cũng tiện, nhưng nghĩ tới thực tế và bảo trì dài hạn thì tách spec .ads và body .adb mới là cấu trúc mang phong cách Ada.

19. Kết luận — Xác định ranh giới tái sử dụng bằng kiểu

Lập trình generic trong Ada không chỉ là “tính năng viết mã không phụ thuộc kiểu”. Bản chất hơn là nêu tường minh, như hợp đồng của kiểu, subprogram và giá trị, rằng linh kiện tái sử dụng đòi gì.

Từ hợp đồng tới tái sử dụng an toàn kiểuViết hợp đồng, viết generic body, instantiate với kiểu giá trị hàm, rồi dùng an toàn kiểu mà không copy mãViết hợp đồngViết generic bodyInstantiate với kiểu, giá trị, hàmDùng an toàn kiểuTái sử dụng không copy

Như bài đã thấy, generic Ada nhận các formal parameter sau.

  • Kiểu
  • Giá trị
  • Subprogram
  • Package

Với kiểu, còn chỉ định category khá mịn: private, limited private, range <>, mod <>, digits <>, delta <>, (<>). Nhờ đó generic body không phụ thuộc thao tác “không biết có làm được không”, mà cài an toàn chỉ bằng thao tác đã ghi trong hợp đồng.

Tài sản C hay C++ cũ đôi khi tái sử dụng bằng macro, void*, function pointer, rẽ nhánh kiểu viết tay. Generic Ada thay được nhiều chỗ đó bằng dạng an toàn kiểu, dễ đọc. Đặc biệt bảo trì dài hạn, embedded, real-time, phần mềm độ tin cậy cao: thiết kế “chốt ranh giới lúc biên dịch” mang giá trị lớn.

20. Lĩnh vực tư vấn liên quan

KomuraSoft LLC nhận phát triển ứng dụng Windows, khảo sát và chỉnh sửa tài sản hiện có, sắp xếp ranh giới COM / ActiveX / 32-bit / 64-bit, tư vấn kỹ thuật và rà soát thiết kế. Không chỉ thiết kế nghiêng static typing và độ tin cậy cao như Ada; cách sắp xếp, kéo dài vòng đời, rồi di chuyển tài sản C/C++, C#, VB6, MFC, COM hiện có cũng là chủ đề gần trong thực tế.

Tài liệu tham khảo

Các bài viết gần đây có cùng thẻ để tìm hiểu sâu hơn những chủ đề lân cận.

Các trang này đặt chủ đề trong bối cảnh rộng hơn của dịch vụ và quyết định.

Câu hỏi thường gặp

Các câu hỏi thường gặp khi tư vấn về chủ đề của bài viết.

Generic trong Ada là gì?
Đó là cơ chế tái sử dụng nhận kiểu, giá trị, subprogram, thậm chí cả package làm formal parameter, rồi kiểm tra kiểu tĩnh tại thời điểm instantiation bằng new. Không phải thay thế văn bản: lúc biên dịch đã chốt được "linh kiện này có thỏa hợp đồng này không". Generic subprogram chỉ khai báo thì chưa gọi được; phải đưa kiểu cụ thể để instantiate thì mới dùng như procedure hay function thường.
Generic Ada khác template C++ ở điểm nào?
Ada từ đầu dùng contract model: ghi hợp đồng tường minh. Generic body chỉ được dùng thao tác mà formal parameter đã hứa, và bản thân body được kiểm kiểu độc lập. Template C++ trước đây chỉ lộ lỗi khi instantiate; C++20 concepts đã cải thiện điều đó. Ngoài value parameter và subprogram parameter, Ada còn cho phép chính package làm formal parameter, nên các linh kiện generic ghép được với nhau.
Vì sao formal type parameter của Ada lại chỉ định type category?
Để nêu tường minh, như một hợp đồng, những thao tác mà body được phép dùng. Với type T is private chỉ được giả định thao tác cơ bản như gán và so sánh bằng; so sánh lớn/nhỏ hay số học thì không. Cần số học nguyên thì range <>, cần floating-point thì digits <>, cần bit thì mod <>. Category còn là đặc tả cấp kiểu cho người đọc: linh kiện này đòi gì.
Generic Ada có điểm nào cần chú ý?
Vì mỗi kiểu sinh một instance, nhiều instantiation có thể làm tăng kích thước mã. Đây là trade-off cũng thấy ở template C++ và monomorphization của Rust. Exception khai báo trong spec của generic package cũng thành exception riêng từng instance. Hướng dẫn thực tiễn: bắt đầu với formal parameter tối thiểu, chỉ tăng khi có lý do cần thay, và dùng named association khi instantiation có nhiều đối số.

Hồ sơ tác giả

Trang giới thiệu tác giả bài viết.

Go Komura

Đại diện của KomuraSoft LLC

Chuyên về phát triển phần mềm Windows, tư vấn kỹ thuật và điều tra lỗi, đặc biệt trong các dự án có hệ thống hiện hữu và lỗi khó tái hiện.

Liên kết công khai

Quay lại blog