9 ms·
I think types should express intend. This adds more 'value' to the code for people than allowing them to switch their brains off because something is 'unlikely
by virtualritz 5y ago
I think types should express intend.
This adds more 'value' to the code for people than allowing them to switch their brains off because something is 'unlikely' to happen. A large class of bugs exists because people assume stuff is unlikely.
I also think one has to distinguish between API and actual code.
If you write a lib that takes inputs that should never be negative you can express this without writing a single line of documentation about it (or if you do, requiring the user to read it) by using unsigned types in these places.
You can still use signed types in your actual library code if you want/must/believe in that but do not pollute the outside API with them if unnecessary.
- kstenerud 5y agoIn a perfect world, this would be so. And if there were types that were performant AND could enforce such invariants without the downsides, I'd welcome them gladly. But primitive types come with baggage and limitations, so you have to make compromises. And the compromises when using unsigned ints are just too costly in my opinion. They're a good tool for specific use cases, but not as good as signed for general purpose code.
- virtualritz 5y agoCould you list some of the downsides & compromises in the case I made?
- kstenerud 5y ago- silent unintended underflow behavior - dangers of mixed signed/unsigned code (signed values MUST exist, but unsigned values don't have to) - more cognitive load to read and understand unsigned underflow detection code + issues once you exceed the halfway point This is why Java offered signed integer types only (I think they should have allowed unsigned as well, but for different reasons)
- virtualritz 5y ago> silent unintended underflow behavior Please show me an example in the wild that could not be trivially caught/alleviated. With 'trivially' I mean the code that needs to be written to prevent it, not the thinking you need to do to consider it. As for the latter: this is what I get paid for. Writing the code out is necessary but not what I really get paid for (unless I were a very slow typist, that is). I find that most of the time I was simply too lazy. Or rather: I wrote C/C++ code for 20+ years. I used int and didn't think (unintended rhyme). Now I write (mostly) Rust I find myself considering the edge cases regularly and also covering them because the compiler forces me to. See also this comment [1]. I.e. a - 1 + b where a is always positive (incl. 0) and b is always > 1 'just' works with int (and unsigned [overflow] too!) in C/C++. In Rust you will get a panic when a = 0 which will then make you think what you're doing and simply reorder this to a + b - 1. I find that this is the 'cognitive load' that needs to be applied commonly and I'm pretty fine with that. > dangers of mixed signed/unsigned code Alleviated by simply not mixing. There is a reason languages like Rust simply do not allow this without explicit typecasts. And those, in cases where one type is signed and the other is not and they have equal width translate to "all bets are off". > more cognitive load to read and understand unsigned underflow detection code [...] See first point. IMHO this is simply the same point expressed differently. Underflow happens when you subtract. Ensure you do not subtract a larger value from a smaller. This is as easy as writing max(a, b) - min(a, b) or what author of [1] does. Same as abs(a - b) but works with unsigned. Cognitive load? I do not see any and I agree with the author of the article that this is easier to read. [1] https://news.ycombinator.com/item?id=29767877 https://news.ycombinator.com/item?id=29767877
- kstenerud 5y agoThis is one of those "If everyone would just do X" kinds of situations, which is why I'm against it. Things go wrong all the time - in fact it's a miracle anything works at all. So when I see "code that needs to be written to prevent it", I let out a sardonic laugh; people just don't work that way. People WILL forget to check, or implement the check wrong (and it won't be discovered until the next black swan event), or some invariant will change in future and nobody will know to update this code. The code doesn't care because the compiler doesn't enforce the invariants, so you're left with bare and likely confusing (and probably eventually undefined) behavior. I for one would LOVE it if popular languages could be instructed to inject invariant enforcement code (not asserts) at certain boundaries. It would help with a whole host of problems! When you cross zero with a signed integer calculation, it's easy to find out, and the code has low cognitive overhead. Also, with code generally structured this way, bad input propagation tends to get stopped earlier because of the number of functions that check against negative values. It's also possible to underflow check with unsigned, but the code is more complicated and can't stop propagation if it fails, and if you are allowing values > the signed positive limit (or unwittingly allowing it), your overflow detection code gets even more complicated (or wrong) due to the reduced overhead space. You COULD do a bunch of checks beforehand, but that's not always feasible due to the combination of operations being done on the operands, and once again people are people. Even though many modern languages block uncast type conversions, that still doesn't protect you from converting a negative integer to an unsigned integer. Yes, you SHOULD be checking before the cast, buuuuuuut... What we want to do is reduce the number of ways things can go wrong, reduce the fallout of an errant process, and minimize cognitive load to reduce bugs overall. Making that worse just to double the positive integer space doesn't make sense to me.
- joppy 5y agoIf my intent is that a certain integer should never be negative, labelling it unsigned does a poor job at conveying this intent because any arithmetic just silently does the wrong thing. 3-4 is some huge number rather than “error”.
- tromp 5y agoIn Rust, 3u64 - 4u64 is "error", specifically thread 'main' panicked at 'attempt to subtract with overflow'
- Macha 5y agoIt panics _in debug builds_ but wraps in release builds because including the checks in every arithmetic operation is not zero cost.
- masklinn 5y agoThough you can configure your release builds to panic as well. That can be quite useful if you don't expect trivial computations to be a large cost center (and you can always bench to check that assumption). Even more so now that custom profiles have been added to Cargo (yay), you can enable overflow checking in release, and add a `release-unchecked` or whatever.
- tialaramex 5y agoOr, better in many cases, you can write what you meant explicitly in Rust. 3u64.checked_sub(4u64).expect("Overflow"); This allows you to spell out that you don't expect this to overflow, and Rust should panic if it does, regardless of your compiler flags.
- masklinn 5y agoOf course you can do that, but I would say that doing so defensively for every operation is way more verbose and unwieldy than you'd want. Much simpler to just toggle on the relevant flag.
- jstimpfle 5y ago> I think types should express intent. > If you write a lib that takes inputs that should never be negative you can express this without writing a single line of documentation So, let's make an odd_int_less_than_20587 type and a billion of other types? "Not negative" normally helps only one tiny bit towards documenting anything about the accepted values. Making a separate type for that and complicating everything is ridiculous. > If you write a lib that takes inputs that should never be negative Have you ever compared the difference between two unsigned integers? Because unsigned integers don't have "natural" subtraction. Did you ever find the need for signed numbers to interact with unsigned numbers? Because if you did, it's a bad idea to separate them without need. Premature isolation is a prime cause of complexity.
- morelisp 5y ago> So, let's make an odd_int_less_than_20587 type and a billion of other types? Bounded integer types have been extremely useful for me on a few occassions, and I wish more languages had type systems powerful enough to support them. They also address your concerns about subtraction.
- virtualritz 5y agoI think you assume my comments are regarding C/C++. They are not. I write mostly Rust these days.
- jstimpfle 5y agoI don't really care, why would Rust be much different in that regard?
- jcelerier 5y ago> So, let's make an odd_int_less_than_20587 type and a billion of other types? Yes, what's wrong with that ? Every variable's type should be defined as precisely as possible when meaningful, that's the whole point (and C/C++ not easily allowing to do that is one of their biggest drawbacks imho.)
- 5y ago
- whattheactualf3 5y agoIt does not express intent. I remember being a junior C programmer and using "unsigned" to express my intent (it's greater than or equal to zero). In reality, like all C keywords, it's completely misleading and actually really means "weird_arithmetic_mode_that_will_mess_you_up".
- carlmr 5y agoI don't know what's misleading about it or how the arithmetic is weird?
- SAI_Peregrinus 5y agoSigned and unsigned integer types both are finite fields, rather than Abelian rings like the normal integers used in most math. That difference makes the arithmetic "weird", but going to signed integers doesn't change anything, anything except bigints (or generic arbitrary precision floating point) is a form of "weird_arithmetic_mode_that_will_mess_you_up" if you pretend you're operating over the real numbers. Not sure what they mean about it being misleading though. They might prefer it if they were called "modular integers" instead of "unsigned integers" but that would lead to confusion with modules IMO.
- carlmr 5y ago>Not sure what they mean about it being misleading though. They might prefer it if they were called "modular integers" instead of "unsigned integers" but that would lead to confusion with modules IMO. Modular integers to me would seem very confusing. They're integers without the sign, and they have some max value determined by the number of bits, I can't really think of a better name for them except for the fixed-width versions which include that bit information. Extremely intuitive, simple English words, exactly what it says it is (integer without the sign bit).
- b3morales 5y agoImagine a parallel situation. There is a type `NonEmptySet`, with an `intersection` operation. Now, given `NonEmptySet(1, 2).intersection(NonEmptySet(3, 4))`, and the requirement that `intersection` produces another `NonEmptySet` what does that return? How about a set containing everything except 1, 2, 3, 4? Does that seem reasonable? That's analagous to what `(unsigned)3 - (unsigned)4` does. In short, the operations on `unsigned int` do not really behave like "non-negative integer"; as such it's a weird match to use them for that purpose.
- UncleMeat 5y agoIn most languages that offer this distinction, unsigned integers also don't really express your intent in an effective way. Imagine you have a bug in your program. If you intend for a value to be non-negative and use a signed integer, you get a negative value. Your code is broken. You didn't want a negative value. But you do have have a very easy way of checking for this at runtime, at least. Now imagine you instead use an unsigned integer. The bug is still there but now it manifests as overflow or underflow. Your program is still broken. Nothing about using an unsigned integer saved you. And worse, you don't have an easy predicate to check that something has gone wrong. I think this can only be a reasonable choice if you are working in a language that has dynamic checks for overflow and so using an unsigned integer enforces not only the predicate that the value is nonnegative but also that all operations stay within its expected arithmetic domain.
- deleted 5y ago[deleted]
- barrkel 5y agoWhat you want is range subtypes of signed integers.
- kazinator 5y agoImportant: without silent conversion. Languages like Pascal and Ada require explicit conversions in more places to make that sort of thing useful.
- pyjarrett 5y agoIt also allows creating type checked "new" numerical types for semantics purposes to help improve interfaces. This helps prevent things like accidentally passing an integer in the wrong units for example. Two of the worst offenders I've seen of this is: "Is this uint64_t (or float) "dt" parameter in ticks, seconds, milliseconds, microseconds or nanoseconds?" and "Is this angle parameter in radians or degrees?"
- deleted 5y ago[deleted]
- kazinator 5y agoIn C or C++, if you write a function void api(unsigned), you can cheerfully call it as api(-42) or api(b - c) where b - c are int. This is a poor, poor substitute for contract checking.
- alerighi 5y agoAnd why also limit to that? What if I have a function that accepts values in the range [0, 100]? Make a type to express that? What if I have a function that takes only positive integers (i.e. unsigned integers minus 0)? Chances are that you should anyway validate your input. Also what if you have a function that takes an unsigned and a signed is passed? On most situations you only get a warning (or not even that) and it's passed a completely wrong input to the function. Isn't better to take an int and validate it before doing anything with that? You see, doesn't make a lot of sense to distinguish between positive and negative in the end. From a mathematical point of view negative integers have the same dignity of positive integers since a couple of centuries. The same operations that apply on positive integers apply on negative integers, and not only that, an operation between two positive integers can as well result in a negative integer! The unsigned type was created just to have one more bit if you *are sure* you have only positive integers. That could had a sense in the era of 8/16 computers. Nowadays that we have 32 or 64 bit computers does really that extra bit count that much? I don't think so... 2 billions is plenty enough for most usages.
- Avshalom 5y ago"unsigned" doesn't express intent. It expresses implementation, maybe it expresses assumption. Ada's "Positive" or "Natural" or subtype X is Integer range Y..Z; expresses intent (well expresses requirement really), it also enforces that constraint, so it's not merely an assumption.
- marcosdumay 5y ago> I think types should express intend. Yes, and let's start with overflow behavior because it's much more relevant than the type domain. If overflow isn't encoded by the type, there is just no way to correctly set a strict domain.
- 8note 5y agoTypes aren't always expressive enough for that. I usually can't specify that a number should always be negative for instance, or that there's no 0. My preference is to minimize surprise. If everyone is used to using signed types for everything, you better have a good reason for using something else, and leave that justification in a comment
- CamperBob2 5y ago"This value should never be negative, so I'll use an unsigned type." There are two types of programmers: ones who are reassured by that reasoning, and ones who shudder at it.
- imtringued 5y agoYeah, "should not" and "can not" aren't the same at all.