Comparing different types doesn't raise an error?!?!?

I’m sorry but this is annoying, I try to convert 3.5 to an Int and I get an InexactError. I mean I really do just want to truncate the fricken number, but fine, I will truncate it, then convert it, hey maybe I’ll splurge and round it, doesn’t really matter.

However I accidentally put the wrong variable in a comparison so I’m comparing a string with an Int…and that’s totally fine! Because of course comparing different types is SOOOOO common, at least compared to how often people just want to convert a float to an int.

Sorry I’m venting, shame on me for having two variable close in name…but I would expect if I have to be EXACT with conversions, I would have to be EXACT with comparisons as well!

That conversions was particularly annoying because really i was parsing a string as a float with 2 digits of precision, multiplying it by 100 and displaying it. But of course that particular value couldn’t be exactly represented in a float hence the InexactError error during the conversion…

Sorry you found this annoying. In Julia you can compare any two values and not get an error—it may just tell you they are not equal. Consider the alternative: should 1 == 1.0 throw an error just because they are different types? If not, why are real-valued numbers different from other types?

Conversely, I think many people would be unhappy if 3 == 3.5 returned true. If you accept that, then I think you need to be worried about automatic rounding upon assignment: for example, if you assign x = y, then you might be forgiven for anticipating that x == y. But if it behaved the way you have urged, this would not be true.

For your immediate problem, I suggest you check out the manual: https://docs.julialang.org/en/latest/manual/mathematical-operations/#Numerical-Conversions-1.

4 Likes

truncate(Int, ...)

2 Likes

Vent away if it helps, we are here to listen :wink: But I am not sure what your problem was, did you run into

julia> 1 == "a fish"
false

with the expectation of getting an error? Or was it something else?

Yes, I find I do it quite frequently. Eg

ix = findfirst(isequal(z), xs)
if x == nothing
    do_stuff()
end

A lot of programmers have an expectation about equality comparisons. I would recommend reading Kent Pitman’s classic. TL;DR:

There is no uniquely determined equality function for complex structures–there are only arbitrary ones.

You don’t need two steps, round/trunc/ceil/floor all take a type argument.

I guess it’s that I’ve found Julia to require me to be EXACT when it comes to data types. If I have a method:

function foobar(a::Int32)
end

foobar(0)

That results in a compiler error, I’m assuming because I wasn’t exact. Nevermind that there is only one foobar method defined. Constants are Ints (or maybe Int64s) and the compiler will not automatically change the data type of a static. I must be EXACT in my data type declarations, no assumptions will be made.

This “side effect” I really love:

function foobar(b::Int, a::Int32 = 0)
end

foobar(4)

ERROR: LoadError: MethodError: no method matching foobar(::Int64, ::Int64)
Closest candidates are:
  foobar(::Int64, ::Int32) at /home/pixel/test.jl:2
  foobar(::Int64) at /home/pixel/test.jl:2

Fine, I accept that, all my constants are wrapped in their data types.

Likewise data type conversions must be exact, if they can’t been done exactly an exception is thrown (not a compiler error but still drastic). I needed to do bit twiddling on numeric values, so I needed to treat signed values as unsigned. I was required to use reinterpret() for that change…again I needed to be exact in what I wanted to happen, no assumptions!

Again I’ve accepted I must be exact, it’s a good thing, it ensures I tell the compiler EXACTLY what I’m doing.

Then there are comparisons…oh THERE you don’t need to be exact because, because…because I don’t know why.

julia> 5 == 5.0
true

julia> UInt(5) == -5.5
false

Here however, 5 == 5.0 automatic conversion, I guess it was a safe conversion? And UInt(5) == -5.5 is false not because the values are different but because a common type cannot be found for both types…I guess? Okay so where is the exactness that I’m required everywhere else?

And if safe conversions happen automatically then why not here?

function foobar(b::Int64, a::Int128 = 0)
end

foobar(4)

ERROR: LoadError: MethodError: no method matching foobar(::Int64, ::Int64)
Closest candidates are:
  foobar(::Int64, ::Int128) at /home/pixel/test.jl:2
  foobar(::Int64) at /home/pixel/test.jl:2

Wouldn’t an automatic conversion from Int64 to Int128 be safe?

I guess I just assumed I needed to be exact to avoid unintentional programming errors. Yet comparing values of different types I don’t need to be exact…I can be as sloppy as I like?

I think you’re confusing intrinsic properties of the language (like dispatch) with method existence. == is just a function, and it has been defined (exactly) that it will accept unequal types.

It’s not a language issue, as such. Equality isn’t a built-in, magical thing.

1 Like

It requires you to be exact because that’s what you ask for. You don’t need that ::Int32 most of the time.

Because you didn’t ask for exact. == is defined properly to not require that, unlike the foobar above.

3 Likes

Possibly.

I like consistency. Consistency makes it easy to remember what is going to happen. And if I need to be exact in one aspect of a language, I would expect to be exact in other aspects.

Truthfully this was just one more minor thing on top of a bunch of minor annoyances when I was close to getting things working.

Also, in terms of argument conversion, there was an issue about adding a syntax for that, but that’s a syntax sugar completely orthogonal to dispatch.

Not sure if you realized that but there are MANY different types. There are comparison defined for different array types for example, and there are more than one string types, number types etc. I do agree there are some comparisons that is more likely to be an error than not in some context, but it is still context dependent (admittedly requiring isequal in those context isn’t too bad) and it’s very subject to say which comparison is good and what’s not.

More over, julia is very exact about types, so in the specific context where you want to make sure the type is good, you can always use a type assertion with basically no additional cost.

You did realize that you did not need to be exact with conversion on type the same way you don’t need to be exact with comparison on types right? Also, you also realize that you do need to be exact with value on conversion the same way you do need to be exact with value on comparison right? AFAICT this is very consistent.

You cannot expect everything in the language to be all exact or not exact about everything at the same time. That’s logically imposible.

1 Like

True. And I’m kind of back on forth on if I should declare the type…ultimately the value is being passed to a C library where the value is an Int32. Not sure if it’s better to keep it an Int32 because that is what it’s going to be, or let it be “whatever” and do the conversion right before I pass it to the library. The later would be less typing and “easier”.

There was an article I can’t find…the title was basically “Your bad code is destroying my planet.” The gist of the article was that companies pump out less efficient code because it’s faster to produce and thus cheaper, but it does require more CPU to run which cause more greenhouse gasses, and on and on.

And yes, in this case, the few nano seconds I save is not big in the grand scheme of things, but given the option I prefer to save the cycles.

And perfered, unless your own code requires it to be a Int32.

What cycle are you saving here? Adding the type assertion has absolutely no effect on efficiency. If you are adding since you want to require the user to pass in the right type (since that will likely save time to avoid conversion everywhere) then you should be happy that you got an error if it’s the wrong type coming in. Adding a conversion there just move the where the “extra cycles” is, rather than saving it.

2 Likes

If you don’t like equality, what about less than? Should 2.1 < 5 error? It seems to me that disallowing equality tests between unequal types would make generic programming very hard.

Do you think that the cpu cycles have a bigger (initially wrote smaller) carbon footprint than the programmer-hours? I wouldn’t bet on it…

Values are fundamental truth. There is nothing mathematically wrong about asking 3 == 3.5. Types are a construct of programming. They shouldn’t matter unless there is no alternative. Int(3.5) is not possible, because you’re specifying the type.

I’d urge you to have a bit more humility about the possibility that your own world view is shaped by your personal experience rather than a law of nature that only you see.

4 Likes

What about posting code snippets? That way you could turn rage into useful help much more efficiently

2 Likes

Okay I give up, I don’t know what to to do, or what I should do. Assignment do have automatic conversions, just not when it comes to methods…

mutable struct Foo
    a::Int32
    b::Int64
    c::Float32
    d::Int8
    e::UInt8
    Foo(a, b, c, d, e) = new(a, b, c, d, e)
end

Foo(1, 2, 3.5, -5, 8)

That all works, types are converted without me specifying it.

From a coding for performance standpoint writing:

function fooset(f::Foo, e::UInt8)
    f.e = e
end

Ensures that there is no conversion overhead like checking for underrun/overrun. While a generic:

function fooset(f::Foo, e)
    f.e = e
end

Would be easier to use. But if the caller does not pass in a UInt8 it would contain some (not much) overhead…Which just leaves the question of how many times does the conversion have to be called before it’s considered a performance hit and needs to be optimized. Maybe that’s just something only the program manager can answer because they know how fast the program has to run. (My worldview has always been that if it’s unnecessary code then once is one time too many.)

All that said, I still really don’t like that this errors:

function foobar(b::Int, a::Int32 = 0)
end

foobar(4)

ERROR: LoadError: MethodError: no method matching foobar(::Int64, ::Int64)
Closest candidates are:
  foobar(::Int64, ::Int32) at /home/pixel/test.jl:2
  foobar(::Int64) at /home/pixel/test.jl:2

While this works:

function bar()
    local a::Int32 = 5
end

Calling method is not an assignment. I mean, where is the assignment (=) when you call a function?

That’s exactly what I said. If you worry about the performance issue (which is basically none for this case no matter how many time you do it for machine integer types), you should pass in a UInt8 from the beginning. The two versions of code have no difference when you do that. If you want to pass in a non UInt8, you’ll always have a conversion and what you wanted is simply putting that on the function call, which makes absolutely no difference in this case. If you actually care about avoiding conversion, as I said abve, you should be very happy about the current behavior since it makes sure you never pass in anything other than the correct type and therefore avoid any conversions.

I do not disagree with that. But,

  1. That’s not a inconsistency since method call is not an assignment.
  2. This is a necessary trade off to make it fit everything else. It does lead to more confusing cases when you have more than one methods and there’s an issue for it. In any case, this (default parameter) is an isolated case on it’s own that has absolutely nothing to do with all other cases you’ve brought up.
1 Like

Also, to summarize all the issues (AFAICT) that you’ve brought up so far,

  1. Consistency in exactness between comparison and conversion

    They are consistent. Both are defined to be relaxed about the type and both are defined to be exact about the value. (Of course how they response to inexactness is different but that’s due to the nature of those methods. A comparison that errors on non-equalness is not very useful)

  2. Comparing string to number.

    There are indeed cases that are likely to be coding error. If it’s unambiguous we’ll try our best to make it an error. For this case, it’s definately a more subjective one and disallowing comparing different types is definately not a solution. We do give you the tool to compare the types if you need to FWIW so if you know your need you can do that.

  3. Method calls requiring exact type matching

    This is the case we cannot change since it’s fundamental to the language (dispatch). There has been talks about adding conversion syntax to the argument. It’s not high priority and can be done currently manually (just reassign it to a typed local variable instead)

  4. Method dispatch is different from assignment.

    Function calls are not assignment.

  5. Inefficiency from convertion.

    You claim this is the reason you add the type constraint on the method call. That’s a valid reason in general (not super well in this case but that’s still fine). However, in this case, the current behavior is exactly what you want, rather than just moving the implicit conversion elsewhere.

  6. Behavior of default argument

    This one is an issue. I believe there’s an open issue about it. Personally, I’ll even give you that if you say this case looks like an assignment (as opposed to function call) and should therefore behave like one.

2 Likes

Try to use this (\equiv) for a month, maybe you’ll find it’s better than == :

julia> ≡(x,y) = (typeof(x) == typeof(y)) && (x == y)
≡ (generic function with 1 method)

julia> 1 ≡ 1.0
false

Edit: or just use === ?

As for the argument conversion syntax, the issue was https://github.com/JuliaLang/julia/issues/8710 . See the discussion there. There doesn’t seem to be a very strong opinion either way over there though.

For the optional argument issue, see https://github.com/JuliaLang/julia/issues/7357 . I thought there was also a PR that changes the lowering but couldn’t find it…