julia> map(_ -> 0, 0)
0
julia> struct Nvmber end
julia> Base.ndims(::Nvmber) = 0
julia> Base.length(::Nvmber) = 1
julia> Base.iterate(f::Nvmber) = (f, missing) # The `missing` here can be anything
julia> Base.iterate(f::Nvmber, state) = nothing
julia> map(_ -> Nvmber(), Nvmber())
1-element Vector{Nvmber}:
Nvmber()
julia> map(_ -> Nvmber(), 1)
Nvmber()
Note that 0 (or any numbers) is a point in a Vector Space scalar having ndims(⋅) = 0. So the outcome in the first line is 0, which is also of ndims zero. So map is a “closed” operator on the number field (is this the correct term?).
But this pattern is not preserved in the penultimate command, where it returns a Vector—having ndims(⋅) !== 0, hence the map is no longer a “closed” operator. (The last command is correct, though, in terms of preserving ndims)
Conclusion: preserving length is not enough:
julia> length(0) == length([0]) == 1
true
since clearly 0 is very different from [0]. map should also preserve ndims, Maybe also preserving axes whenever applicable:
I agree this is undesirable. map’s generic method should be returning a container of the same size as the input, so here the sensible generic fallback would have likely been an Array{Nvmber, 0}.
The issue here is just that the default IteratorSize for any type is HasLength(). If you overload IteratorSize and size, map indeed returns a 0d array:
Well, this probably isn’t what an user want either. A 0d array is strange in practice. I guess just the scalar is better. I’ll have to admit that it’s somewhat inconsistent. But I think this special method map(f, x::Number, ys::Number...) = f(x, ys...)
makes more sense. And it’s much more straightforward.
There are options for what to return for ()-shaped objects by default:
a 0d array
a Ref
a scalar
Out of them, a 0d array probably follows the principle of least astonishment, because for non-empty-shaped collections map returns a correspondingly-shaped array. A Ref would probably work just as good with less overhead, though.
A scalar, I think, does add inconsistency, and special-casing numbers breaks an invariant shape(map(f, x)) == shape(x) if f returns a collection.
Well, as long as I don’t use map on a zero-ndims object, I think the existing behavior is okay.
Maybe the behavior I want is broadcast.
Close this topic.