Intermediate

Functions in Ada

Learn how to define and use subprograms in Ada - functions and procedures, parameter modes, default and named parameters, recursion, and overloading with Docker-ready examples

In Ada, the units of reusable behavior are called subprograms, and they come in two distinct flavors: functions, which compute and return a value, and procedures, which perform actions but return nothing. This clean separation is a deliberate design choice. A function call is an expression; a procedure call is a statement. Keeping the two apart makes Ada code easier to read and reason about — when you see a function call you know it produces a value, and when you see a procedure call you know it does something.

Ada gives subprograms a level of expressiveness that many older languages lack. Parameters have explicit modes (in, out, in out) that document exactly how data flows in and out. Callers can supply arguments by name, making call sites self-documenting. Parameters can carry default values, and a single name can be overloaded across multiple parameter profiles. As a strongly, statically typed language, Ada checks every call against its declared profile at compile time, so mismatched arguments are caught long before the program runs.

In this tutorial you’ll define functions and procedures, control how arguments are passed using parameter modes, use default and named parameters, write recursive subprograms, and overload a subprogram name. Every example is a complete, runnable program you can compile with GNAT.

Functions and Procedures

The most fundamental distinction in Ada is between a function and a procedure. A function declares a return type and must return a value; a procedure does not. Both are declared in the declarative region of a program (between is and begin), and both can be called from the executable part.

Create a file named functions.adb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
with Ada.Text_IO;

procedure Functions is

   --  A function returns a value and is used as an expression
   function Add (X, Y : Integer) return Integer is
   begin
      return X + Y;
   end Add;

   --  Functions can do real work before returning
   function Square (N : Integer) return Integer is
   begin
      return N * N;
   end Square;

   --  A procedure performs an action but returns no value
   procedure Greet (Name : String) is
   begin
      Ada.Text_IO.Put_Line ("Hello, " & Name & "!");
   end Greet;

   Sum    : constant Integer := Add (3, 4);
   Result : constant Integer := Square (5);

begin
   Ada.Text_IO.Put_Line ("3 + 4 =" & Integer'Image (Sum));
   Ada.Text_IO.Put_Line ("5 squared =" & Integer'Image (Result));
   Greet ("Ada");
end Functions;

A few things to notice:

  • function Add (X, Y : Integer) return Integer declares the parameter profile and the return type. Parameters of the same type can share a declaration (X, Y : Integer).
  • The end Add; clause repeats the subprogram name. As with the main procedure, this redundancy helps the compiler catch mismatched blocks.
  • Integer'Image converts an integer to its String form. It prepends a space for non-negative values, which is why no extra space is needed after the = signs.
  • A function call like Add (3, 4) appears wherever an expression is expected; a procedure call like Greet ("Ada") stands alone as a statement.

Parameter Modes and Default Parameters

Ada parameters declare a mode that states how the data moves. The default mode, in, makes the parameter read-only inside the subprogram. An out parameter is a way to send a result back through the argument list, and an in out parameter is both read and written. Ada also lets in parameters carry a default value, so callers may omit them.

Create a file named parameters.adb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
with Ada.Text_IO;

procedure Parameters is

   --  'in' parameters are read-only; Exp has a default value of 2
   function Power (Base : Integer; Exp : Integer := 2) return Integer is
      Result : Integer := 1;
   begin
      for I in 1 .. Exp loop
         Result := Result * Base;
      end loop;
      return Result;
   end Power;

   --  'out' parameters return data through the argument list
   procedure Divide (Dividend, Divisor    : in Integer;
                     Quotient, Remainder  : out Integer) is
   begin
      Quotient  := Dividend / Divisor;
      Remainder := Dividend mod Divisor;
   end Divide;

   --  'in out' parameters are read and then modified in place
   procedure Double (Value : in out Integer) is
   begin
      Value := Value * 2;
   end Double;

   Q, R : Integer;
   N    : Integer := 21;

begin
   --  Omit Exp to use its default of 2
   Ada.Text_IO.Put_Line ("5 squared =" & Integer'Image (Power (5)));
   --  Override the default
   Ada.Text_IO.Put_Line ("2 to the 8th =" & Integer'Image (Power (2, 8)));

   Divide (17, 5, Q, R);
   Ada.Text_IO.Put_Line ("17 / 5 =" & Integer'Image (Q) &
                         " remainder" & Integer'Image (R));

   Double (N);
   Ada.Text_IO.Put_Line ("21 doubled =" & Integer'Image (N));
end Parameters;

The mode system is one of Ada’s safety features. The compiler enforces it: you cannot assign to an in parameter, and you cannot read an out parameter before writing it. Because the direction of data flow is part of the declaration, a reader understands a subprogram’s effect from its signature alone. Functions traditionally use only in parameters, which keeps them free of side effects on their arguments.

Recursion

A subprogram in Ada may call itself. Recursion is the natural way to express problems that break down into smaller versions of themselves, such as factorials or Fibonacci numbers. Ada’s numeric subtypes — Natural (integers >= 0) and Positive (integers >= 1) — let you constrain parameters and results, and the runtime will raise an exception if a value ever falls outside its range.

Create a file named recursion.adb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
with Ada.Text_IO;

procedure Recursion is

   --  Classic recursive factorial; result is always Positive
   function Factorial (N : Natural) return Positive is
   begin
      if N = 0 then
         return 1;
      else
         return N * Factorial (N - 1);
      end if;
   end Factorial;

   --  Recursive Fibonacci; each call spawns two more
   function Fib (N : Natural) return Natural is
   begin
      if N < 2 then
         return N;
      else
         return Fib (N - 1) + Fib (N - 2);
      end if;
   end Fib;

begin
   Ada.Text_IO.Put_Line ("5! =" & Positive'Image (Factorial (5)));
   Ada.Text_IO.Put_Line ("10! =" & Positive'Image (Factorial (10)));
   Ada.Text_IO.Put_Line ("Fib(10) =" & Natural'Image (Fib (10)));
end Recursion;

Using Natural for the input and Positive for the factorial result documents the contract directly in the types: a factorial is never zero or negative, and the input is never negative. If a calculation overflowed the range of Positive, Ada would raise Constraint_Error rather than silently producing a wrong answer — a small illustration of why Ada is favored in systems where incorrect results are unacceptable.

Named Parameters and Overloading

Ada lets callers associate arguments by name rather than by position, which makes a call self-documenting and order-independent. Ada also supports overloading: several subprograms can share a name as long as their parameter or return profiles differ. The compiler picks the right one based on the argument types — a form of compile-time dispatch.

Create a file named overloading.adb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
with Ada.Text_IO;

procedure Overloading is

   --  Two subprograms share a name but differ by parameter type
   function Describe (Value : Integer) return String is
   begin
      return "integer" & Integer'Image (Value);
   end Describe;

   function Describe (Value : Boolean) return String is
   begin
      return "boolean " & Boolean'Image (Value);
   end Describe;

   --  Named association lets callers pass arguments in any order
   function Build_Range (Low : Integer; High : Integer) return String is
   begin
      return Integer'Image (Low) & " .." & Integer'Image (High);
   end Build_Range;

begin
   --  The compiler chooses the matching Describe by argument type
   Ada.Text_IO.Put_Line (Describe (42));
   Ada.Text_IO.Put_Line (Describe (True));

   --  Arguments passed by name; order no longer matters
   Ada.Text_IO.Put_Line ("Range:" & Build_Range (High => 100, Low => 1));
end Overloading;

The two Describe functions coexist because their parameter types differ; calling Describe (42) selects the Integer version and Describe (True) selects the Boolean version. In Build_Range, the High => 100, Low => 1 syntax binds each argument to its parameter explicitly, so the reversed order is harmless and the intent is obvious at the call site. Named association is especially valuable when a subprogram has several parameters of the same type, where positional arguments would be easy to transpose.

Running with Docker

The codearchaeology/ada:latest image bundles the GNAT compiler. Mount the current directory and run gnatmake to compile each program, then execute the resulting binary.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Pull the Ada image (includes GNAT)
docker pull codearchaeology/ada:latest

# Functions and procedures
docker run --rm -v $(pwd):/app -w /app codearchaeology/ada:latest sh -c 'gnatmake functions.adb && ./functions'

# Parameter modes and default parameters
docker run --rm -v $(pwd):/app -w /app codearchaeology/ada:latest sh -c 'gnatmake parameters.adb && ./parameters'

# Recursion
docker run --rm -v $(pwd):/app -w /app codearchaeology/ada:latest sh -c 'gnatmake recursion.adb && ./recursion'

# Named parameters and overloading
docker run --rm -v $(pwd):/app -w /app codearchaeology/ada:latest sh -c 'gnatmake overloading.adb && ./overloading'

Expected Output

Running functions:

3 + 4 = 7
5 squared = 25
Hello, Ada!

Running parameters:

5 squared = 25
2 to the 8th = 256
17 / 5 = 3 remainder 2
21 doubled = 42

Running recursion:

5! = 120
10! = 3628800
Fib(10) = 55

Running overloading:

integer 42
boolean TRUE
Range: 1 .. 100

Key Concepts

  • Functions vs. procedures — Functions return a value and are used as expressions; procedures perform actions and are used as statements. Ada keeps these two roles strictly separate.
  • Parameter modesin (read-only, the default), out (write-back), and in out (read and write) document exactly how data flows through a subprogram, and the compiler enforces them.
  • Default parametersin parameters can declare a default value, so callers may omit them, as with Exp : Integer := 2 in Power.
  • Named association — Arguments can be passed by name (High => 100, Low => 1), making calls self-documenting and order-independent.
  • Overloading — Multiple subprograms can share a name when their parameter or return profiles differ; the compiler resolves the call by argument types.
  • Recursion with constrained subtypes — Subprograms may call themselves, and subtypes like Natural and Positive add range contracts that the runtime checks automatically.
  • Repeated end names — Closing a subprogram with end Name; mirrors the declaration and helps the compiler catch structural mistakes.
  • Compile-time checking — Every call is validated against the declared profile, so wrong argument counts or types are rejected before the program ever runs.

Running Today

All examples can be run using Docker:

docker pull codearchaeology/ada:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining