The curse of strong typing
fasterthanli.me
fasterthanli.me
I generally like C++'s approach. Most things that should work together (like doubles, floats, ints, etc) can work together. If you want to have stronger types, you can make your own classes or use enum classes. If you want, the compiler can warn you about different types, or just let it slide.
Because integer promotion goes every which way, not just strict ext/sext it’s been a regular source of security issues, which is why most low-ish level langages have swung so hard against it.
How does it help? I don't mean "strict typing" in general, but specifically how does making us decorate numeric literals help with "math done at runtime with some kind of input"?
It forces you to be specific about rounding, minimum/maximum, and floating-point arithmetic.
Your compiler doesn't/can't know expected extremes of a value. If it defaults to, let's say, int64, then you're potentially wasting enormous amounts of memory (depending on the size of your data).
Similarly, the programmer needs to be specific about precision. If you know you're dealing with integers, then an integer type is great. If you know you need N digits of precision, you can select a numeric type that fits.
The type system becomes useful if/when you start to mix these numbers together. It can warn you that you're losing precision (or adding artificial precision, by casting an integer to a double, for example).
And that isn't even getting into questions of whether you want the number stored on the stack or the heap, which I believe Rust gives you more control over than most languages do.
Yes, it can! For one thing, it's a literal; it has one value; trivially, that's both extremes. But even leaving aside possibilities for anything new and smart, Rust has type inference so it knows what type a given literal has to be (or it doesn't; I have no objection to making the programmer be specific in that case).
I'm asking what problem you see arising from a policy like "`2u32` means 2 as a 32 bit unsigned integer, but `2` means 2 as whatever type is inferred, no defaulting, and we catch it at compile time when the literal can't be represented exactly in the type." (Ignoring simple path dependence - it would be a breaking change because it would make some expressions ambiguous where they relied on a lack of suffix meaning i32 or f64.)
> The type system becomes useful if/when you start to mix these numbers together.
As mentioned, I'm not objecting to the type system, or asking for any implicit conversions except a lossless(!) implicit conversion from the string the programmer typed to the datatype inferred by the type checker.
https://play.rust-lang.org/?version=stable&mode=debug&editio...
This is the situation I was explicitly excluding from my original comment. I was talking about runtime input, which is by far the more common use-case for numbers in code.
A smart compiler will just optimize operations on literals into their result at compile time anyway.
Haskell does not allow Integer * Double (or even Integer * Int32), but it does allow `2 * 3.14`, and you seemed to be saying that what Haskell does is somehow dangerously weakly typed.
IMHO Rust's `Into` and `TryInto` traits offer a better solution than Haskell's `fromInteger`, distinguishing between conversions which cannot fail and ones which may. It also infers the correct width for unsuffixed integer literals from the context—but it draws a sharp distinction between integer and floating-point literals, which is why `2 * 3.14` (integer * float) is a type error while `2 * 314u16` would be accepted without issue. The downside of the Rust approach is that the type inference rules for integer literals are hard-coded into the language and can't easily be extended to cover user-defined types, whereas Haskell's approach can accept integer literals where any type with an instance of the `Num` typeclass is expected. One alternative, combining the best of both worlds, would be to infer the narrowest type which can hold the literal value and add an implicit `.into()` for non-lossy conversion to any compatible type.
While I agree that you don't want to mix up actual division and floor division, I also rather strongly prefer “/” to be actual division, not floor division, irrespective of the operands. int/int -> rational is the most correct behavior. Scheme’s numeric tower is the poster child for getting this right (not just for division but for things involving numbers generally, including decimal literals specifying exact numbers and not approximate binary floats by default.)
If I want an operation that is not actual division, that that is what is happening should be visually distinct in code.
Hint: it's not about integer types at all
Because I started reading. I got pretty far, and it was _still_ talking about integer types. The article is very long, and I gave up, not having any idea why I should keep reading. So saying the article isnt about them "at all" is wrong. Maybe, the int type thing is an intro to some other thing? But thats a long intro. So, tl;dr, at least not all the way. Why don't you tell us the point so we have desire to actually read it.
As you can also see from the table of contents at the top of the article page, it goes through a great breadth of topics. And you can use that table of contents to jump to the afterword.
Different kinds of numbers
Conversions and type inference
Generics and enums
Implementing traits
Return position
Dynamically-sized types
Storing stuff in structs
Lifetimes and ownership
Slices and arrays
Boxed trait objects
Reading type signatures
Closures
Async stuff
Async trait methods
The Connect trait from hyper
Higher-ranked trait bounds
Afterword
So 17 sections, 16 technical (excluding the Afterword) and only the first two focus on the int/float stuff. So to answer your question:> Maybe, the int type thing is an intro to some other thing?
No, it's not an intro, it's just the first two sections.
Honestly, I don't even mind the folks who do comment without reading. Comments are free, do what you like. I just find it mildly hilarious that 100% of the comments are about (the first) 3% of the article.
I don't understand the urge to comment on something one hasn't read.
I mean, it reads like a Homestuck pesterlog.
I think this post is longer than the entirety of the Oberon-2 language specification, which is about 24 pages long (not including details of certain modules etc.) : https://link.springer.com/content/pdf/bbm%3A978-3-642-97479-...
The safe way is to require the programmer intend about the type to use to be clear in the code and to force the programmer to be mindful about whether or not he is using floating-point. This is just what some languages do, including Rust.
In the first example, the author is essentially complaining that he has to type '2.0' instead of '2'. I don't think that's a lot of extra typing.
Perhaps it is good and convenient to have automatic int-to-float conversion, but such conversion involves error if the int values are large. Conversion in the opposite direction is similarly lossy/erroneous, even if the floating-point type can represent every real number perfectly, since not every floating-point value is an int value. Even with a perfect floating-point type, supporting automatic conversion only in one direction is weird/confusing from a user-interface POV, so the normal thing to do is to just not do automatic conversion.
To drive the point about floating-point error, perhaps some people complaining about lack of automatic conversion may be surprised by how the following round-trip conversion between int64 and float64 accumulates error, as demonstrated in a Python interpreter:
>>> int(float(0xffffffffffffff)) - 0xffffffffffffff
1
If you drop one digit, there is no error. A language forcing you to do the conversion explicitly forces you to be mindful of the potential for floating-point error. A language that does automatic conversion promotes blissful thinking and eventually big surprise.
In a browser console it drops off after 16 digits and right-pads with zeros
> 11111111111111111111
> 11111111111111110000
Our solution was to pass all IDs as strings because they aren't ever going to be manipulated like a number will
I can only have discrete buckets of things. These things have a floating point weight. What is the total weight? Forcing explicit conversion of your int to a float to do the multiplication is semantically wrong.
I could live with banning int + float or int - float.
It's almost as bad as go requiring you to convert a number to a time to figure out the integer multiple of a time interval.
Performing a float op with an int, which you said should be allowed, entails automatic int-to-float conversion before the op, does it not?
Whether there is a conversion is architecture dependent. I believe x86 has an opcode for direct multiplication.
Think I'll keep giving Python the squeeze.
Computers are much stricter about what is what. In mathematics people are on the surface less strict, a lot of maths is done up to some sort of isomorphisms/forgetful functor, but we are smart enough to fix up potential type errors. Doing maths, as a mathematician is using a much more expressive language that takes care of some details for you.
For 2 \pi, \pi ( ) is a program, an infinite list, a monad, and we implement it as a lazy calculation sometimes we can extract numbers up to a certain accuracy. 2 \pi is 2 \pi or if you know more, you can write it as another sequence, or pull the 2.
As for integer * float, well what is this supposed to actually mean to a computer?
Integers are not subsets of the reals. Integers are not sets, the reals are not sets. There is a representation of the integers in the reals represented as a set however. There is however a way to act Z on R. It even seems like a very natural operation. However we are doing maths, using our powerful reasoning abilities. Now it is your job to explain all these differences to a computer. Pretty tricky, so how about we just make things more restrictive. If you need real(integer) * real how about you just use that.
> 69 minute read
Heh.
Looking for an opinion about Rust's type system, I am presented with a tutorial on the difference between basic number types. I even think this is a great tutorial, but I wasn't looking for a tutorial. I suppose what I've learned from this is that the type of programmer represented by the protagonist doesn't know about this, and may think it's overkill for their use of numbers. I got tired of reading the tutorial before I became smarter about how to close the gap between programmers who care about types and programmers who don't.
https://fasterthanli.me/articles/lies-we-tell-ourselves-to-k...
I mean, what is the point of making float x int an unsupported operation?
I understand they're different types, but that doesn't seem like a good enough reason. Isn't it obvious that the output type should be float? So why not just cast the int to a float and multiply them and output a float, like other languages do?
I don't know for sure why Rust chose this route, but I would guess it's a mix of type inference/type system considerations and a desire to avoid potential implicit loss of data.
[0]: https://stackoverflow.com/questions/64244351/why-does-ocaml-...
[1]: https://stackoverflow.com/questions/19019093/haskell-multipl...
Floating point operations aren't quite the same as integer operations. Float operations aren't commutative, for one. So it's nice to be suae you really intended to use a float operation on an integer by requiring an explicit cast, so you can get the order correct.
To you that might be obvious. The compiler can't infer what your intentions were from your code.
> "So why not just cast the int to a float and multiply them and output a float, like other languages do?"
What if the result is being stored in an integer type, how should this be handled? In order to compile the code in the ways your post specifies the compiler has to make many assumptions. Choosing to minimise the amount of assumptions made by the compiler about types is part of 'strong typing'. Being a systems programming language, it needs to be concerned with the underlying representation of the data. What if the result variable only has 8-bits, and can't reasonably represent a floating point type at all?
I do lots of coding in Ada, which has a strong type system. It allows you to specify arbitrary ranged numeric types. This allows you to better semantically describe your problem domain. You could have a specific numeric type representing the number of slices in a pizza (an integer), and one specifying the number of kilometres between two points on a map (a float). Multiplying one by the other might not make sense in your problem domain. The compiler would prevent you making what it assumes would be an erroneous calculation.
Avoiding that sort of an error at compile-time is right there at the heart of the core premise of Rust, so Rust should optimize for it.
This boils down to two things: 1. What do we think is correct at any given time for any given program? The compiler can't know so we have to tell it, which brings us to 2 2. How do we write down in our programming language what is supposed to be correct?
The problem with floats is that sometimes you care about precision, sometimes you don't sometimes you care about overflows, sometimes you don't. Sometimes you care about inverse operations, sometimes you don't. Commercial programming language which only expose a "float" types usually are unable to deal with the greater complexity of _ensuring_ that some property you care about isn't broken. That is why compilers let you operate on floats and shoot yourself in the foot when you divide and then multiply back.
On the other hand, one could imagine a future programming language (and some academic experiments already exist) where you can tell the computer "in this part of the code, it's important that I never overflow" or "in this function, I expect multiplication and division to be inverses of each other". In which case the compiler will display diagnostic information if you do something that break those properties.
It's not clear yet what is the most convenient user interface to write down and check those properties. But many believe it by using even more advanced type systems than Rust. Many other believe that we can add static analysis atop existing programming languages to obtain the same result.
TLDR: Rust allows it for now because 50-100years in the future we'll have to tools to tell when it's ok and when it's not ok to multiply floats together. Right now we're still smacking rocks together to make fire.
> "Compilers (and type systems) are written by and for humans..."
The abstraction that is the type system might be designed for humans, but the underlying physical layers are totally non-abstract, tangible things with their own constraints. If I'm sending data to a DAC over the i2c protocol, the protocol has very real constraints at the electrical level. In order to interact with such protocols in a meaningful way we need low-level semantics to control the shape of data at the level of individual bits.
Consider the following:
uint32_t a = 1234567;
float b = 0.34;
uint8_t c = a * b;
fwrite(&c, 1, 1, stream);
What value gets written to the stream here?> "first tell me the use case for multiplying a float by an int that does not lead to a float"
Should the Rust compiler forbid all multiplication between the two types that doesn't get stored in a float?
That being said, multiplying by 2 not working is pretty funny because that's always exact until you overflow the float. It's pretty annoying the compiler didn't just figure out there was no precision loss anywhere here so just do it instead of complaining.
You're getting warmer. The reality is compilers default floating point is wrong for most uses. You should be able to represent things like pi and 1/3 exactly by default. That that's not the case in JS is criminal.
So in a static typing context float * int has to be float. Otherwise it is not * it is some other operations (e.g. * composed with floor).
In dynamic typing you could decide the type at runtime. A JS runtime could in theory do that under the hood although semantically it should behave as a float.
Moreover in many cases int * float is an intrinsic, so on the hardware level there is no conversion operation interposed into the operator.
Out of curiosity, what instructions support mixed integer/floating-point operands? I can think of integer-/floating-point-only instructions off the top of my head, and I know I've seen conversion instructions, but not mixed ones. I'm hardly an assembly expert, though, so I wouldn't be surprised if I just wasn't aware of such a thing.
Does seem that it converts its arguments first, though?
> The FIMUL instructions convert an integer source operand to double extended-precision floating-point format before performing the multiplication.
(Via https://www.felixcloutier.com/x86/fmul:fmulp:fimul)
In addition, I thought compilers tended to avoid the x87 FPU registers nowadays?
At the end of the day you still need the float multiplied.
[1]: https://play.rust-lang.org/?version=stable&mode=debug&editio...
How does the compiler know what my intentions were when I multiplied two floats? Maybe I intended to receive a partridge in a pear tree.
Intentions are not relevant here. What's relevant is what makes sense for a multiplication operator to do.
All type systems are just abstractions designed to make it easier to express your intent while minimising mistakes. Storing the result of dividing an integer by a float, or vice-versa, in an integer is totally possible in C, but very likely not the behaviour the programmer intended. Should the compiler just forbid this? I guess what Rust has done is just one possible tradeoff.
For what it's worth, Ada has actually introduced semantics for dealing with arithmetic using units of measurement[1].
1: https://www.adacore.com/gems/gem-136-how-tall-is-a-kilogram
That's a very strong statement and I don't see any back up on this claim.
> At any rate, I now find myself in a beautiful house, with a beautiful wife, and a lot of compile errors.
Sounds like a layer 8 problem to me.
I already questioning everything in this blog post that comes after.
For example, the "confident asshole" link goes to the author's own site. He's referring to himself as the "confident asshole". He's being tongue-in-cheek, funny, joking.
The "propaganda" is also a joke because, well, it's true that memory safety issues lead to security problems.