6 ms·
What programming language would cause least friction for providing discrete types for all domain primitives or even all handled data?
by korpiq 7y ago
What programming language would cause least friction for providing discrete types for all domain primitives or even all handled data?
- mosen 7y agoI’ve found F# to be good in this regard. Can’t claim that it would have the least friction, however.
- rajandatta 7y agoI've always thought that a language like Idris based on Dependent Types would by far be the best language for this. 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.
- korpiq 7y agoThank you, both insights help me in pondering how to ever better ensure quality.
- JadeNB 7y ago> I've always thought that a language like Idris based on Dependent Types would by far be the best language for this. 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.
- fhennig 7y agoI think most statically typed languages should be good for this.
- carlmr 7y agoStatic typing isn't really the only thing here. Strong typing would also be good. 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.
- gpderetta 7y ago> 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). 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.
- carlmr 7y agoEnum classes are a good first step, but C++ is still very relaxed on integer and float conversions. I just wish I could use rust or ADA at work.
- ronlobo 7y agoAgree, strongly typed languages are best suited for this as it usually allows to embed business domain invariants into the type system which can be checked at compile time. Rust does a great job in that regard and is not too slow.
- frou_dh 7y agoOCaml, F#, Haskell, Elm, ... (anything with lightweight notation to define sum types [sometimes with only 1 variant], and a module system to limit who can construct them) There's a whole well-reviewed book on exactly this: https://pragprog.com/book/swdddf/domain-modeling-made-functional https://pragprog.com/book/swdddf/domain-modeling-made-functi...
- nicoburns 7y agoRust and Swift can also be added to this list.
- tmountain 7y agoNot saying it's the least friction, but Golang can accomplish this pretty easily with interfaces, structs and receivers. In Haskell, you can use phantom types to accomplish this in really elegant way, as is illustrated here: https://wiki.haskell.org/Phantom_type https://wiki.haskell.org/Phantom_type.
- StevenWaterman 7y agoTypeScript is surprisingly good at this. It's not popular as a backend language, but shows that your choices aren't limited to pure FP languages and low-level systems programming languages.
- Normal_gaussian 7y agoTypescript is explicitly bad at this; it is structurally typed, not nominally typed, meaning its effectively useless at enforcing domain guards. 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/#0MYGwhgzhAEAKD2ECWAXJA3ApgSQHYswHNMAnaAbwChpp0wQBXTALmlwYFsAjUgbmujB4uCChINgKeCQAU6Vu26kAlBQE0kAMznQAPNAAMy9TWgoAFiXgB3NplsBREldnL+ps+aQQAdHUaY0AC8tAIAvpQRoJAwAHJEYGhYeATEZFQ0-kwKnDwk7oLCouKS0nI5SiSqGaZaOvpGJjQWVra49tBOLjJuJhbefvRMwaE0EVHgUNAAymISKAwk9CAAngAiWpqkmPgpRKRqHvJsuXwmQiJzpbLHinnVTab9vugj6AVjkZSUIJgo0AAPVgIZBJHD4fZkELtWwg1AYcGpUgyACMRn4v3+K1Y8UIiQRezSIxh0Fx+OSELSqPRPz+0AAXqxZiUFksQKsNpotiQdihCQdoR1mfNFst1pttrtKci0b1KACRit+CsRvT+PSRgDeEA https://flow.org/try/#0MYGwhgzhAEAKD2ECWAXJA3ApgSQHYswHNMAna... [2] https://typescript-play.js.org/#code/MYGwhgzhAEAKD2ECWAXJA3ApgSQHYswHNMAnaAbwChpp0wQBXTALmlwYFsAjUgbmujB4uCChINgKeCQAU6Vu26kAlBQE0kAMznQAPNAAMy9TWgoAFiXgB3NplsBREldnL+ps+aQQAdHUaY0AC8tAIAvpQRoJAwAHJEYGhYeATEZFQ0-kwKnDwk7oLCouKS0nI5SiSqGaZaOvpGJjQWVra49tBOLjJuJhbefvRMwaE0EVHgUNAAymISKAwk9CAAngAiWpqkmPgpRKRqHvJsuXwmQiJzpbLHinnVTab9vugj6AVjkZSUIJgo0AAPVgIZBJHD4fZkELtWwg1AYcGpUgyACMRn4v3+K1Y8UIiQRezSIxh0Fx+OSELSqPRPz+0AAXqxZiUFksQKsNpotiQdihCQdoR1mfNFst1pttrtKci0b1KACRit+CsRvT+PSRgDeEA https://typescript-play.js.org/#code/MYGwhgzhAEAKD2ECWAXJA3A...
- StevenWaterman 7y agoThere are ways to enforce nominal typing in TS [1] 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/#approach-4-intersection-types-and-brands https://michalzalecki.com/nominal-typing-in-typescript/#appr...
- thesuperbigfrog 7y agoAda does a great job in this regard. Using some examples in the article: >> 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.
- coldacid 7y agoOh my god. I wish C# had this.
- naasking 7y agoAda is pretty much still the only language that has this.
- rauhl 7y ago> Ada is pretty much still the only language that has this. 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.
- ragnese 7y agoAnd a related question: How far is too far and/or impractical? 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.
- maxdeviant 7y agoYes. This is exactly what we do at work. It makes sure that you never pass a `LocationId` where a `UserId` is expected; the type system literally will not allow it.