[ANN]: ZeroDimensionalArrays.jl: zero-dimensional arrays/references/boxes

ZeroDimensionalArrays.jl is being registered, meaning it has entered the three-day-long wait period. Feel free to suggest breaking changes until the wait period is over!

It’s a tiny package, implementing several similar types, each being a zero-dimensional array.

The package is a response to the frequent confusion regarding Ref because of the semantic overloading, and the fact that Julia programmers often don’t expect Ref to be an abstract type. It’s meant to replace most/all non-ccall-related use of Ref. It’s also suitable for replacing Array{T, 0}.

6 Likes

Why not Scalar instead of ZeroDimArray?

I think Box is a pretty good name, but isn’t it used already in Julia in another context, for boxed variable in closures? That might create some confusion…

2 Likes

This would kind of make sense, but I think it’s not acceptable to name an array type just “scalar”? Perhaps something like ScalarArray or ArrayScalar? The bigger issue, I think, is that “scalar” implies something numeric, however a ZeroDimArray is not restricted to holding numbers.

True, there will be a slight amount of confusion when reading code_warntype/code_typed output, but it seems worth it. Keep in mind Core.Box is always fully qualified (the Core. part), and also colored in red in code_warntype output.

2 Likes

I agree with @favba, I don’t really love the current names in the package, I think that Box would be better. Maybe also RefBox or BoxRef could work? They are a bit repetitive but they don’t clash as much with Core.Box at least

What do you mean? Is that a typo?

Not as short as Box, but I think RefVal is also a good name, and closer to its origin.

2 Likes

I think @favba was also proposing Box as a name, or am I wrong? In any case the new proposal of RefVal seems also good to me

Box is what’s currently used.

Let’s have a poll:

What should the name of the mutable zero-dimensional array type be?
  • Box
  • Boxed
  • BoxRef
  • RefBox
  • RefVal
  • BoxVal
  • BoxedVal
0 voters

But this does nothing that isn’t already available in Base, not even more convenient APIs?

I mean, literally the only issue is the unfortunate naming (Base.Ref for the abstract type, Base.RefValue for the concrete type)?

I feel like this is not a fixable thing: Julia 2.0 will learn not repeat the mistake of the bad name; maybe julia will in the future export a type alias, aka a better name, than Base.RefValue. But otherwise, it’s just a minor wart that we have to live with (and Base.Ref is taken, using that name for something else would be breaking).

The cure is obviously much worse than the disease: If you can remember to use an extra package, then you would also be able to remember to use Base.RefValue instead of Base.Ref. And each additional dependency introduces a certain overhead, a la the NPM leftpad debacle. (the overhead is in terms of mental bandwidth and “software supply chain”, not in terms of moving electrons)

Also, Box is a terrible name because ZeroDimensionalArrays.Box rhymes with Core.Box!

4 Likes

This is a tiny package, obviously reimplementing it is easy, so I don’t really get your point. That said, you’re wrong, of the provided types, Box most clearly corresponds to Ref (RefValue), but the other two are not found in Base. In any case, RefValue is not available to be used from Base, it’s not a public interface.

Anything specific in mind? I’d say the interface is slightly more convenient, given that these are array types, unlike Ref.

No. The situation with Ref is a mine field and using it should be avoided except when doing ccalls:

  • Ref is an abstract type, but some of its subtypes don’t implement it according to spec: `Ptr` doesn't observe the supertype semantics of `Ref` · Issue #49004 · JuliaLang/julia · GitHub
  • The fact that Ref is abstract doesn’t seem to be communicated clearly enough in the docs, as apparent from the frequent issues people have with it, see some of the Discourse and Github links in the package readme.
  • Again, RefValue is private.
  • Ref was initially meant to be used for FFI (ccall-ing), but then got hijacked for broadcasting and other uses. Something with FFI-specific features shouldn’t be used as a generic container.
  • Ref has constructor methods even though it’s an abstract type

Base is a dependency, too, for what that’s worth. A fat one. I don’t want to get too off-topic, however I strongly dislike the anti-dependency attitude that lots of people share with you. Especially considering the superb package management story with Julia. IMO, ideally:

  • Base would be much slimmer than it is, something like it should only define primitive types
  • Small packages would be more prominent in the ecosystem.

Cast your vote above?

2 Likes

I have to say that I also find the name of the package itself a bit confusing, specifically: Why the package is named ZeroDimensionalArrays.jl if the main use case is boxing fields? something like e.g. BoxedVals.jl seems better to me. Besides that, why the struct version is called ZeroDimArray while all the rest refers to boxes? Isn’t there a way to uniformize a bit more the naming?

3 Likes

Just to augment the readme, a ZeroDimArray differs from a 0-dim FillArrays.Fill when it comes to broadcasting. A Fill doesn’t drop the array container, whereas a ZeroDimArray does. This behavior is consistent with 0-dim Arrays, whereas a Fill isn’t consistent.

1 Like

Is that a bug?

julia> using ZeroDimensionalArrays, FillArrays

julia> fill(1) .+ 1
2

julia> Fill(1) .+ 1
0-dimensional Fill{Int64}, with entry equal to 2

julia> ZeroDimArray(1) .+ 1
2

Edit, found my issue about this. Original example was:

julia> real(fill(1+im))  # returns an array
0-dimensional Array{Int64, 0}:
1

julia> real(Fill(1+im))  # returns an array of arrays
0-dimensional Array{Fill{Int64, 0, Tuple{}}, 0}:
Fill(1)

But there’s another broadcasting difference, that Fill makes purity assumptions. This one I think is a choice, which FillArrays.jl does not regard as a bug:

julia> (_ -> rand()).(fill(1)) .+ zeros(2,3)  # ordinary fused broadcast
2×3 Matrix{Float64}:
 0.720996  0.162743  0.170527
 0.217833  0.417129  0.955952

julia> (_ -> rand()).(Fill(1)) .+ zeros(2,3)  # calls rand just once
2×3 Matrix{Float64}:
 0.72484  0.72484  0.72484
 0.72484  0.72484  0.72484

julia> (_ -> rand()).(ZeroDimArray(1)) .+ zeros(2,3)
2×3 Matrix{Float64}:
 0.0791902  0.467466  0.74044
 0.06619    0.701926  0.531931
1 Like

I agree, the names seem confusing to me too. I’m not sure why there are 3 different cases at all. I had to look at the source to see what “BoxConst * declared with mutable struct” means. What’s that even for? The only thing I can think of is someplace you do need an objectid but also want to forbid mutation, which seems quite deep in the weeds.

If this is a prototype aiming to replace the use of Ref in broadcasting, then IMO it should be one struct and as few lines as possible.

julia> isa.(Ref([1,2,3]), [Array, Dict, Int])  # README's user-facing example
3-element BitVector:
 1
 0
 0

julia> println.("hello", " ", ["user", "world"]);  # internal usage
hello user
hello world

julia> Base.broadcastable("hello")
Base.RefValue{String}("hello")

There was a PR adding basically Fill to Base for this purpose, and IIRC it failed on the above surprising-broadcasting, plus just being more code than seemed justified.

But this package seems to aim at many other uses of Ref. In this example, I’m not really sure why this is better, why not Ref(0.2) here?

const some_const_binding = Box(0.2)

For fields, clearly abstract mutable_int::Ref{Int} is bad, but isn’t the right pattern for some mutable fields to have mutable struct with const immutable_bool::Bool? Why is this better?

struct SomeImmutableType
    immutable_bool::Bool
    immutable_float::Float64
    mutable_int::Box{Int}
end
1 Like

Boxing is merely one of the uses, see the examples in the readme. Two of the three types, the ones whose names currently start with Box* prefix, box their only element, due to being defined with mutable struct. In contrast, the third type, ZeroDimArray, is defined with struct, not with mutable struct, so it doesn’t do boxing.

Will try to make this more clear in the readme again.

A name such as BoxedVals.jl would only cover two of the three exported types, as just explained.

For performance mostly:

  • Use ZeroDimArray to avoid the overhead of a mutable struct, when a mutable struct is not required.
  • Use BoxConst (that’s the current name at least) to:
    • forbid mutation
    • reap the benefits from letting the compiler know there can’t be any mutation, such as better inferred effects, to enable constant folding

Will try to add something to that effect to the readme, or make it more clear.

Just addressed this immediately above.

Sometimes one just needs a reference to accomplish the goal, that is, access performance is not the only important thing. I’ll quote @foobar_lv2 from two days ago:

It’s possible I haven’t ever written code like that, but the idea is, if it’s not clear, that there’s some data structure one wants to implement, one needs references to implement this data structure, and either:

  • needs to forbid mutation for safety
  • wants to let the compiler know there’s no mutation to enable the compiler optimizer more

Will also try to explain this better in the readme, however I’m afraid it’s becoming too big, possibly causing some to avoid reading it. It’s possibly already bigger than the source code. Perhaps it just needs better structure?

2 Likes

It would cover the majority of the types, then?

While the Boxes can also serve as zero dimensional arrays, that use case is pretty much covered already in Julia: just use Array{T,0} if you want it mutable, SVevtor{0} for immutable. My guess is that’s what someone needing literally a zero dimensional array would do.

It is the boxing aspect that needs improvement in Julia because of the Ref abstract opaquenes and the gatekeeping of Base.RefValue
So I think a name like RefVals.jl would improve the discoverability of the package for the people that would be looking for a package to help them with their use case.

Of course, the best solution to the Ref problem would be for people to stop trying to babysit julia users, export the damn Base.RefValue type and let people use it as they see fit. But that is unlikely to happen.

I like the name ZerodimentionalArrays. It communicates the intent clearly. The names based on Ref assumes a lot of knowledge of implementation details of Julia.

4 Likes

From the docs: “Any zero-dimensional array is an iterator containing exactly one element (this follows from the zero-dimensional shape).”

Call it nitpicky, but what is the difference between 1-d and 2-d arrays? Number of dimensions, not number of elements.

a = [1]

This array has exactly one element, but it is not zero-dimensional.

I find this terminology confusing.

2 Likes

My suggestion for this would be to have a separate blog post: the Readme should describe what’s in the package and how to use it, and the blog post should describe why it’s necessary and when to use it. (“blog post” can be a locked post here on Discourse, or even just a why.md file in the repo, whatever is most convenient).

In the Diataxis framework, the Readme would cover the right hand side of the categorization, and the blog post the left hand side of it. If this is intended to be a package that’s quick to pickup, easy to use, and commonly used (and it seems to be intended so), such a post would also give you a lot more room to make it accessible to every Julia user, taking it closer to that intended goal.

2 Likes

Just to chime in with a different perspective: I find this new package great. It fills an important API hole with a small, self-contained implementation with no dependencies.

What API hole is that? Broadly speaking, references. Julia’s references have poor semantics and seems ad-hoc designed. It’s annoying that Ref is an abstract type, yet does not behave like other abstract types: Clearly the API revolves around using the Ref constructor directly, and then having the returned type be implementation details. Which means any type which contains a Ref is now either abstractly typed, or parameterized with a private type. Not to mention the mistake of having Ptr <: Ref.

On top of that, having the trio of immutable, mutable + const, and mutable wrapper structs, covers all the types of references to Julia objects I can think of.

Ideally, this stuff is so basic it should not go in a standalone package. Base’s references should be cleaned up, but I’m skeptical that it’s doable in a non-breaking manner. In any case, whether or not this eventually ends up in Base, having it in a package is a good start.

8 Likes