Rant: I hate that it's possible to iterate over an integer

Right, and I think it’s not implemented for some good reason. In my case, being able to iterate over an integer turned out to be confusing and caused quite a few hard to track down bugs. Maybe there are other reasons.

I was just trying to explore what other languages did in this case. Doesn’t necessarily mean Julia should follow these languages.

Same thing with Haskell, by the way:

main = print $ map fromIntegral (1::Integer)

Error message:

51307213/source.hs:3:34: error:
    • Couldn't match expected type ‘[Integer]’
                  with actual type ‘Integer’
    • In the second argument of ‘map’, namely ‘(1 :: Integer)’
      In the second argument of ‘($)’, namely
        ‘map fromIntegral (1 :: Integer)’
      In the expression: print $ map fromIntegral (1 :: Integer)

Basically, Haskell (rightfully) expects the second argument to map to be an array of integers, not the integer itself.

Whereas in Julia this “works”: map(Float64, 1) == 1.0. Of course, comparing Julia and Haskell/Rust may not be entirely fair (Haskell, AFAIK, has a different type system than Julia, so what makes sense in Julia doesn’t necessarily make sense in Haskell, and vice versa) - these are just some possibilities I’m thinking about. Something along the lines of “everyone (maybe not everyone - I don’t know) does it like this, but Julia and R choose a different route - does it really bring noticeable benefits?”

1 Like

Yes, it does:

for k in axes(P, 1) will do as intended, since it is a range, even if that range is occasionally of length 1.

5 Likes

BTW, this is recognized as a WAT in Julia: 🚧 WIP 🚧 A most refined collection of Julia WATs

Numbers are iterable:

julia> first(1,2)
1-element Vector{Int64}:
1

Rationale: Partly explained in the docstring for first . Maybe a MATLAB-ism.

julia> # Credit to Dheepak Krishnamurthy
julia> 1[1][1][1] == 1
true

Though I’m not sure how the docstring for first explains this. It just says:

first(itr, n::Integer)

Get the first n elements of the iterable collection itr, or fewer elements if itr is not long enough.

So looks like one single integer is officially an “iterable collection”.

just remove the word collection, iterable is iterable

2 Likes

I would rather think of numbers as values without any particular dimensionality attached to them. There are many ways that we can interpret numbers such that they have various methods on them, but I want that to be something I opt into when I want it.

When I want to make a “scalar” number, I want an easy way to wrap it in a zero-dimensional container or explicitly use a zero-dimensional number type. Likewise I don’t want strings, symbols, structs, or functions to be iterable by default, even though there are ways to interpret these things as iterable.

It is surely convenient for some use cases which is why Matlab has it and Julia inherited it, but Julia is now used in many more cases than those. Of course, which methods to add to a type is a judgment call, but for my use cases I’d be better off without it.

The main question to me is should we bother fixing it. That requires looking at the usage again when 2.0 is being designed.

You can create a 0-dimensional array with fill:

julia> a = fill(3)
0-dimensional Array{Int64, 0}:
3

julia> a[]
3

julia> a[] = 4
4

julia> a
0-dimensional Array{Int64, 0}:
4

But probably using Ref is better, unless you specifically want the AbstractArray behavior of pretending to have any number of trailing singleton dimensions (e.g. a[1,1,1,1] == a[1] == a).

3 Likes

For some historical perspective on this matter, have a look at:

https://github.com/JuliaLang/julia/issues/7903

5 Likes

We tried changing this pre-1.0 but it was surprisingly disruptive, not because of broadcasting which was not widely used at that point, but because there’s a lot of array code where being able to treat an index and a collection of indices uniformly is very useful. Perhaps @mbauman can say more as he was the one who attempted the change.

10 Likes

I think it was me? The relevant links are here and here.

9 Likes

Hah, right, wrong person! It was usually Matt with array code :grimacing:

7 Likes

There is some more casual discussion in this Zulip thread.

I agree this is terrible, and it’s a shame it wasn’t successfully removed pre 1.0. It’s terrible for two reasons: Primarily because in my experience, it leads to so many unnecessary bugs. I have to track down a bug due to this misfeature at least twice a month, so this is a hobby horse of mine. This is less an academic discussion, and more a practical consideration: Iterability of numbers make the language harder to use.

Secondarily, because I don’t think it makes sense semtantically. I don’t think numbers should be thought of as 0-dimensional things. I think they should be thought of as primitive values. It’s not helpful at all to bring up linear algebra considerations in the context of numbers: Numbers are fundamental and universal, used in all computer programs, and taught to 5 year olds, linear algebra is not. If it makes more sense in LA terms to have numbers iterable, too bad, the concerns of kindergarden arithmetic trumps the concerns of grad school LA. And if you don’t put you LA goggles on, the idea that a number is a 0-dimensional collection of sorts makes no sense.

The worst argument in favor of the iterability of numbers is really “it lets you write code Y that does X”. I just don’t understand that argument. I mean, we could also define + for strings to uppercase them - wouldn’t that be handy? Then you could just write +"foo" instead of uppercase("foo"). But that’s missing the point! You shouldn’t be able to do +"foo", and being able to do it is not a feature, it’s a problem! Similarly, if you want to iterate over a number, just put it in a tuple - then it is actually a collection.

8 Likes

I grant you, + is invalid but …

julia> macro u_str(p) uppercase(p) end
@u_str (macro with 1 method)

julia> u"abc"
"ABC"

I’ll agree that it’s a minor footgun but I think describing it as “terrible” is a bit overstated. There is a perfectly coherent point of view in which scalars are equivalent to 0-dimensional arrays, which is where this behavior comes from, and saying that it “makes no sense” is… just not true because there’s a reasonable point of view in which it does make sense.

However, as @stevengj pointed out in the linked issues, that point of view suggests that we should treat all scalars as 0-dimensional arrays, but that conflicts with arrays of things that are themselves iterable. So that’s where the real problem lies. We have 123 behave like fill(123) (i.e. 0-dimensional array containing the value 123), in that it produces just the value 123 itself when you iterate it. This suggests that all x should behave like fill(x) and iterate just the value x when you iterate it, which conflicts with the existence of x which are themselves iterable, such as [1,2,3] or "abc". That to me is the real issue, so it would be worth investigating if we can remove it in 2.0.

21 Likes

A better example would not be that Ref(42) produces just 42 instead of Ref(42)? This is, numbers are infinitely recursively zero-dimensional different from other scalars?

That’s just a specific example of an iterable container, so similar to [42].

I’m not sure I follow this chain of reasoning. With numbers it’s natural that the element of the number is the number itself, but I don’t see why that should be the case for fill(123) or Ref(42). These things are explicitly containers that you’re meant to put something in.

Zero dimensional objects have one element, that element doesn’t generically need to be the container itself.

2 Likes

Here is an idea for Julia 2.0. There could be a new macro “strict”. A user would say:

@strict module MyModule
    ...
end

This macro would disallow certain features of the language such as iterable numbers that make it easier to write short codes but that can also lead to subtle programming errors. Is this possible? Advisable?

Having a feature for opting out of undesired behaviors seems useful for 1.0. For 2.0 I’d rather such behaviors be removed entirely.

1 Like

For the common footgun of for-loops only iterating a single time, you could write this macro yourself pretty easily to tree-walk to find for-loops and insert code to error on Number types. Something like:

julia> using MacroTools

julia> macro strictfor(ex)
           MacroTools.postwalk(ex) do x
               @capture(x, for i_ = itr_; body__ end) || return x
               return quote
                   let y = $itr
                       @assert !(y isa Number)
                       for $i=y
                           $(body...)
                       end
                   end
               end
           end
       end
@strictfor (macro with 1 method)

julia> @strictfor f(x) = for i=x
           println(i)
       end
f (generic function with 1 method)

julia> f(1:2)
1
2

julia> f(1)
ERROR: AssertionError: !(y isa Number)
Stacktrace:
 [1] macro expansion
   @ ./REPL[41]:6 [inlined]
 [2] f(x::Int64)
   @ Main ./REPL[42]:1
 [3] top-level scope
   @ REPL[44]:1

Now modules have to be at the top level, so you can’t apply this macro to modules in the way you show, but other than that limitation…

3 Likes

I did also attempt it prior to your attempt. I had removed one case where we relied upon it and quickly came to a similar conclusion:

Comment on JuliaLang/Julia#10331

2 Likes