Why does scientific notation break the range function?

This line of code creates a 1000 element array of values, evenly spaced from 0 to 10:

x = range(0,10,length=1000)

The code above works perfectly. This line of code, however, throws an error:

x = range(0,10,length=1e3)

As I understand it, 1000 should be the same thing as 1e3, and I’ve checked to make sure Julia knows they’re equal:

julia> 1000==1e3
true

Can anyone help me understand why length=1000 works, while length=1e3 throws an error? Here’s the error message it produces, which to me is 30 lines of cryptic nonsense:

TypeError: in keyword argument length, expected Union{Nothing, Integer}, got a value of type Float64

Stacktrace:
  [1] top-level scope
    @ ~/Documents/coursework/Untitled-1.ipynb:1
  [2] eval
    @ ./boot.jl:373 [inlined]
  [3] include_string(mapexpr::typeof(REPL.softscope), mod::Module, code::String, filename::String)
    @ Base ./loading.jl:1196
  [4] #invokelatest#2
    @ ./essentials.jl:716 [inlined]
  [5] invokelatest
    @ ./essentials.jl:714 [inlined]
  [6] (::VSCodeServer.var"#164#165"{VSCodeServer.NotebookRunCellArguments, String})()
    @ VSCodeServer ~/.vscode/extensions/julialang.language-julia-1.6.17/scripts/packages/VSCodeServer/src/serve_notebook.jl:19
  [7] withpath(f::VSCodeServer.var"#164#165"{VSCodeServer.NotebookRunCellArguments, String}, path::String)
    @ VSCodeServer ~/.vscode/extensions/julialang.language-julia-1.6.17/scripts/packages/VSCodeServer/src/repl.jl:184
  [8] notebook_runcell_request(conn::VSCodeServer.JSONRPC.JSONRPCEndpoint{Base.PipeEndpoint, Base.PipeEndpoint}, params::VSCodeServer.NotebookRunCellArguments)
    @ VSCodeServer ~/.vscode/extensions/julialang.language-julia-1.6.17/scripts/packages/VSCodeServer/src/serve_notebook.jl:13
  [9] dispatch_msg(x::VSCodeServer.JSONRPC.JSONRPCEndpoint{Base.PipeEndpoint, Base.PipeEndpoint}, dispatcher::VSCodeServer.JSONRPC.MsgDispatcher, msg::Dict{String, Any})
    @ VSCodeServer.JSONRPC ~/.vscode/extensions/julialang.language-julia-1.6.17/scripts/packages/JSONRPC/src/typed.jl:67
 [10] serve_notebook(pipename::String, outputchannel_logger::Base.CoreLogging.SimpleLogger; crashreporting_pipename::String)
    @ VSCodeServer ~/.vscode/extensions/julialang.language-julia-1.6.17/scripts/packages/VSCodeServer/src/serve_notebook.jl:136
 [11] top-level scope
    @ ~/.vscode/extensions/julialang.language-julia-1.6.17/scripts/notebook/notebook.jl:32
 [12] include(mod::Module, _path::String)
    @ Base ./Base.jl:418
 [13] exec_options(opts::Base.JLOptions)
    @ Base ./client.jl:292
 [14] _start()
    @ Base ./client.jl:495
1 Like

1e3 is a Float64, but Int is expected.

3 Likes
julia> 1000==1e3
true

julia> 1000===1e3
false
7 Likes

For the start of errors you can ignore what is after Stacktrace: , the first line says it all:

expected Union{Nothing, Integer}, got a value of type Float64
10 Likes

I must confess, Union{Nothing, Integer} is complete nonsense to me.

The entire error message, including the first line, is unnecessarily cryptic and overly complicated for such a simple issue. Even the first line of the message requires users to understand multiple Julia commands well enough to effectively compute code in their head.

Even as I now understand what the first line is trying to say, I cannot understand how Union{Nothing, Integer} is the same thing as saying Int. To figure this out, I find myself searching the docs for Nothing, and find this:

help?> Nothing
search: Nothing nothing isnothing

  Nothing

  A type with no fields that is the type of nothing.

  See also: isnothing, Some, Missing.

So tell me, what part of this error message is helpful to users who may innocently use 1e3 to represent the number 1000?

1 Like

This part:

TypeError: in keyword argument length, expected Integer, got a value of type Float64 

This is very helpful.

Unfortunately, there is also a part mentioning a union with Nothing, which is pretty confusing.

1 Like

Yep, and after that there’s 30 lines of Stacktrace garbage that buries the tiny morsel of the error message that could potentially be helpful.

From my perspective as a beginner, cryptic and unhelpful error messages appear to be pretty pervasive in Julia, and it makes it really hard to get started. I don’t know what I’d do without this forum!

But back to the original issue: Should the range function be updated to allow whole numbers for the length definition, even if they’re Floats?

1 Like

I agree this could be better, but the useful part is at the very top, so it’s difficult to agree that it’s buried.

I don’t think so. Only whole numbers make sense, so restriction to integers make sense, and helps catch errors earlier and more reliably.

A similar discussion was had (on github) concerning indexing with floats, which used to be allowed, way back. I cannot find it at the moment, but it may contain some relevant points.

4 Likes

Yes, we’d definitely like to abbreviate stack traces by default in the future. See e.g. Suggestion: abbreviate stack traces by default · Issue #40138 · JuliaLang/julia · GitHub

8 Likes

Allowing floating-point values to specify array lengths was discussed pretty early on in the development of Julia (in 2011!), and was ultimately rejected. See the discussion here Matrix constructors should be able to take floating point value for size · Issue #136 · JuliaLang/julia · GitHub and here Pass integer valued floats to matrix constructors · Issue #1972 · JuliaLang/julia · GitHub and here zeros(1e4) -> ERROR: no method Array(DataType,Float64) · Issue #4275 · JuliaLang/julia · GitHub for example.

6 Likes

For the sake of the big picture, I disagree. When driving a car, I prefer a manual transmission, but I’m a car enthusiast, and I recognize that most people are just driving to the store, and don’t want the extra work of operating a clutch. By the same token, if Julia is to be widely adopted by people who just want to accomplish a task and are not coding enthusiasts, I think it’s an unnecessary burden to make them write this:

x = range(0,10,length=Integer(1e3))

when they could just as easily write this:

x = range(0,10,length=1e3)

Before you point out that many ordinary drivers (especially outside the USA) prefer a manual transmission, I note that no culture has ever gone back to widespread use of manual transmissions after growing accustomed to automatic transmissions.

I am currently coming from MATLAB, which graciously accepts either input:

>> x = linspace(0,10,int16(1e3));
>> x = linspace(0,10,1e3);

To me, the fact that Julia breaks with 1e3 but not 1000 feels like a small and unnecessary step backward from MATLAB.

2 Likes

You would normally write one of these

x = range(0,10,length=1000)
x = range(0,10,length=10^3)

I don’t often see “Matlab” and “graciously” in the same sentence :grin: I’d point out that all number literals are floats in Matlab, though, so you almost have to allow that.

Julia cares about types. I’m afraid it’s just something to get used to.

23 Likes

Note that stacktraces are not always that long. Your stacktrace is worse than usual because you are running a Jupyter notebook in VS Code. If you run that one line of code directly in the REPL, you get this:

julia> range(0, 10, length=1e3)
ERROR: TypeError: in keyword argument length, expected Union{Nothing, Integer}, got a value of type Float64
Stacktrace:
 [1] top-level scope
   @ REPL[1]:1

Here’s another option if you want to write a long integer:

range(0, 10, length=1_000_000)

In other words, use underscores to make the integer more readable.

14 Likes

It’s written as Union{Nothing, Integer} because the length keyword has been implemented so that it takes in an Integer. This is an abstract type which takes into account all kinds of integers, not just 64-bit ones (Int64, or Int), for example:

julia> range(0, 10, length=10^BigInt(100))
0.0:1.0e-99:10.0

The range function also has the case where the length is not given, and it’ll default to a stepsize of 1. That’s where the Nothing type comes in.

I agree the error message is confusing, as well as Integer vs. Int, but I don’t think the Union itself is too bad.

5 Likes

How’s this for a toy idea?

julia> 1E6 === 1_000_000
true

julia> 1E-6
ERROR: Domain error
Integer valued powers of 10 cannot be less than 1. Do you mean 1e-6?

Not sure I think this is necessary, and it might just add to the confusion, but the syntax is up for grabs.

2 Likes

I think you’re arguing here for a “do what I mean” behavior. Floating point “integers” like 1e10 usually don’t match their closest integers perfectly, but there’s rounding error involved. So saying something like “9.9999999” is 10 just because it’s very close to 10 invites a lot of bugs and weird behavior. For example, how close should a floating point “integer” be to its next integer so that it is allowed to be used as one? I personally like the decision to just squash that whole class of bugs by requiring integers. If you want the language to “do what I mean” in one simple situation like 1e10 you often have to deal with bad egde cases in other circumstances that are not immediately obvious.

14 Likes

In general, Julia is actually very accommodating when it comes to accepting varying input types. Much more so than Matlab. When calling built-in Matlab functions, you will often see that only double is accepted (perhaps one or two more built-in types), because it calls into compiled C++ libraries.

Julia functions are very often highly generic, and accept any type that “quacks like a duck”.

What happens if you try

linspace(int64(0), int64(1), 10)

in Matlab? This works in Julia, with all sorts of integers, (after fixing the names).

In Matlab things are built around double, and almost everything is a double. Stray from that and you soon get in trouble.

2 Likes

The error thrown by Julia is actually pretty good in this case, and almost the same as Python gives:

julia> range(0, 1, length=1e2)
ERROR: TypeError: in keyword argument length, expected Union{Nothing, Integer}, got a value of type Float64
Stacktrace:
 [1] top-level scope
   @ REPL[1]:1

vs

In [1]: import numpy
In [2]: numpy.linspace(0, 1, 1e2)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-2-5b8b1326f642> in <module>
----> 1 numpy.linspace(0, 1, 1e2)
<__array_function__ internals> in linspace(*args, **kwargs)
/usr/local/lib/python3.9/site-packages/numpy/core/function_base.py in linspace(start, stop, num, endpoint, retstep, dtype, a
xis)
    111 
    112     """
--> 113     num = operator.index(num)
    114     if num < 0:
    115         raise ValueError("Number of samples, %s, must be non-negative." % num)
TypeError: 'float' object cannot be interpreted as an integer

Allowing floating point values would open a whole another can of worms. For example, 1e10 == 10^10, but 1e20 != 10^20.

14 Likes

Unfortunate example since the left hand side can be represented exactly but the right hand side overflows.

julia> 1e20 == BigInt(10)^20
true

julia> 10^20
7766279631452241920
4 Likes

Indeed, you are right! A better example would probably be Int(1e16 + 1) == Int(1e16) which would make range(... length=1e16 + 1) very confusing.

1 Like