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.
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?”
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.
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).
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.
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.
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.
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?
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.
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?
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…