Ada for the C++ and Java Developer [pdf]
learn.adacore.com
learn.adacore.com
procedure Main is
type Distance is new Float;
type Area is new Float;
D1 : Distance := 2.0;
D2 : Distance := 3.0;
A : Area;
begin
D1 := D1 + D2; -- OK
D1 := D1 + A; -- NOT OK: incompatible types for "+" operator
A := D1 * D2; -- NOT OK: incompatible types for ":=" assignment
A := Area (D1 * D2); -- OK
end Main;
> The predefined Ada rules are not perfect; they admit some problematic cases (for example multiplying two Distances yields a Distance) and prohibit some useful cases (for example multiplying two Distances should deliver an Area). These situations can be handled through other mechanismsI can get why you have to be explicit in type casts if you're trying to be safe, but is there any way to say that
type Area is Distance * Distance
Or Type CubeVolume is Distance * Distance * Distance
So that of I say a: Area and b : CubeVolume and a := d1 * d2 it just works, and then b := a * d3 also works.IOTW, explicit casts not needed.
Or is it the case that Ada really took the concept of "explicit is better than implicit" and went to town?
I just wonder because I love F#'s unit of measure types and how they can prevent logical type errors (say, multiplying 10 m/s by 3kg when assigning to a variable of type m/s^2).
Or for this case, if I wrote
x: Distance := y: Distance * z: Distance
It'd fail hard, because a Distance * a Distance is an Area.I sorta figured Ada would have invented this sorta stuff and then it was cribbed by other languages.
So yeah, wondering what the other mechanisms the author mentions are. Because I've always heard Ada is great at type safety, so I'd be keen to see how safe it makes your code when combining values with units that aren't logical to combine.
function "*"(Left, Right : Distance) return Area
and if dedicated enough build up a whole set of unit-types with corresponding conversion rules.Not an Ada guy at all though so no clue if there's a less by-hand way of doing it.
with Ada.Text_IO; use Ada.Text_IO;
procedure Main is
type Distance is new Float;
type Area is new Float;
function "*" (Left, Right : Distance) return Area is
temp : Distance;
begin
temp := Left * Right;
return Area (temp);
end "*";
D1 : Distance := 2.0;
D2 : Distance := 3.0;
A : Area;
begin
A := D1 * D2; -- OK
Put_Line(A'Image);
end Main;
Interestingly, it took me several attempts to create this overload, make it a single line return would make it recursive: function "*" (Left, Right : Distance) return Area is
begin
return Left * Right;
end "*"; -- ^ Does not work
-- raised STORAGE_ERROR : stack overflow or erroneous memory access
Also: function "*" (Left, Right : Distance) return Area is
begin
return Area (Left * Right);
end "*"; -- ^ Does not work either, compiler gives me "ambiguous operand in conversion" return Area(Float (Left) * Float (Right)); return Area (Distance (Left * Right));
? Return Area( Distance'Base'(Left * Right) );
The apostrophe at the inner portion is 'qualification', a method for directing the compiler that the enclosed value is supposed to be a particular subtype. (Typically used to resolve ambiguities.)I haven't played with it yet, but it does looks pretty interesting.
[0]https://gcc.gnu.org/onlinedocs/gcc-4.9.4/gnat_ugn_unw/Perfor... [1]https://blog.adacore.com/uploads/dc.pdf
Nothing very high-level, as we need to explicitly manipulate the single-component records, but it does the job.
with Ada.Text_IO; use Ada.Text_IO;
procedure Main is
type Distance is record
Value : Float;
end record;
type Area is record
Value : Float;
end record;
function "*" (Left, Right : Distance) return Area is
begin
return (Value => Left.Value * Right.Value);
end "*";
D1 : constant Distance := (Value => 10.0);
D2 : constant Distance := (Value => 20.0);
-- D3 : constant Distance := D1 * D2;
-- Does not compile. GNAT returns the following error:
-- main.adb:21:33: expected type "Distance" defined at line 4
-- main.adb:21:33: found type "Area" defined at line 8
A : constant Area := D1 * D2;
begin
Put_Line ("Area A is " & Float'Image(A.Value));
end Main;*as in:
with Ada.Text_IO; use Ada.Text_IO;
with System.Dim.Float_Mks; use System.Dim.Float_Mks;
with System.Dim.Float_Mks_IO; use System.Dim.Float_Mks_IO;
procedure Main is
subtype Distance is System.Dim.Float_Mks.Length;
subtype Area is System.Dim.Float_Mks.Area;
D1 : constant Distance := 10.0*m;
D2 : constant Distance := 20.0*m;
-- D3 : constant Distance := D1 * D2;
-- Does not compile. GNAT returns the following error:
-- main.adb:13:08: dimensions mismatch in object declaration
-- main.adb:13:37: expected dimension [L], found [L**2]
A : constant Area := D1 * D2;
begin
-- print: Area A is 2.00000E+02 m**2
Put ("Area A is ");
Put (Item => A);
Put_Line ("");
end Main; Function "*"(Left, Right: Distance) return Distance is abstract;
then use: Function "*"(Left, Right : Distance) return Area;https://learn.adacore.com/courses/Ada_For_The_CPP_Java_Devel...
Preconditions: "a condition or predicate that must always be true just prior to the execution of some section of code or before an operation"[1]. It is up to the caller (client) to set up the preconditions and ensure that they are true before calling the code in question. If any preconditions are not met, it is the fault of the caller (client).
Postconditions: "a condition or predicate that must always be true just after the execution of some section of code or after an operation"[2]. The code in question guarantees that the postconditions will be true after the code is executed. If any postconditions are not met, it is the fault of the callee (supplier).
Invariants: conditions or predicates that "can be relied upon to be true during the execution of a program, or during some portion of it"[3]. Both parties must ensure the invariants hold.
These features make it very easy to determine where a bug is in the code and make it very explicit what is expected of the caller (client) and callee (supplier).
They act as run-time sanity checks and push Ada / SPARK code in the direction of Haskell function signatures and types. With proper preconditions, postconditions, and invariants in place I think it should be possible to implement a QuickCheck-style[4] testing system to provide some empirical checks if SPARK proof checking is not used.
I would love to see these design-by-contact features added to Rust, C++, and even C.
[0] https://www.eiffel.com/values/design-by-contract/introductio...
[1] https://en.wikipedia.org/wiki/Precondition
[2] https://en.wikipedia.org/wiki/Postcondition
[3] https://en.wikipedia.org/wiki/Invariant_(mathematics)#Invari...
Specifically, do the language semantics allow a mode where these checks can be turned off? Leaving them on all the time can be a huge performance burden, both directly and in the barriers they add to optimization (while there are potential optimization benefits to exploiting the guarantees of contracts, I don’t think as a practical matter they balance out).
But allowing them to be turned off in “release mode” eliminates much of the benefit in all modes: Developers can no longer assume that their code always runs in a context where the contractual guarantees are met, so they have to guard data integrity or safety critical operations dynamically anyway.
I think the performance impacts are usually not as big as you think (the Ada compiler is smart enough to eliminate redundant checks where it can), and I believe that they can be turned off per package, too, so for performance critical modules they can be deactivated in release mode.
I do not think contracts were intended to raise recoverable errors in a normal application's behavior; if they are triggered, then the program is incorrect, not just encountering an error. I think most developers would leave them on in release mode (if possible) just to get stack traces and exception messages to see what failed.
My thumb rule is to have all preconditions/postconditions/invariants turned on only in the boundary functions (i.e. public api) of a module while the inner cohesive functions only have preconditions turned on.
Yes, with Ada it is typically a pragma or compiler-flag to turn checks off; however, Ada has a long history of having mandatory-checks and strongly-encouraging compiler-implementers to optimize the checks away when it is known they cannot fail (which is a surprising chunk of the time if you're coming from a C-based language).
For C you can have a look at Frama-C. [2]
D also has DbC. [3]
In case you can target Windows only, VC++ supports SAL Annotations, while not DbC they help to improve code security
[1] - http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p233...
[2] - https://frama-c.com/html/acsl.html
[3] - https://dlang.org/spec/contracts.html
[4] - https://docs.microsoft.com/en-us/cpp/code-quality/using-sal-...
I highly recommend reading Bertrand Meyer's papers "Applying Design By Contract" and "Design by Contract". They are well worth every programmer's time.
As the team leader for the second version of a database middleware product developed in C, being convinced by study of the efficacy of assertions for DbC and hence better quality software, I made sure my team used C assertions heavily throughout the codebase, at the entry and exit points of functions, to implement preconditions and postconditions, despite some opposition to it.
End result: the product was a success, and was used in multiple software projects for customers.
We were rewarded well for it.
Edit: I first learned about DbC myself, via reading about Eiffel and Bertrand Meyer's work, early on.
I found the Java type system to be horrendous in comparison. Much better employment potential, though...
Although Turbo Pascal isn't Ada, it did allow for a similar approach with stronger types, which C++ also kind of supports (but C compatibility spoils it).
Used in aviation extensively where toy and aspirational languages don't do the job. http://archive.adaic.com/projects/atwork/boeing.html
Why Ada isn't Popular (1998): https://news.ycombinator.com/item?id=7824570
"You’ve got to love it when people who [know] nothing about Ada like to tell other people, who know nothing about Ada, things about Ada that they probably heard from someone else, who knew nothing about Ada." -- https://users.rust-lang.org/t/if-ada-is-already-very-safe-wh...
That also applies to lots and lots of comments on this site!
(If you read here, hello, Luke :-)
My gut perception of Ada is unfortunately mediated through the murky lens of its bastard offspring PL/SQL, which is by a good distance the least favourite of any language I have ever used, although I'd be willing to entertain the argument that this is in large part due to all the ugly and ill-considered bits nailed onto it by Larry's mob rather than inherent defects in the parent language itself.
I'm currently using Go. Although I would prefer Ada as a language, (iv) is in the end decisive for my tasks. If I used Ada I'd spend half of my time reinventing the wheel or interfacing with C libraries. I'm hoping to find a use for it in the future, though.
2. This problem also has another side - C is much more popular than the language itself warrants.
I see here following reasons: 1. Unix (which is popular in academia since 70s-80s) and later GCC (it was hard to compete with a free compiler at times when most other required an expensive license) 2. Microsoft designated C and C++ as "official" languages for Windows: MS provided IDE - Visual C++ supported only C/C++ [1] and official documentation implies that everybody uses C or C++ to create Windows apps.
[1] Visual Studio later added .Net support, but this not reduced C/C++ popularity because .Net competes mostly with Java.
There are a few; if you head over to comp.lang.ada you can find threads on the issue by people who were involved at the time. As I understand it though there are four or five points:
1. Ada was designed and specified completely before any implementations were extant, it used then bleeding-edge theory and integrated several big concepts/features: this lead to the very first 'implementations' being either incomplete or pretty bad performance-wise.
2. The backlash among DoD contractor's programmers; "Don't tell us what language to use!"
3. The rise of C's popularity; I believe this had massive consequences, ultimately setting back the field of computer science by decades. [Take a look at Multics, VMS, and the Burroughs... then realize how many of their features have been added to 'popular' OSes in the last 10–15 years.]
4. Misunderstanding the compiler's mindset: a lot of programmers take the view that they need to "shut the compiler up" rather than as the compiler helping them out by finding problems.
5. Misunderstanding "mandatory checks" — a lot of programmers are used to C & decedent's "simple" nature and really don't understand how things can be leveraged. A good example here is the sequence F(F(F(X))); if F takes Natural and returns Natural, then there is only one check that is needed for this sequence: N on the innermost call... and if X is defined as natural, even that check can be optimized away.
Newer VHDL deviates from Ada syntax in some unfortunate ways that can lead to confusion. For example ".all", in Ada is used to "dereference pointers" while in VHDL it is used to import everything from a library.
Also, coming from Ada you eventually realize that many FPGA vendor's tools are non-conforming to the standard in regards to certain features that are seldomly used by hardware designers but are bread and butter for Ada developers. (enum -> int, int -> enum conversions were partly unsupported in the Xilinx toolchains some years ago, not sure if the problem persists.)
In VHDL, the meaning of the suffix all depends on kind of the prefix. When the prefix is an object of access type, it behaves similarly to Ada.
Why did they name that cryptocurrency "Ada"?
For example: are array out-of-bounds checked? Or prevented at compile time? What about overflow? ...
Overflow is checked.
However both can be disabled via unsafe code pragmas if so desired.
As of Ada 2012, the SPARK proof system was integrated into Ada and you can also use DbC as formal proofs.
Many of the use cases that in C++ would require new/delete are handled by the compiler itself, thus there is an error if when a function is called there is not enough space available.
In the cases that there is a need to explicilty do malloc/free like programming, only malloc (new) is considered safe, manually releasing memory is an explict unsafe operation and marked as such.
For example, just consider if C allowed to return a plain C string from a function without any heap allocation. Things like unsafe sprintf/strcopy would never happen then.
Other compilers have similar switch.
Heck even C and C++ have them, although few make use of them.
And if you can afford it, Codepeer (static analysis) finds most of the ones the compiler doesn't find. And then if you can live with the Spark subset, you get proof of initialization in 'bronze' mode :-).
I'm not sure if it lets you shoot yourself in the foot regarding invalid type conversions, misuse of unions, that kind of thing.
http://www.ada-auth.org/standards/12rm/html/RM-3-10.html#p13...
> dynamic checks (such as array bounds checks) provide verification that could not be done at compile time. Dynamic checks are performed at runtime, similar to what is done in Java.
> SPARK builds on the strengths of Ada to provide even more guarantees statically rather than dynamically. As summarized in the following table, Ada provides strict syntax and strong typing at compile time plus dynamic checking of run-time errors and program contracts. SPARK allows such checking to be performed statically. In addition, it enforces the use of a safer language subset and detects data flow errors statically.
Pointers - accesses in Ada parlance - have rules that help ensure you can't have an invalid access. For one, the object itself has to be declared 'aliased' to create accesses to it. There are also rules to do things like ensure you can't create an access to some type at a higher scope than the type itself.
There are also memory pools that you can use. So you can specify that all allocations of a type occur in a specific pool or sub-pool. In addition to letting you control how allocation occurs, you can also use them for memory safety. All items in a sub-pool will be freed when the pool falls out of scope, so you can simply leave deallocations to occur that way.
There are also concurrency tools baked-in, with runtime support at least. Specifically in terms of memory safety, you have protected objects. They will automatically ensure you have a single writer at a time, and will do other nice things like still allow multiple readers. If you need to, you can get a fair bit of control over how it all works.
65 pages of straightforward translation.