>> For example, say that you want to represent the number of books ordered. Instead of using an integer for this, define a class called Quantity. It contains an integer, but also ensures that the value is always between 1 and 240
The Ada code to implement this is:
type Quantity is new Integer range 1 .. 240;
>> instead of just using a string, define a class called UserName. It contains a string holding the user name, but also enforces all the domain rules for a valid user name. This can include minimum and maximum lengths, allowed characters etc.
The Ada code to implement this is:
with Ada.Strings.Bounded; package UserName is new Ada.Strings.Bounded.Generic_Bounded_Length (Max => UserName_Max_Length);
Dynamic predicates or even a string subtype could be used to further refine the UserName definition depending on exactly what restrictions are needed.
While it's not perfect, Ada does make it pretty easy to specify constraints on data types and will complain loudly when the constraints are violated.
Common Lisp does too:
(deftype quantity () '(integer 0 240))
(defun foo (x)
(declare (type quantity x))
(1+ x))
(foo 1) → 2
(foo -1) → ERROR
(defun valid-username-p (string)
(and (< 8 (length string) 24)
(every (lambda (char)
(find char "abcdefghijklmnopqrstuvwxyz0123456789-_=./" :test #'char=))
string)))
(typep "foo" 'username) → NIL
(typep "foobarbaz" 'username) → T
(typep "foobar-baz" 'username) → T
(typep "foobar-baz " 'username) → NIL
Common Lisp is pretty awesome. type
Quantity = 1 .. 240;
Colors = (Red, Green, Blue);
Pixels = array [1..1024, 1..768, Colors] of Byte;
Just a small example how its type system already so much better than C, although not as good as Ada.And overall, Pascal's type system is NOT so much better. Not strictly better at all, if at all any better. It's a chore to do the simplest things, starting from the mess that is the various types of strings, to a confusing memory management story, continuing with extremely verbose type declaration syntax (which requires to add many additional names), to the mess that is 0-based vs 1-based indexing, and let me not start with the messy object systems that were put on top in Delphi.
If you ask me it's definitely WORSE over all, although for example Delphi has nice aspects to it, especially in the IDE.
Oh yeah, and if Ada was ever adopted by a significant adoption of programmers, then they probably have committed suicide in the meantime.
By the way, better check your github issues.
I don't know how much effort you put into finding this, but the result feels almost like a certificate of quality to me and confirmed my opinion that this style of coding is pretty f*ing safe in practice. And that while I spent definitely less than 1% time on debugging memory issues (running valgrind twice, according to my git history, compared to working on this project during 7 months, initially 2.5 months full time, leading to an estimate of about 500h of development time).
There's a whole well-reviewed book on exactly this: https://pragprog.com/book/swdddf/domain-modeling-made-functi...
See the same program in flow [1] (nominally typed) and TypeScript [2] (structurally typed).
In the case of flow the type can only be constructed with the class -thus enforcing the guards - whereas in TypeScript I can accidently (or deliberately) bypass all guards by having a class with an equivalent structure.
[1] https://flow.org/try/#0MYGwhgzhAEAKD2ECWAXJA3ApgSQHYswHNMAna...
[2] https://typescript-play.js.org/#code/MYGwhgzhAEAKD2ECWAXJA3A...
type Brand<K, T> = K & { __brand: T }
type USD = Brand<number, "USD">
type EUR = Brand<number, "EUR">
const usd = 10 as USD;
const eur = 10 as EUR;
function gross(net: USD, tax: USD): USD {
return (net + tax) as USD;
}
gross(usd, usd); // ok
gross(eur, usd); // Type '"EUR"' is not assignable to type '"USD"'.
[1] https://michalzalecki.com/nominal-typing-in-typescript/#appr...* It uses an arguably invalid construct "K & { __brand: T }", where K is not an object, is an empty intersection. The fact that typescript allows casting a number to this type is concerning.
* Typescript currently allows "{} as USD" for non-object "K"'s but this will throw up serious issues down the line (obj is not number etc.); this is a likely error after validating JSON for example.
* Similarly typescript will allow you to bypass the guards for primitive types by using the structurally invalid value "{__brand: 'USD'}", or bypass them for object types using a structurally valid form with a "__brand: 'USD'" member. Which is more concerning I don't know.
* The type system now believes you have a member "__brand" that you don't actually have.
* In summary, you cannot enforce the guards through the type system.
That said, this is an interesting hack that, assuming your developers aren't trying to hurt you and you don't use it for primitives, could help get some extra safety in there. However the absurdity of the intersection looms heavily over it, I wouldn't bet on this working in a few years...
I just recently was working on a backend application and was modeling the database entities. The project uses UUIDs as primary keys. Should each entity have its own primary key type? `Location` gets a `LocationId`, User gets a 'UserId', etc, etc, where they're really all just wrappers around UUID?
Honestly, I thought about doing that a bunch of times during the start of the project, but I was pretty sure I'd get some harsh, sideways, glances from the rest of the team.
It makes sure that you never pass a `LocationId` where a `UserId` is expected; the type system literally will not allow it.
The problem is a non-trivial one even for 'simple' things like a person's name. Having a rule that takes in languages, special characters, spaces etc is hard.
As someone who loves the idea of dependent types, it seems to me that this is the best theoretical solution, but maybe not the best practical solution. If solving a simple-seeming domain problem involves modelling, not just first-order types, but the whole theory of dependent types, then I think people are going to start looking for escape hatches rather than upgrading their mental models.
E.g. C has a very weak type system, which is static. There's a lot of implicit conversion going on. Also the expressiveness of the type system is very limited (in C++ also).
OCaml, F#, Haskell and other functional candidates are strongly and statically typed, with very expressive type systems.
Idris with it's dependent types would be ideal and goes even further than the above.
In embedded most likely ADA and Rust offer strong enough static type systems.
It is true that the subset that C++ shares to C has too many implicit conversions, but you can do much better.
For example in C++ you can use enum class to define strongly typed integrals that do not implicitly convert to the basic types.
I just wish I could use rust or ADA at work.
Rust does a great job in that regard and is not too slow.