6 ms·
I don't know why integer derived types don't exist in every language, from what I understand they are a compile time feature that removes large swaths of potent
by com2kid 3y ago
I don't know why integer derived types don't exist in every language, from what I understand they are a compile time feature that removes large swaths of potential errors.
We all agree that string enums are generally a good idea when we want to limit passed in configuration values to a limited set of valid strings, so why not ensure that if 2 (or 5...) integers are passed into a constructor (looking at you Java) that they are actually all of the proper "type" of integer?
For those who don't know what I am talking about, Ada lets you do stuff like (taken from the linked book)
type Age is new Natural range 0..150;
and now you can say that the age parameter on a function is of type Age, and if you try to pass some other integer type in, it will fail.
Ada's type system is so powerful that the metric system of measurements, with physics units, speed, acceleration, all that jazz, can be expressed in it.
I've seen C++ people say "but templates can do that!" but if I ask about how much eye bleed is included, they always change the topic...
- teaearlgraycold 3y agoIt doesn’t compare, but TypeScript can do this for small ranges, or small sets, very well.
- throwaway13337 3y agoAgreed! Expanded type guards can replace a huge amount of unit tests in a way that is more concise and provides a better promise to more of the code. It's about orientation. Orienting around the data makes so much more sense. Why test every function's email address param when you can instead guard via an email address type? Typescript is the most popular language to try and their type system is a lot of fun. However, the compiler messages remind me of working with Boost libs in C++. It's also unfortunate that typescript actually provides no guarantee about runtime so I never have complete confidence like I do with real static typed languages. You can also get to a point in typescript where you go down a type system rabbit hole and find yourself a code philosopher. It's been a personal black hole for time wasting on a project for me. Libs like Zod go a long way in JS and they accomplish the same but better for me. They leverage typescript and you get compile time and runtime support. Vogan in C# provides compile time warnings but uses generated code to achieve it. It is impressive what the new code generation support in C# can do now. Still, it would be nice to have first class support from a language. https://github.com/colinhacks/zod https://github.com/colinhacks/zod https://github.com/SteveDunn/Vogen https://github.com/SteveDunn/Vogen
- mbStavola 3y agoRust is considering this under the name of "pattern types" [0][1]. If the full idea is realized, you should be able to handle strings and enums as well. There is also a PR for a minimal implementation using a macro and targeting integers specifically[2]. It's experimental and there is a chance this doesn't get stabilized, but here's hoping it makes it through! [0]: https://github.com/rust-lang/rust/pull/107606 https://github.com/rust-lang/rust/pull/107606 [1]: https://cohost.org/oli-obk/post/165584-ranged-integers-via https://cohost.org/oli-obk/post/165584-ranged-integers-via [2]: https://github.com/rust-lang/rust/pull/120131 https://github.com/rust-lang/rust/pull/120131
- phoehne 3y agoI’m glad to hear that. We’re moving stuff to Rust at work and it’s great at memory safety. Much better than relying on Unchecked_Deallocation. But that said, there are a lot of times when something can’t be out of a range, like voltage is from 0.00 to 1.85 in steps of 0.01. Ada is much better at safely specifying that range, while Rust will happily take values that are outside the acceptable range.
- binary132 3y agoAlmost as though in order to maintain runtime type invariants on values you must use some kind of function to “construct” them...
- pyjarrett 3y agoAda checks the ranges on types assigned to each other at compile time and then invariants on assignment and when passed as parameters. You can turn the runtime side of these checks on or off individually at the module level.
- kevlar700 3y agoThat's unfortunate. Ada is so much nicer to work with and Spark now has deallocation safety and memory leak prevention. https://youtu.be/97G1V2U8Drk?si=wNqNs1GiuO9ZdGwP https://youtu.be/97G1V2U8Drk?si=wNqNs1GiuO9ZdGwP
- pjmlp 3y agoWhile people keep using Ada for ranged types examples, this was already present in Pascal and Modula-2. type Age = 0..150; type Age = [0..150]; It is a pity that has taken so many years for people to start appreciating type driven design.
- ainar-g 3y agoA feature that combines well with range and enum types are typed indexes of arrays: TYPE TWeekday = (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday); VAR WeekdayNameIndex : ARRAY [TWeekday] OF ShortString = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'); Now `WeekdayNameIndex` is a properly type- and range-checked look-up table. It's really sad that these simple joys of Pascal are still lacking from a lot of mainstream languages.
- pjmlp 3y agoIndeed, I used that kind of stuff a lot.
- pyjarrett 3y agoThis is an awesome feature, combined with appropriate type and bounds checking and prevents so many errors. It can also avoid resorting to a heavier-weight map type. Ada has this as well, including using any arbitrary continuous range for array indexing which handles remapping indices for you. e.g. if the key range is 20-40 the language handles associating it with array indices for you.
- 3y ago
- cjfd 3y agoC++ can do this very well, no eye bleed is involved.
- criddell 3y agoI’m skeptical that C++ can manage anything as clear and concise as: type Age is new Natural range 0..150;
- jcelerier 3y agoWell, for the end user it'd look like using age = in_range<int, 0, 150>; Not the end of the world
- 9659 3y agoa little bigger, more complete, example would help. this is something i have never seen in C++. (note C++ is a big language that changes a lot)
- ziotom78 3y agoDoes this really define a new range-checked type? I never used in_range, but it seems a function [1] that you must call whenever you want to do the check on one variable, while in Pascal all the checks are injected automatically every time you modify a variable of that type. [1] https://en.cppreference.com/w/cpp/utility/in_range https://en.cppreference.com/w/cpp/utility/in_range
- Rexxar 3y agoIf I understand correctly the other comments, they just say it's possible to do in c++ with templates/operator overloading/constexpr. It's not an existing type of the standard library. This type could work in simple situations but will probably not be as good as a native type in more complex situations.
- criddell 3y agoThat’s a function, not a type.
- touisteur 3y agoI wish rust had learned from that and also kept range/overflow checks on by default and not only in debug mode.
- lolinder 3y agoOne of the key principles of Rust is zero-overhead abstractions. The more runtime code they insert the harder it would be to sell the language to C/C++ programmers.
- touisteur 3y agoAnd yet bounds checks were kept. Other compilers for other languages gave the option to disable some or all of the range checks, or to enable thorough validity checks. Not an all-or-nothing 'debug' toggle, but project- or file-level options for runtime checks. On most code I worked on the bounds checks are really not having a large performance impact, especially on modern processors and it's been a very rare occasion I had to remove them, most recently converting the code to SPARK and proving the absence or runtime error. If people are OK pleasing the borrow-checker I'm sure they'll enjoy interaction with the prover (which farms most of the work to why3 and SMT solvers) to make sure their optimisation is actually safe.
- pyjarrett 3y ago> the code to SPARK and proving the absence or runtime error I wrote Rob Pike's simple regex from the "Practice of Programming" in Ada/SPARK and it blew my mind that I actually managed to prove an absence of runtime error (guaranteed no overflow or out of bounds).
- touisteur 3y agoWhat I really like about SPARK is the pragmatism. It's not all or nothing, you can call on non-SPARK code, and you don't have to go full functional verification to profit from the SPARK toolset. edit: if you feel like sharing your code and experience there, I'm pretty sure some people would be interested.
- MarcusE1W 3y agoI think this is a good example of how Ada handles data. The approach is to describe the data structures and let the compiler handle the best implementation. I don’t know if that always leads to the most efficient code, but what it does is that you often think about your data first before you start programming. It’s a different type of prototyping. Once that is done a lot of the programme flows naturally around that. Nothing stops you to do that in other languages of course but Ada makes it quite easy and part of the concept. Together with pre- and post-contracts you get a long way in writing code that avoids some of the usual error groups without much effort. Plus it is (in my view) easier for someone to read code if you can quickly check the range of an integer or to read the pre-post-contracts of functions. It is surprising for me how much of an implementation is fixed, once that sort of thing is clear.
- bdw5204 3y agoYou could potentially do the same thing with a class but it'd be really verbose: Class Age { private int value = null; public boolean SetValue(int value) { if (value >= 0 && value <= 150) { this.value = value; return true; } return false; } public int getValue() { return this.value; } } That's approximately what the equivalent would look like in any language that supports classes (any errors are because it was typed on my phone). Being able to do that in 1 line would be much more convenient. That's why most programmers working in those languages just define age as an int and move on.
- tored 3y agoNot only in one line but also you don't need to wrap your function argument with a class instantiation, you can just pass the integer into the function you are calling and the compiler takes care of the rest. And that is a problem with many class based languages, that a class can't work as a primitive, compare Java's int vs Integer. program TestType; type age = 0..150; procedure test(age:age); begin writeln(age); end; begin test(100); test(200); end.
- Jtsummers 3y agoThat's a runtime check which does part of what Ada will do. Ada will also perform compile time checks related to uses of these restricted range variables. SPARK can go even further and detect potential overflow/underflow errors (and other things) forcing you to add preconditions (one resolution) to functions that declare that the values being added (or otherwise operated on) are small enough that overflow won't happen. Ada will also use a reasonable storage size. If Age is ranged [0,150] then it can place it in a single byte (you can also influence the storage size so you can increase this if you want for some reason), there is less memory overhead than an object. Since it's a range it brings in (automatically) all the arithmetic operations you'd expect. EDIT: Regarding storage. That's technically implementation defined. GNAT, at least, will use a reasonable storage size by default, you have to override it and ask for something bigger. I had occasion to use Green Hills years ago and it did the same as I recall. I'd expect any other commercial implementation to use an appropriate size and not something absurdly large like 64 bits for a range that easily fits in 8 bits, it would not fit their general market (safety-critical, performance-critical, real-time, and embedded systems). Poor use of memory could cripple the utility of a compiler in these kinds of systems (especially performance-critical, real-time, and embedded).
- samatman 3y agoJulia has Value types for when a primitive type can be used during compilation: https://docs.julialang.org/en/v1.10/manual/types/#%22Value-types%22 https://docs.julialang.org/en/v1.10/manual/types/#%22Value-t... They aren't the same thing, I mostly brought them up to point out that Ada can only throw compile-time errors for range types under limited circumstances, otherwise you're expressing a runtime bounds check using the type system. Which I agree is a nice feature for a static language, which Julia is not; Ada won't let you leave out the runtime check, so numbers outside the range are unrepresentable in the program. It should be possible to use Value types to write a generated function which instantiates a constructor for a primitive type which is restricted to a range. All type errors in Julia are at runtime, but like I said, Ada has to push many range exceptions to runtime as well, and the property that a number outside of the range is unrepresentable is preserved in both cases. Although I can't point you at an implementation of this in Julia, I'm increasingly confident that it could be done. Julia's type system is also powerful enough to express units https://github.com/PainterQubits/Unitful.jl https://github.com/PainterQubits/Unitful.jl
- kevlar700 3y agoAda is very flexible and does let you leave out the runtime check. However the program will be stopped or atleast be exceptive by default if a logic error creates an invalid value that you haven't checked the validity of. Spark can be used for a higher degree of value analysis at compile time because flow analysis is obviously needed in many cases. Volatility can still be an issue but in most cases Ada knows the inputs such as for API usage validity.
- samatman 3y agoI would rather say that Ada allows you to remove the runtime check on ranges. This is the opposite of enforcing a range with a home-rolled check of the values, that's what I mean by "won't let you leave it out": without deliberate action, the range will either be proven by static analysis to be in-bounds, or that invariant will be checked at runtime. Julia has a similar system for array bounds: by default, accessing an array is checked for range restriction, and an error is thrown if the value is out of range. Range checks inside functions (including the system `getindex` function) can be annotated with the @boundscheck macro, and user code which knows an index is in-range can use @inbounds to elide that check. If this is done wrong, Julia will segfault or corrupt memory. In C, if it isn't all done perfectly correctly, same thing: opt-in, not opt-out. Julia is more like Ada in that respect.
- tubthumper8 3y agoIn this example, how do you get an instance of `Age`? i.e. If a user is asked for their age and they type in a number
- Jtsummers 3y agoAda has had generics since the start. https://learn.adacore.com/courses/intro-to-ada/chapters/generics.html https://learn.adacore.com/courses/intro-to-ada/chapters/gene... - Generics in Ada. This is from a good intro course for Ada and the site has several other courses as well. https://learn.adacore.com/courses/intro-to-ada/chapters/generics.html#example-i-o-instances https://learn.adacore.com/courses/intro-to-ada/chapters/gene... - generic IO portion of the above. A sketch, you'd need to place these lines in valid locations since it won't run directly like this but these are the lines you'd use: type Age is range 0..150; package Age_IO is new Ada.Text_IO.Integer_IO (Age); -- declared somewhere A : Age; -- from user input Age_IO.Get(A); -- if we "use" the above use Age_IO; -- now we can skip the prefix Get(A); This would result in a runtime error if the read value is out of range.
- kazinator 3y ago> and if you try to pass some other integer type in, it will fail. But since that passage has to be done in order to complete the programmer's task, the fix will be to stick in a coercion. Over time, those things will pile up and uglify the program. Then an age shows up that is 151, because of non-human animals and objects. Oops!
- sacado2 3y agoAda also has decimal values. I don't understand why all languages don't have them. Floats are a PITA when dealing with monetary values.