-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
design of array constructors #24595
Comments
One point concerning |
I much look forward to your thoughts on |
Convenience constructors:
|
Convenience constructors:
|
dc = d + im*convert(Vector{elty}, ones(n)) |
(1:size(A,1)).*ones(Int,size(A,2))'
instead of repmat(1:size(A,1), 1, size(A,2))
(ad hoc repmat
/repeat
). Line 1857 in 8085047
iall = (1:size(A,1)).*ones(Int,size(A,2))' |
isequal(Array(sparse(complex.(ones(5,5), ones(5,5)))), complex.(ones(5,5), ones(5,5)))
where introducing a name is better as e.g. in A = fill(1.0+im, 5, 5); isequal(Array(sparse(A)), A)
. Line 27 in 8085047
@test isequal(Array(sparse(complex.(ones(5,5), ones(5,5)))), complex.(ones(5,5), ones(5,5))) |
ones(2,3) * ones(2,3)'
instead of fill(3., 2, 2)
(perhaps an "unusual ad hoc fill
"). Line 1204 in 8085047
@test isequal(ones(2,3) * ones(2,3)', [3. 3.; 3. 3.]) |
Questionable use of zeros
: While zeros
certainly has legitimate uses, as @StefanKarpinski
argues in Deprecate ones
? #24444 (comment), due to its mindshare and brevity zeros
sees use where something else would serve better.
- (An observation related to 3, 5, and 6 from the analysis in Deprecate
ones
? #24444 (comment): Of the ~30% ofones
calls in the wild that are reasonably semanticallyones
, an appreciable fraction of such calls appear in concatenations, where some lazy equivalent would be better. A similar statement holds forzeros
.)
- Non-orthogonality / overlap with ...
fill
Subsuming ones
and zeros
, and obviating the antipatterns and ad hoc methods described in shortcoming 3, fill
is the missing abstraction mentioned explicitly in shortcomings 1-2 and implicitly in shortcoming 3. Moreover, fill
possesses some distinct advantages over ones
and zeros
:
-
By nature of requiring a value,
fill
avoids the element type ambiguities ofones
/zeros
(shortcomings 1-2). -
By nature of accommodating any value,
fill
avoids the "handling values without an associated function" quandary and the consequent antipatterns and ad hoc method proliferation (shortcoming 3). -
fill
generalizes to other array types better thanones
/zeros
: As described in the OP, generalizations ofones
/zeros
exacerbate those functions' element type ambiguities and introduce additional default element type ambiguities. Generalizations offill
avoid those ambiguities. Moreover,fill
dovetails nicely with the OP's second proposal for addressing the broader array construction design issue. Consequently,fill
may produce less generalization pressure thanones
/zeros
. -
For constructing
Array
s filled with a value other than some zero or one,fill
is (always?) more efficient and (usually? almost always?) more compact than equivalents usingones
/zeros
. For example, considerfill(1im, shape...)
versusim*ones(Int, shape...)
orfill(1f0/ℯ, shape...)
versusones(Float32, shape...)/ℯ
.
fill
's primary perceived downside is length. Though for constructing Array
s filled with Float64(0)
or Float64(1)
fill
is marginally longer than ones
/zeros
(e.g. fill(1., shape...)
versus ones(shape...)
), for other types admitting literals fill
is usually shorter (e.g. fill(1f0, shape...)
versus ones(Float32, shape...)
). For types not admitting literals, fill
and zeros
/ones
are comparable in length (e.g. fill(Float16(0), shape...)
versus zeros(Float16, shape...)
), with a slight edge to zeros
/ones
. But the edge of course goes to fill
for values other than a zero or one.
At present ones
/zeros
seem to box fill
out of mindshare in the wild.
Resolution Options
-
Better specify
ones
/zeros
. See challenges/pitfalls described above in shortcomings 1-2. Fails to address any of shortcomings 3-7. -
Move
ones
/zeros
to MATLAB compat. Addresses shortcomings 1-4 and 7. Addresses shortcomings 5-6 insofar as those shortcomings do not or only partially shift tofill
. -
Deprecate
ones
/zeros
methods other thanones(shape...)
andzeros(shape...)
. Reasoning: ForFloat64
, the additive generator and multiplicative identity coincide, and the additive identity and multiplicative zero also coincide. So these methods do not suffer from shortcomings 1-2. Though shortcomings 3-7 remain, by reducingones
/zeros
's scope: (a) awareness and use offill
may improve, perhaps mitigating shortcoming 3; (b)ones
/zeros
generalization pressure may decrease, perhaps mitigating shortcoming 4; and (c)ones
/zeros
may become less attractive for questionable use, perhaps mitigating shortcomings 5-6.
What about methods of ones
/zeros
accepting an array (instance, not type) first argument?
Triage (uniformly?) found these methods questionable. And the analysis in #24444 (comment) and similar analysis of base suggests these methods rarely appear in the wild. So independent of what happens with the more common ones
/zeros
methods, triage favors deprecating these methods.
Thanks for reading! :)
First, thank you very much @Sacha0 for writing up such complete and well considered notes here - extremely useful, as it is warranted by the complexity. Overall, I agree with the thrust and the razors and so-on (very handy set-up in the OP). I have a couple of orthogonal thoughts so I'll split them up into separate posts: First, regarding convenience functions like
|
But this would create an array containing (aliases of) only one |
It seems to me that some of the issues facing array constructors are also shared by I'll start with the signature I would also argue that a generic interface for array constructors should be accepting their |
I'm not sure if this is the best forum for brainstorming, but to me when a function argument is ambiguous then I would consider giving it a keyword argument (which hopefully will be type stable soon!). Assuming we want to deal with offset arrays and |
Much thanks for reading and contributing your thoughts! :) Regarding generalization of
|
In #16029 (comment), @nalimilan proposed to capture the cases where There is a similar subproposal above for e.g.
which can return a The introduction of |
Please note that |
They go together, like |
A recent discussion on discourse shows that many in the user community want a convenience constructor for the uninitialized case. This is qualitatively different from (uninitialized)(T::DataType,dims...)=Array{T}(uninitialized,dims) |
I would also accept renaming it to |
c.f. |
But |
I was just thinking it might be nice to be able to use something like |
Bringing up #25107 since it seems a lot of this proposal mentions shape, when shape isn't enough information to fully characterize the structure of many abstract arrays. |
I often find myself wanting to generate random julia> S = rand(4,4); S = S'S + I It would be nice to abstract this away by adding constructors for generating random structured matrix types (like |
Intro
This post is an attempt to consolidate/review/analyze the several ongoing, disjoint conversations on array construction. The common heart of these discussions is that the existing patchwork of array construction methods leaves something to be desired. A more coherent model that is pleasant to use, adequately general and flexible, largely consistent both within and across array types, and consists of orthogonal, composable parts would be fantastic.
Some particular design objectives / razors might include:
Given an unfamiliar array type, you should have a reasonable sense of how to construct an instance with desired contents without manual/method/code sleuthing.
Reading an incantation that constructs an array of unfamiliar type, you should be able to largely deduce the array's type and contents without manual/method/code sleuthing.
The general tools for array construction should be discoverable, and writing *common operations via those general tools should be sufficiently pleasant and concise that: (1) pressure to write ad hoc convenience methods does not escalate to the point where such methods proliferate; and (2) antipatterns for array construction do not emerge to avoid (or in ignorance of) the general tools.
(*Common operations include constructing: (1) an array uniformly initialized from a value; (2) an array filled from an iterable, or from a similar object defining the array's contents such as
I
; (3) one array from another; and (4) an uninitialized array.)Via a tour of the relevant issues and with the above in mind, let's explore the design space.
Tour of the issues
Let's start with...
Vector
:Vector(x)
should be able to construct aVector
from an arbitraryHasLength
iterablex
(as with e.g.Vector(1:4)
, which intuitively yields[1, 2, 3, 4]
). But this cannot work for tuples now, as e.g.Vector{Float64}((2,))
instead constructs an uninitializedVector{Float64}
of length two.The prevailing idea for fixing the preceding issue is to: (1) deprecate uninitialized-array constructors that accept (solely) a tuple or series of integer arguments as shape, removing the method signature collision; and (2) replace those uninitialized-array constructors with something else. Two broad replacement proposals exist:
blah(T, shape...)
for typeT
and tuple or series of integersshape
, that returns an uninitializedArray
with element typeT
and shapeshape
. This approach is an extension of the existing collection ofArray
convenience constructors inherited from other languages includingones
,zeros
,eye
,rand
, andrandn
.(* Please note that
blah
is merely a short placeholder for whatever name comes out of the relevant ongoing bikeshed. The eventual name is not important here :).)Array{T}(blah, shape...)
constructors whereblah
signals that the caller does not care what the return's contents are. These constructors would be specific instances of a more general model that extends and unifies the existing constructor model. That more general model is discussed further below.The first proposal
The first proposal leads us to...
ones
,zeros
,eye
,rand
,randn
, and the proposedblah(T, shape...)
all produceArray
s. How do we generalize these functions to array types broadly? Two approaches exist:The de facto approach is introduction of ad hoc perturbations on these function names for each new array type: Devise an obscure prefix associated with your array type, and introduce
*ones
,*zeros
,*eye
,*rand
,*randn
, and hypothetically*blah
functions with*
your prefix. This approach fails all three razors above: Failing the first razor, to construct an instance of an array type that follows this approach, you have to discover that the array type takes this approach, figure out the associated prefix, and then hope the methods you find do what you expect. Failing the second razor, when you encounter the unfamiliarbones
function in code, you might guess that function either carries out spooky divination rituals, or constructs ab
full ofone
s (whateverb
refers to). Along similar lines, doesspones
populate all entries in a sparse matrix with ones, or only some set of stored/nonzero entries (and if so which)? Failing the third razor, the very nature of this approach is proliferation of ad hoc convenience functions and is itself an antipattern. On the other hand, this approach's upside is that it sometimes involves a bit less typing (though often also not, see below). Nonetheless, this approach is fraught.So what's the other approach? #11557 started off by discussing that other approach:
ones
,zeros
,eye
,rand
, andrandn
typically accept a result element type as either first or second argument, for exampleones(Int, (3, 3))
andrand(MersenneTwister(), Int, (3, 3))
. That argument could instead be an array type, for exampleones(MyArray{Int}, (3, 3))
andrand(MersenneTwister(), MyArray{Int}, (3, 3))
. This approach is enormously better than the last: It could mostly pass the first and second razors above. But it nonetheless fails the third razor, and exhibits other shortcomings (mostly inherited from the existing convenience constructors). Let's look at some of those shortcomings:Default element type ambiguity: When element type isn't specified, for example as in
eye(MyArray, (3, 3))
orones(MyArray, (3, 3))
, what should the returned array's element type be? Should that default element type be consistent across array types, or allowed to vary? At present these functions yieldFloat64
by default, which is a reasonable (useful) choice when running on modern CPUs. But other defaults may be more appropriate for array types associated with other hardware or applications, for exampleFloat16
orFloat32
for array types / contexts associated with GPUs. And one could also argue thatInt
is a more canonical type independent of context, or thatBool
usually provides better promotion behavior, and so on. (This shortcoming to some degree violates the second razor.)ones
(and, to lesser degree,eye
) element type ambiguity: As Deprecateones
? #24444 highlights, whetherones(MyArray{T}, shape...)
returns element typeT
's multiplicative identity (one(T)
) or additive generator (oneunit(T)
) is ambiguous. Of course one or the other can be chosen and documented. But choosingone
,ones(MyArray{T}, shape...)
can no longer consistently return aMyArray{T}
, as for some typestypeof(one(T))
does not coincide withT
(e.g.one(1meter) == 1 != 1meter
). And as demonstrated in Deprecateones
? #24444, with either choice some subset of users's expectations will be violated and use cases unsatisfied, creating pressure for ad hoc solutions or additional value-names.eye(MyArray{T}, shape...)
's element type should less ambiguously beone(T)
, which mitigates the latter issue but runs into the former. (This shortcoming to some degree violates both the first and second razors.)zeros
element type ambiguity: Prior to Disambiguate the meaning ofone
#16116, whetherone(T)
returned a multiplicative identity or additive generator forT
was ambiguous. WIP: add oneunit(x) for dimensionful version of one(x) #20268 resolved this ambiguity by introducingoneunit(T)
as the additive generator forT
and affirmingone(T)
as a multiplicative identity.zero
suffers from a similar issue, though likely less important in practice: Iszero(T)
the additive identity or a sort of multiplicative zero forT
? To illustrate, is3meters * zero(1meters)
0meters^2
or0meters
? Consequently,zeros
suffers from an ambiguity analogous to that described above forones
.Handling values without an associated function: To construct a
MyArray
of1
s, you callones(MyArray{Int}, (3, 3))
. To construct aMyArray
of0
s, you callzeros(MyArray{Int}, (3, 3))
. To construct aMyArray
containing the identity matrix, you calleye(MyArray{Int}, (3, 3))
. Great so far. But how do you construct aMyArray
of2
s, or-1
s, or containingI/2
? If you are used to these convenience constructors, perhaps you respectively call2*ones(MyArray{Int}, (3, 3))
,-ones(MyArray{Int}, (3, 3))
, andeye(MyArray{Int}, (3, 3))/2
. Or in the first two cases perhaps you callfill!(blah(MyArray{Int}, (3, 3)), [2|-1])
for mutable andfill!
-supportingMyArray
, limiting your code's scope. If you want to avoid generating a temporary, you probably use thefill!
incantation. But these incantations are less pleasant thanones
orzeros
, so perhaps you give your common values names:twos(MyArray{Int}, (3, 3))
. And to avoid the temporary in theeye
call, perhaps you roll ahalfeye(MyArray{Int}, (3, 3))
function to avoid allocating the temporary. Overall, antipatterns emerge and ad hoc functions proliferate. And as demonstrated in Deprecateones
? #24444 (comment) and discussed elsewhere, this issue bears out in practice and is widespread. (This shortcoming violates the third razor.)Two disjoint, incongruous, and overlapping models are necessary: To construct an array from another array, or from an iterable or similar content specifier, you have to switch from these functions to constructors. So users must be familiar with two disjoint, incongruous, and non-orthogonal models.
Minor type argument position inconsistency: The position of these functions' type argument varies, requiring method sleuthing to figure out the correct signature. Examples:
ones(MyArray{Int}, (3, 3))
versusrand(RNG, MyArray{Int}, (3, 3))
.Each of these shortcomings is perhaps acceptable considered in isolation. But considering these shortcomings simultaneously, this approach becomes a shaky foundation on which to build a significant component of the language.
In part motivated by these and other considerations, #11557 and concurrent discussion turned to...
The second proposal
... which is to introduce (modulo spelling of
blah
, please see above)Array{T}(blah, shape...)
constructors, whereblah
indicates the caller does not care what the return's contents are. These constructors immediately generalize to arbitrary array types as inMyArray{T}(blah, shape_etc...)
, and would be a specific instance of a more general model that extends the existing constructor model:The existing constructor model allows you to write, for example,
Vector(x)
forx
any of1:4
,Base.OneTo(4)
, or[1, 2, 3, 4]
(to construct theVector{Int}
[1, 2, 3, 4]
), or similarlySparseVector(x)
(to build the equivalentSparseVector
). To the limited degree this presently works broadly, the model isMyArray[{...}](contentspec)
wherecontentspec
, for example some other array, iterable, or similar object, defines the resulting array's contents.The more general extension of this model is
MyArray[{...}](contentspec[, modifierspec...])
. Roughly,contentspec
defines the result's contents, whilemodifierspec...
(if given) provides qualifications, e.g. shape.What does this look like in practice?
For the most part you would use constructors as you do now, with few exceptions. Let's go through the common construction operations mentioned above:
(Constructing uninitialized arrays.) To build an uninitialized
MyArray{T}
, where now you write e.g.MyArray{T}(shape...)
, instead you would writeMyArray{T}(blah, shape...)
. ([WIP] add some junk #24400 explored this possibity forArray
s, and inevitably became a bikeshed of the spelling ofblah
:).)(Constructing one array from another.) Constructing one array from another, as in e.g.
Vector(x)
orSparseVector(x)
forx
being[1, 2, 3, 4]
, would work just as before.(Constructing an array filled from an iterable, or from a similar object defining the array's contents such as
I
.) What is possible now, for exampleVector(x)
forx
either1:4
orBase.One(4)
, would work as before. But where e.g.Array[{T,N}](tuple)
now fails or produces an uninitialized array depending onT
,N
, andtuple
, such signatures could work as for any other iterable. And additional possibilities become natural: ConstructingArray
s fromHasShape
generators is one nice example. Another, already on master (constructors for Matrix and SparseMatrixCSC from UniformScaling #24372), isMatrix[{T}](I, m, n)
(alternativelyMatrix[{T}](I, (m, n))
), which constructs aMatrix[{T}]
of shape(m, n)
containing the identity, and is equivalent toeye([T, ]m[, n])
with fewer ambiguities.Great so far. Now what about perhaps the most common operation, i.e. constructing an array uniformly initialized from a value? Under the general model above, this operation should of course roughly be
MyArray[{T}](it, shape...)
whereit
is an iterable repeating the desired value. But this incantation should: (a) be fairly short and pleasant to type, lest ad hoc constructors for particular array types and values proliferate to avoid using the general model; and ideally (b) mesh naturally with convenience constructors forArray
s.Triage came up with two broad spelling possibilities. The first spelling possibility led to...
ones
andzeros
iterable, allowing e.g.MyArray([ones|zeros], shape...)
. At first blush this spelling seems reasonable: It's fairly short/pleasant, satisfying (a). And it ties to theones
/zeros
convenience constructors, somewhat satisfying (b) (caveat being the slightly unnatural reversed identifier ordering as in e.g.ones(T, shape...)
vsMyArr{T}(ones, shape...)
). But further consideration reveals that this spelling foists most shortcomings of the first design proposal (that is, the e.g.ones(Int, ...)
->ones(MyArray{Int}, ...)
proposal described above) onto this second design proposal. Specifically, the "Default element type ambiguity", "ones
/eye
/zeros
element type ambiguity", and "handling values without an associated function" shortcomings described above all apply here as well. Sad razors.The second spelling possibility is
MyArray(Rep(v), shape...)
modulo spelling ofRep(v)
, whereRep(v)
is some convenient alias forIterators.Repeated(v)
withv
any desired value. (Another possible spelling ofRep(v)
discussed in triage isFill(v)
, which dovetails beautifully with thefill
convenience constructor for the same purpose specific toArray
s. Independent of the iterator's name, this spelling is a clean generalization offill
fromArray
s to arrays generally.) In practice this would look likeMyArray(Rep(1), shape...)
(instead ofMyArray{Int}(ones, shape...)
). This spelling possesses some distinct advantages:By nature of requiring a value, this spelling suffers from neither the "default element type ambiguity" nor the "
ones
/eye
/zeros
elementy type ambiguity" described above.By nature of accommodating any value, this spelling avoids the "handling values without an associated function" issue and the consequent antipatterns and ad hoc method proliferation.
By nature of requiring and accepting a value, this spelling is frequently more compact and efficient than equivalents with the other spelling: Consider
MyArray(Rep(1.0im), shape...)
versusim*MyArray{Complex{Float64}}(ones, shape...)
, orMyArray(Rep(1f0/ℯ), shape...)
versusMyArray{Float32}(ones, shape...)/ℯ
.This spelling is a composition of well-defined, fundamental tools that, once learned, can be deployed to good effect elsewhere. In contrast, the other spelling is ad hoc and a bit of a pun.
Great. With this latter spelling, overall this second proposal appears to satisfy both the broad design objectives and three razors at the top, and avoids the shortcomings of the first proposal.
What else? Convenience constructors
Convenience constructor are an important part of this discussion and about which there is much to consider. But that topic I will leave for another post. Thanks for reading! :)
The text was updated successfully, but these errors were encountered: