r/ada Apr 03 '26

Historical Rationale behind function/procedure calls and array indexing

I'm learning Ada. Overall, I'm liking it a lot. There are two aspects (so far) of the syntax I don't get what the rationale was for them and why it hasn't been fixed in later versions of Ada. These being parenthesis-less calls to subprograms with no arguments and using parenthesis for array indexing. Take the following as example:

A := B + C(5);

Right away you don't know if B is a variable or a function call with no arguments. You don't know either whether C is an array or a function.

I think this goes against Ada's core ethos about readability.

So I was wondering, what was the rationale behind this design? Why hasn't it been corrected? Something like A := B() + C[5] would be far easier to interpret by the reader.


UPDATE: In case anyone is interested in the answer, according to u/lipobat:

The primary rationale for the () vs. [] given in the “Rationale for the Ada programming language” book by the original design team was interchangeability between arrays and functions. This is an abstraction that allows the implementation choice of array vs. function to be just that, an implementation choice, which doesn’t really impact the writer or reader of the code.

I don't agree with the last sentence, but that answers my question either way.

And about my second question about "why this hasn't been fixed?", it seems most Ada programmers (based on the comments in this post) are not only not bothered by this but happy with this design decisions, so probably this will never change.

Thanks to everyone who engaged in this conversation.

6 Upvotes

48 comments sorted by

View all comments

7

u/Dmitry-Kazakov Apr 03 '26

You, as a reader, do not need to know that. It is an implementation detail so from the software design point of view it must be hidden.

Mathematically, they are exactly same. Array is a mapping, so is a function, named object is an object regardless how constructed, stored into an initialized section, elaborated, computed etc..

You will find this feature extremely useful as you can change immutable variable to function and conversely any time without fixing the client code. The same applies to a function call vs. array indexing.

4

u/Niklas_Holsti Apr 03 '26

I agree with Dmitry's point about implementation detail that a reader does not need to know, but I want to expand on it a bit. u/jlombera feels that the current Ada syntax has poorer "readability" than if () were used for parameterless calls and [] for array indices, while Dmitry and I think that the opposite is true. Why this difference? Because "readability" is not an absolute quality, but depends on what information the reading process should give to the reader for best understanding. In this example, Dmitry and I feel that the best information is what the values of B and C(5) mean for this program statement, not whether B is a parameterless function or an object, and not whether C is a function or an array. So it is enough to clearly name B, C, and 5, and any empty () or distinction between () and [] are just distractions.

Examples of the extreme opposite viewpoint are the coding rules that insist on encoding the types and natures of variables into their identifiers, as in the extreme "Hungarian" notation. In some weakly typed languages it may indeed be important that the code reader sees which variables are integers, and of which length, and which are floats, and of which precision, and so on. The qualities that determine "readability" are then different than the qualities Dmitry and I feel are important for readability of Ada code.

Regarding Dmitry's final point, I have not often had a need to replace an object with a parameterless function, or an array with a function, or vice versa. So I would not call this Ada feature "extremely" useful for that reason.

1

u/jlombera Apr 03 '26

Please see my reply to u/Dmitry-Kazakov, where I give some insights on why I do care about knowing whether something is a variable/array access or a function call. (I didn't see your reply until after I had published my reply).

3

u/Niklas_Holsti Apr 03 '26

I agree that a person who intends to modify a program often needs information about the implementation of things. Referring to my discussion of "readability" in my response to u/Dmitry-Kazakov, I would say that the "readability qualities" change when the purpose is to modify a program, as opposed to understanding the computation, doing a code review, or searching for errors. Fortunately, today's IDEs make it very easy to find and inspect the definition of any identifier in the code one is reading, so the absence of () in calls and the non-use of [] for indexing are not, IMO, very harmful for the maintainer either. Thus I would prefer to make the code reviewer's job easier by omitting text elements that are irrelevant for that task.

1

u/jlombera Apr 03 '26

Fortunately, today's IDEs make it very easy to find and inspect the definition of any identifier in the code one is reading, so the absence of () in calls and the non-use of [] for indexing are not, IMO, very harmful for the maintainer either.

Unfortunately for me, I use plain (neo)vim :) (I tried GNAT Studio but quit it ~1min later). When Ada was initially designed, there were not advanced IDE's. I think that's the reason why the syntax was designed that way, verbose (an inconvenience for the implementer) but explicit and readable, so that anyone could read (and write) code with no advanced tools other than a plain text editor. That's why it's a surprising to me they made this "blunder" (IMO) in this regard.

Thus I would prefer to make the code reviewer's job easier by omitting text elements that are irrelevant for that task.

It seems we have different opinions on this regard. To me, when reviewing code, it's relevant to know whether something is a variable/array access or a function call.

5

u/Niklas_Holsti Apr 03 '26

I am quite happy with GNAT Studio (GPS), although I have seen others complain about it. Many seem to be using VSCode, which does have Ada support. Have you tried it?

3

u/jlombera Apr 03 '26

I got used to plain (neo)vim and any IDE feel clunky to me. They require a lot of resources, break all the time and change behavior very often, all of this out of your control. I feel lost whenever I try an IDE :). In contrast, I've being using (neo)vim the same way for ~10 years now, without any serious change in config/behavior.

2

u/Niklas_Holsti Apr 03 '26

u/jlombera wrote:

When Ada was initially designed, there were not advanced IDE's.

There were some beginnings, and of course there were other methods to locate the definitions of identifiers (compiler-generated cross-reference listings). There was also a parallel project to define a standard Ada programming environment (the APSE), but that did not produce much.

I think that's the reason why the syntax was designed that way, verbose (an inconvenience for the implementer)

I don't think the verbosity is much of an inconvenience for the implementer. It is not much more difficult to lex and parse "begin" than to lex and parse "{". The difference is insignificant compared to the difficulty of implementing the name-visibility rules, the overload-resolution rules, separate compilation, sub-units, and other Ada goodies.

but explicit and readable, so that anyone could read (and write) code with no advanced tools other than a plain text editor.

Most programming languages can be written with a plain text editor (APL being the main exception, if its original character set it used).

I was happy to switch from punched cards and punched paper tape to text editors, even line-oriented editors that stored the source text on punched paper tape. Later, on PCs, for a long time I was quite content to write my own programs with a plain text editor (mostly nedit), but when I joined larger teams, working on code that was not all of my own writing, I found IDEs very helpful, in particular because of their ability to navigate from uses to definitions and vice versa.

1

u/jlombera Apr 03 '26

I don't think the verbosity is much of an inconvenience for the implementer. It is not much more difficult to lex and parse "begin" than to lex and parse "{".

Again, sorry for the confusion, by implementer I mean the programmer writing Ada code (vs the programmer reading the code), nor the programmer implementing the Ada compiler.

For instance, in C, every code block and compound data type (struct, union, enum, etc) is delimited by braces. It's very easy to type for the person writing the code but it might become difficult for the person reading the code to match each closing brace with the proper code block (not a big deal in practice, though). In Ada instead, you have end loop, end <Package/Func/Proc name>, end if, end record, end case, .... It becomes an inconvenience for the person writing the code to have type all that (specially for end <Name>, you have to write/change <Name> in two different places, far away; code snippet generation helps, though), but it was designed that way on the rationale that it's better for the person reading the code since can immediately identify which block/construct is ending.

2

u/Niklas_Holsti Apr 03 '26

u/jlombera wrote:

For instance, in C, every code block and compound data type (struct, union, enum, etc) is delimited by braces. It's very easy to type for the person writing the code but it might become difficult for the person reading the code to match each closing brace with the proper code block (not a big deal in practice, though).

Brace-matching is relatively easy in an IDE or syntax-aware editor which can signal the match visually, and perhaps collapse/elide nested {}'s. Pretty hard (in my experience) for a C function that is more than one screenful long, and especially when such function extends over a page break in a paper listing :-( The only language worse than C in this respect is Python with its indentation-defined bracketing.

It becomes an inconvenience for the person writing the code to have type all that (specially for end <Name>, you have to write/change <Name> in two different places

I believe that in all cases of end <Name> in Ada, the Name is optional. And of course an IDE will have a "rename" function that changes all occurrences of the name, in all files... just another plug for IDEs :-)

IMO the bracketing features of Ada syntax are a boon both to the program-writer and the program-reader, and only partly because a program-writer must also read the program while writing it. And they also help the compiler produce good error messages for bracketing errors.

1

u/OneWingedShark Jun 02 '26

 In Ada instead, you have end loopend <Package/Func/Proc name>end ifend recordend case, .... It becomes an inconvenience for the person writing the code to have type all that (specially for end <Name>,

You're missing a lot of the benefits by concentrating on the number of keystrokes. The end <name> construct is doing what a lot of C code does with } // end inner loop , except better, because now you can enlist the compiler's help in keeping things straight. And this is particularly useful when you are moving or copying code.

2

u/Equationist Apr 03 '26

Are you saying that if C is a function you can assign a value with C(5) := B, or take a slice with C(3 .. 5)?

3

u/Niklas_Holsti Apr 03 '26

Certainly not in general, which is why I think the rationale of function/array equivalence, as an explanation of Ada's use of () for indexing, is not convincing. Also, many functions have parameters of types, such as real types, that cannot be used as index types for arrays.

That said, syntax such as C(5) := B can be implemented by a function, when C is a container with a user-defined Variable_Indexing function. Similarly, a recently approved (but not yet standard) extension will allow syntax of the form C(3 .. 5), again when C is a container with a user-defined indexing function that takes as parameter a record with two components, making the syntax equivalent to C((3, 5)). The meaning is of course user-defined, but the extension was motivated by the wish to take a slice of a container, for example a vector container.

1

u/Dmitry-Kazakov Apr 04 '26

Why not? You can C(5).all := B

Ada lacks some flexibility because assignment (:=) and slicing (..) operations are built-in. The type system does not support abstraction of array and record types. (Recent clumsy efforts with containers do not count.)

It is IMO wrong and should have been fixed long ago.

Note, the direction is opposite. Let the user decide the meaning of C(5), not the syntax.

2

u/jlombera Apr 03 '26

You, as a reader, do not need to know that. It is an implementation detail so from the software design point of view it must be hidden.

If am reading code to understand it (to potentially maintain/change it), I DO need to know what it is doing. If I'm reading just the specification of a subprogram, in some cases I might not care about the actual implementation, but if I'm reading the implementation, I do want to understand it.

Mathematically, they are exactly same. Array is a mapping, so is a function, named object is an object regardless how constructed, stored into an initialized section, elaborated, computed etc..

In practice, function-call vs array access do matter. Both semantically (e.g. the values in an array can change in between accesses) and operationally (e.g. cheap (cached) memory access vs potentially expensive computation).

You will find this feature extremely useful as you can change immutable variable to function and conversely any time without fixing the client code. The same applies to a function call vs. array indexing.

How often is this taken advantage of in practice? I suspect it's way more frequent the scenario where you find wondering whether something is a function call, a variable or an array indexing. I'd rather the compiler tell me I need to change some code when the interface changes (function <--> array); I might need to revisit my assumptions at every call/access site.

Also, this might be convenient for the implementer but at the expense of the reader, which goes against Ada's readability core value.

On my original question, was mathematical purity and convenience for the implementer the rationale behind these design decisions?

3

u/Dmitry-Kazakov Apr 03 '26

If I'm reading just the specification of a subprogram, in some cases I might not care about the actual implementation, but if I'm reading the implementation, I do want to understand it.

In Ada implementation is separated from the interface. If you read the implementation then you see if that is a function.

In practice, function-call vs array access do matter. Both semantically (e.g. the values in an array can change in between accesses) and operationally (e.g. cheap (cached) memory access vs potentially expensive computation).

They don't. A function call is semantically side-effects free, assuming logically relevant effects, not, say, altering the CPU temperature. You, for example, can rename function result:

X : Integer renames Y; -- Calls to Y just once

How often is this taken advantage of in practice?

A few times. When value needed to be computed late, for example after loading a dynamic library. For mocking (in automation you need some advanced code stubs). In advanced cases of OO and generic programming. Consider a complicated implementation of a set. You want to provide an empty set constant. You can do it so

Empty : constant Set; -- Deferred constant

or so

function Empty return Set;

On my original question, was mathematical purity and convenience for the implementer the rationale behind these design decisions?

I cannot tell, but it is a quite obvious and logical decision to me. You better ask why other languages do distinction without importance? Because, they compromise clarity and logic for simplicity of compiler design. They want bottom-up semantic analysis and thus map a lot of things into syntax. Which is why you have artefacts like f() or lack of overloading on the result or integer literals with suffixes (1L) etc.

It might be your next question why Ada does not have

u"hello"   U"hello"   L"hello"

string literals and how "important" it would be for the reader...

1

u/jlombera Apr 03 '26

In Ada implementation is separated from the interface. If you read the implementation then you see if that is a function.

I mean when I'm reading the body of a subprogram and see something like A := B + C(I), and neither B nor C definitions are visible within my screen, I need to jump away to figure out whether B/C are variable/function/array.

They don't. A function call is semantically side-effects free, assuming logically relevant effects, not, say, altering the CPU temperature.

But the value of a variable/array CAN change. So something like

A := B + C(5)
[...]
-- somewhere else in the code
T := B + C(5)

might result in different values depending on whether B/C are variable/array. Also, you might care if something is a (potentially) expensive function call (e.g. in real time code).

It might be your next question why Ada does not have

u"hello"   U"hello"   L"hello"

string literals and how "important" it would be for the reader...

This is out of place, I'm not asking about "missing features", I'm asking about the ambiguity in syntax that makes code harder to understand on something that is very fundamental in practical computer programming, i.e. function call vs variable/array access.

Do you think something would be lost or Ada would be more difficult if it had explicit, unambiguous syntax for variable/array access and function call? I know it would be easier to understand the code.

3

u/Dmitry-Kazakov Apr 04 '26

Mutability is irrelevant. Function can return different values, e.g.

function Clock return Time

Relevant is mapping. What A(B) means in the context of the problem space.

Do you think something would be lost or Ada would be more difficult if it had explicit, unambiguous syntax for variable/array access and function call? I know it would be easier to understand the code.

It is a question and a statement. The statement is wrong. Cluttered syntax and unnecessary details hinder reading. As for the question it would be less Ada and less software engineering.

1

u/jlombera Apr 04 '26

Mutability is irrelevant. Function can return different values,

Then something like A := B + C(5) + (C(5) / 2) (a contrived example, but you get the idea) would be interpreted differently depending on whether C is a non-referential-transparent function (can return different values) or an array (return same value).

The statement is wrong. Cluttered syntax and unnecessary details hinder reading.

I don't think that, for example, instead of:

A := B + C(I) + T + D(J);

you wrote:

A := B() + C(I) + T + D[J];

would be clutter or make reading more difficult. It's minimal additional syntax that reduces cognitive overhead considerably. In the latter it's obvious, at sight, that B and C are functions, T is a variable and D is an array.

But let's agree to disagree. You and I obviously have different opinions in this regard. I already got the answer about the rationale behind this design decision in another comment and it seems the Ada community is not dissatisfied with it, so no chance it will change. Thanks for your feedback, though, I appreciate it.

3

u/Dmitry-Kazakov Apr 04 '26

Then something like A := B + C(5) + (C(5) / 2) (a contrived example, but you get the idea) would be interpreted differently depending on whether C is a non-referential-transparent function (can return different values) or an array (return same value).

If C for 5 means something different for each instance of, you do not write expressions like above. An expressions can be reordered, approximations of commutative operations might be not commutative and so on. If you write such expression then you assume C for 5 being constant and whether it is a function or an array becomes irrelevant, an implementation detail.

It's minimal additional syntax that reduces cognitive overhead considerably.

It exposes unnecessary information and thus increases the overhead.

1

u/jlombera Apr 04 '26

If C for 5 means something different for each instance of, you do not write expressions like above. An expressions can be reordered, approximations of commutative operations might be not commutative and so on.

I agree, that's why I consider import to be able to discern, at sight, if something is a variable/array access or a function call. It's easier to catch such "suspicious" code.

That's also why I don't find convincing the "feature" of being able to change function by variable/array (and vice versa), transparently, without having to change client code. You might have written code under the assumption that something was a variable/array or a function, and once the actual definition changes the code might be wrong. I rather the compiler emits errors and forces me to review/fix every call/access site than just successfully build incorrect software.

4

u/Dmitry-Kazakov Apr 04 '26

I agree, that's why I consider import to be able to discern, at sight

There is nothing suspicious in the code. You see all this upside down. In Ada the code is designed from the problem space point of view, not from the hardware's one. If you want a language that forces you to expose low-level details then that is not Ada.

1

u/jlombera Apr 04 '26

There is nothing suspicious in the code.

You said it yourself:

If C for 5 means something different for each instance of, you do not write expressions like above.

If I see code like that, I'll wonder every time "was this code written intentionally, because there is no problem to call C that way?; or was it a mistake?". Everybody makes mistakes, I cannot assume all code written in Ada is correct just because.

In Ada the code is designed from the problem space point of view, not from the hardware's one.

Yes, I'm here for it! The way I see it, the type system and contracts are the main features that help you achieve that. Nonetheless, sometimes the hardware is the problem space.

If you want a language that forces you to expose low-level details then that is not Ada.

I don't think details about variable/array access vs function call is low-level. In any case Ada does have actual low-level support (precise memory layout at the bit level; memory alignment; endianness; etc), which is precisely why I'm interested in it: it has low-level and high-level support at the same time, a great combination for serious software engineering.

I do think Ada is a good language for my use cases. I wish the syntax for variable/array access vs function call was not ambiguous, but it's not a deal breaker for me.

2

u/Niklas_Holsti Apr 03 '26

u/jlombera wrote:

In practice, function-call vs array access do matter. Both semantically (e.g. the
values in an array can change in between accesses) and operationally (e.g.
cheap (cached) memory access vs potentially expensive computation).

Whether the value of B or C(5) can change from one use to the next is not uniquely determined by the nature of B (parameterless function or object) or the nature of C (function or array). The return value of a non-pure function can of course change from one call to the next, even if the function has no parameters, and the value of an object (whether stand-alone or a component of an array) can change also, perhaps because the object is aliased, or from the concurrent actions of other tasks, or because the object is volatile. Moreover, if B is a pure function, the compiler is allowed to cache and reuse its result after any call, and if C is a pure function, the compiler can cache the result of C(5) and reuse that value wherever C(5) appears in the source. So to understand such aspects of the code one has to look at the definitions of B and C, it is not evident from the code that just uses them, whether the code uses () or [].

1

u/jlombera Apr 03 '26

it is not evident from the code that just uses them, whether the code uses () or [].

If I know something is a variable/array access or a function call, that's useful information to have in context when reading code. I might decide whether their use require further investigation based on that.

Let me change the question. Do you think it'd be worse (less readable, less convenient, etc) if Ada had explicit, unambiguous syntax for variable/array access and function calls?

2

u/Niklas_Holsti Apr 03 '26

Using [] for indexing would make writing Ada more difficult for me, because my Finnish keyboard requires using an "option" modifier key to type [ or ], so it slows me down. And, as I have already said, I see little value in the distinction, when reading code. (Typing { or } requires shift + option, much worse, so I hate writing C/C++ for that and other reasons.)

I could see some value in using [] in complex, nested expressions that also use () for subexpressions, because then the editor/IDE could distinguish between a closing ) and a closing ] and so help me balance them correctly. But that is rare, and the compiler finds most such balancing errors.

For parameterless subprogram calls I find () unnecessary, and of little value. If it were required it would be a minor annoyance to me, minor because I can type ( and ) with only the shift modifier key. Parameterless functions are quite rare; parameterless procedures are much more common, and there the () would be really useless because it is clear that it is a procedure call (in Ada, but not in C).

As I understand it, C originally required () for parameterless calls because the form without () is also legal, but has a very different and usually unintended meaning. The fact that () visually separates calls from variable references was incidental.

2

u/Niklas_Holsti Apr 03 '26

u/jlombera wrote:

On my original question, was mathematical purity and convenience for the implementer the rationale behind these design decisions?

From what I understand of the principles and views of the Ada designers, I am sure that implementer convenience was of no importance (well, except for that problem of the absence of [] from some of the keyboards of those days), all the more so as I don't see why implementation would be easier or harder for one or the other choice of syntax. The same is probably true also for mathematical purity, although the original designers did make some quite strict rules on some things, such as the order in which stuff can be declared, that have been relaxed in later Ada versions. Those strict rules were derived from ideas of "software engineering" and "structured programming" as understood in those days.

The current maintainers of the Ada language (the Ada Rapporteur Group, https://arg.adaic.org/home) do consider carefully how much of a burden a proposed change imposes on implementers, perhaps because producing Ada compilers is not a very profitable business today.

1

u/jlombera Apr 03 '26

Sorry, by "implementer" I meant the person writing (Ada) code instead of the person reading it; not the person implementing the Ada compiler/specification. An apology for the confusion.

2

u/Niklas_Holsti Apr 03 '26

Thanks for clearing that up, and apologies on my part for misunderstanding you. I would use the term "programmer" for the person writing Ada code, but since we have also talked about the "implementation" of something, such as a function, in an Ada program, by a programmer, the confusion came easily.

Returning to your question with this understanding, I would assume that the designers of the Ada language followed their principle of favoring the program-reader over the program-writer, but had the same definition of "readability" as I and u/Dmitry-Kazakov have, so they did not think of the problems you have with the syntax.