Open Sacha0 opened 7 years ago
One point concerning rand
to take into account is the "value specification", which is often encoded with a value (e.g. 1:10
) instead of a type. FWIW, independantly of your effort and other's to solve this problem, I have been thinking on how to solve the more narrow rand
's problem (and get rid of sprand
!)... which is at the same time not-so-simple on its own as we (at least I) want allow also generating Set
s or Dict
s (which require specifying the value specification for a Pair
...). I didn't plan to speak about it before PR'ing it, but thought I should mention it here.
I much look forward to your thoughts on rand
, @rfourquet! :)
eye
A brief review of triage's discussions of eye
.
A Matrix{T}
is usually a poor representation of the identity operator/matrix: eye(T, m, n)
requires O(mn)
storage (where in principle constant storage should do). eye(T, m, n) + ...
usually involves O(mn)
operations (where in principle O(min(m, n))
operations should do). eye(T, m, n) * ...
usually involves O(mnk)
operations (where in principle either constant or O(nk)
operations should do). Base includes better representations of the identity operator/matrix for a variety of purposes, including UniformScaling
s, Diagonal
s, and SparseMatrixCSC
s; these representations should be preferred, but eye
often boxes these representations out in practice.
Where a Matrix{T}
is a reasonable representation of the identity operator/matrix, eye
provides no way to efficiently construct a scaled identity operator/matrix: One must write, e.g., 2*eye(T, m, n)
, which involves a temporary and O(mn)
unnecessary operations. (Promotion in such expressions can also require a bit of care.)
Whether eye(T, m, n)
's element type should be T
's multiplicative identity one(T)
or additive generator oneunit(T)
depends on context: Interpreting eye(T, m, n)
as a concrete representation of the identity operator/matrix for use in multiplication, T
's multiplicative identity one(T)
seems the right choice. But where eye(T, m, n)
appears (frequently) in addition, T
's additive generator might instead be the right choice. Standardizing on one or the other is of course possible, but: (1) each choice fails to satisfy some use cases; and (2) choosing one(T)
, eye(T, m, n)
's result element type cannot be T
where T
's multiplicative identity one(T)
is not of type T
.
As in the OP, eye
does not generalize well to non-Array
array types.
eye
is a terrible pun.
Following the second proposal in the OP, introduce array constructors that accept a UniformScaling
first argument and shape-specifying trailing arguments, for example (Array|Matrix)[{T}](S::UniformScaling, shape...)
for Array
s. Then Matrix(2I, m, n)
, for example, serves as a direct replacement for 2*eye(Int, m, n)
. Such constructors avoid shortcomings 2-5.
Deprecate eye
in favor of UniformScaling
s and UniformScaling
-accepting constructors (hopefully addressing shortcoming 1 by encouraging better representation choices / making the usually poor Matrix{T}
choice less attractive).
Best!
ones
, zeros
, and fill
ones
, zeros
, and fill
each construct Array
s filled with some value. This post consolidates/reviews/analyzes discussion of this group of convenience constructors from github, slack, and triage.
ones
and zeros
ones
element type ambiguity: As JuliaLang/LinearAlgebra.jl#484 highlights, whether ones(T, shape...)
returns an Array
filled with T
's multiplicative identity (one(T)
) or additive generator (oneunit(T)
) is ambiguous. Standardizing on one or the other is of course possible, but: (1) as JuliaLang/LinearAlgebra.jl#484 demonstrates, each choice violates some subset of users' expectations and fails to satisfy some use cases; and (2) choosing one(T)
, one(T, shape...)
's element type cannot be T
where T
's multiplicative identity one(T)
is not of type T
. Introducing a oneunits(T, shape...)
function (that constructs Array
s of oneunit(T)
s) and standardizing ones(T, shape...)
to construct Array
s of one(T)
s addresses point (1) but not (2), and the proliferation of (value-)names strongly indicates a missing abstraction.
zeros
element type ambiguity: Prior to JuliaLang/julia#16116, whether one(T)
returned a multiplicative identity or additive generator for T
was ambiguous. JuliaLang/julia#20268 resolved this ambiguity by introducing oneunit(T)
as the additive generator for T
and affirming one(T)
as a multiplicative identity. zero
suffers from a similar issue, though likely less important in practice: Is zero(T)
the additive identity or a sort of multiplicative zero for T
? To illustrate, is 3meters * zero(1meters)
0meters^2
or 0meters
? Consequently, even with disambiguation of zero
, zeros
suffers from an ambiguity analogous to that described above for ones
: Does zeros(T, shape...)
return an Array
filled with T
's additive identity or instead some sort of multiplicative zero? As with ones
, standardizing on one or the other is of course possible, but: (1) each choice will violate some subset of users' expectations and fail to satisfy some use cases; and (2) choosing a multiplicative zero for T
, zeros(T, shape...)
's element type cannot be T
where that multiplicative zero for T
is not of type T
. Bifurcating zeros
into two functions, one for each choice, addresses point (1) but not (2), and the proliferation of (value-)names strongly indicates a missing abstraction.
Handling values without an associated function: How do you construct an Array
filled with 2
s, or 1.0 + im
s, or in general any value other than a zero or one? If you are used to ones
/zeros
, perhaps you respectively call 2ones(Int, (m, n))
and complex.(ones(m, n), ones(m, n))
. (These examples are taken from base, as noted in https://github.com/JuliaLang/LinearAlgebra.jl/issues/484.) Or to avoid the temporary, perhaps you respectively call fill!([ones|zeros](Int, (m, n)), 2)
and fill!([ones|zeros](Complex{Float64}, (m, n)), 1.0 + im)
. But such incantations are less pleasant than ones
and zeros
, so perhaps you give your common values names: twos(Int, (m, n))
etc. Overall, antipatterns emerge and ad hoc functions proliferate. As demonstrated in https://github.com/JuliaLang/LinearAlgebra.jl/issues/484 and discussed elsewhere, this issue bears out in practice and is widespread: more than half of ones
calls in the wild are ad hoc fill
s.
Simultaneous generalization pressure and poor generalizability: As the OP lays out, ones
/zeros
do not generalize well to other array types. But ones
/zeros
's considerable mindshare from Array
s creates pressure for generalization to other array types, in practice yielding the poor de facto generalization described at length in the OP (bones
/bzeros
, dones
/dzeros
, spones
/spzeros
, etc).
Other questionable use of ones
: Likely by nature of its considerable mindshare and being the shortest Array
constructor, ones
sees use where it shouldn't (beyond simple ad hoc fill
s). A few examples from base:
dc = d + im*convert(Vector{elty}, ones(n))
instead of e.g. dc = d .+ elty(1)im
or dc = d .+ Complex{elty}(im)
(ad hoc convert
and broadcast
). https://github.com/JuliaLang/julia/blob/8085047696e2bde1d36158d243a23451285f6dbc/test/linalg/diagonal.jl#L254
(1:size(A,1)).*ones(Int,size(A,2))'
instead of repmat(1:size(A,1), 1, size(A,2))
(ad hoc repmat
/repeat
). https://github.com/JuliaLang/julia/blob/8085047696e2bde1d36158d243a23451285f6dbc/test/arrayops.jl#L1857
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)
. https://github.com/JuliaLang/julia/blob/8085047696e2bde1d36158d243a23451285f6dbc/test/sparse/sparse.jl#L27
ones(2,3) * ones(2,3)'
instead of fill(3., 2, 2)
(perhaps an "unusual ad hoc fill
"). https://github.com/JuliaLang/julia/blob/8085047696e2bde1d36158d243a23451285f6dbc/test/arrayops.jl#L1204
Questionable use of zeros
: While zeros
certainly has legitimate uses, as @StefanKarpinski
argues in https://github.com/JuliaLang/LinearAlgebra.jl/issues/484, due to its mindshare and brevity zeros
sees use where something else would serve better.
ones
calls in the wild that are reasonably semantically ones
, an appreciable fraction of such calls appear in concatenations, where some lazy equivalent would be better. A similar statement holds for zeros
.)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 of ones
/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 than ones
/zeros
: As described in the OP, generalizations of ones
/zeros
exacerbate those functions' element type ambiguities and introduce additional default element type ambiguities. Generalizations of fill
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 than ones
/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 using ones
/zeros
. For example, consider fill(1im, shape...)
versus im*ones(Int, shape...)
or fill(1f0/ℯ, shape...)
versus ones(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.
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 to fill
.
Deprecate ones
/zeros
methods other than ones(shape...)
and zeros(shape...)
. Reasoning: For Float64
, 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 reducing ones
/zeros
's scope: (a) awareness and use of fill
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.
ones
/zeros
accepting an array (instance, not type) first argument?Triage (uniformly?) found these methods questionable. And the analysis in https://github.com/JuliaLang/LinearAlgebra.jl/issues/484 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 ones
and zeros
and rand
and fill
:
I see the ambiguity of additive one/zero and multiplicative one/zero as a distraction here. one
and zero
were designed for fields - types which are monads under +
and *
and have additive and multiplicate identities. For things which aren't monads under *
we need oneunit
(and potentially zerounit
or zeromult
or something). To me it is clear that we would want to have oneunits
(and zerounits
or whatever) if ones
and zeros
are to continue existing (it just seems completely inconsistent otherwise).
How come we never had rands
and randns
and randexps
, to be consistent with zero
vs zeros
, etc?
Random thought (no pun intended): Probably already thought of but what about rand(1:10, size...)
-> where fill(Rand(1:10), size...)
Rand(1:10)
constructs an infinite iterable of random numbers drawn from 1:10
. Similarly for Randn
etc. EDIT: Sorry, this applies to constructors rand(1:10, size...)
-> Array(Rand(1:10), size...)
not fill
.
Sometimes I'd like to be able to fill
an associative with a value, like initializing a Dict
with given keys and initial values of 0
or Vector{T}()
or whatever. In general it would be great if the decisions made here are compatible with Associative
(I'll talk about that more below).
what about rand(1:10, size...) -> fill(Rand(1:10), size...)
But this would create an array containing (aliases of) only one Rand(1:10)
object, unless fill
handles Rand
specially, which I would dislike. I have often wanted an equivalent of C++'s std::generate
, which fills a container by invoking a function, and started implementing a similar functionality in julia, until I realized that a mutating version of that can be achieved by broadcast
(a=zeros(10); a .= rand.()
), and a non-mutating version via Generator
or list comprehensions: [rand(1:10) for x=..., y=...]
. Unfortunately the latter is less performant (than rand(1:10, size...)
), because it doesn't factor out a costly computation which happens within rand(1:10)
, so having an Iterators.randstream([rgn], [dims])
would make sense. But it's not immediately trivial to supress the rand(1:10, size...)
API while retaining performance.
It seems to me that some of the issues facing array constructors are also shared by similar
, which I have been thinking about an awful lot lately. They share an ambiguity where the size might be confused for the keys or values of the output container.
I'll start with the signature similar(array, T, size...)
. It seems to me that size...
is actually a placeholder for the indices
and keys
of the output array. For an offset vector, you would probably be best to specify the indices
rather than just the size - such as similar(array, T, 0:(n-1))
for a zero-based vector of n
elements. Thus I would assert that similar(array, T, 3)
is just shorthand for similar(array, T, Base.OneTo(3))
. For multidimensional arrays perhaps the third argument is just the keys
of the array and similar(array, T, indices...)
is shorthand for similar(array, T, cartesian_product(indices...))
where cartesian_product
returns an AbstractCartesianRange
of the indices...
. (Going even further than this, I had been suggesting a version of similar
for dictionaries such as similar(olddict, ValueType, new_keys)
that returns a container with junk values and given keys).
I would also argue that a generic interface for array constructors should be accepting their keys
- with indices...
and size...
simply as shorthand. (Similarly for fill
and zeros
and rand
...) The advantages are that the expected constructor pattern will follow through to offset arrays (and potentially even to associatives). In fact, I'd probably make it one of your razors that the design should naturally work for offset arrays.
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 indices
and keys
here somewhere, compare the signatures in this gist.
Much thanks for reading and contributing your thoughts! :)
rand
/randn
/randexp
The working plan is to follow the OP's second proposal, i.e. MyArray(randstream, otherspecs...)
for randstream
any random stream. (The above independent convergence on this idea is happily confidence inspiring :).)
Usually being clearer, more explicit, and less ambiguous, keyword arguments are fantastic. I would be delighted to see the OP's second proposal support keyword argument signatures, particularly where positional arguments do not scale well or are ambiguous.
In those simple cases where positional arguments are clear / unambiguous, positional arguments' brevity makes them pleasant to work with (particularly when, for example, hacking at the REPL). The OP's third razor strongly suggests their inclusion in those cases.
Wonderfully, positional and keyword arguments need not be in tension! We can have the best of both worlds by supporting one, the other, or both as appropriate and to best effect. And fortunately we can expand the set of supported signatures at any time, for example in the 1.x series. So working through the details of when/where to support positional and/or keyword arguments is not pressing.
Concerning what precisely the trailing arguments to array constructors should be (that is, what otherspecs...
in the OP's second proposal's MyArray[{...}](contentspec[, otherspecs...])
model should be), I intentionally left otherspecs...
somewhat ambiguous and thus flexible. Working out what otherspecs...
might be in general will require substantial time and experimentation I wager. But fortunately, for reasons analogous to those given at the end of the preceding subsection, working out the details on this front is not pressing.
similar
, allocate
, storage traits, and related topicsOverhauling similar
, introducing allocate
, exploring storage traits, and related proposals are important topics warranting extensive consideration and discussion. Such proposals have a long and broad design and implementation horizon. And as much as I look forward to that discussion and its potential fruits, I would like to keep this thread tightly focused on array constructors in the narrow sense, and particularly on associated near-term considerations. Discussion of these other topics perhaps would be best consolidated in JuliaLang/julia#18161.
With time before 1.0 being scarce, I advocate we keep this thread tightly focused on what must happen by 1.0 to improve / enable future improvement of the design of array constructors in a narrow sense. So far, the relevant consensus action items seem to be:
Array[{T}](blah, shape...)
constructors.Array[{T}](shape...)
constructors in favor of those Array[{T}](blah, shape...)
constructors.eye
in favor of UniformScaling
and UniformScaling
-accepting array constructors.{ones|zeros}(A::AbstractArray, ...)
methods.Edit: More tasks:
BitArray
to uninitialized
-accepting constructors.RowVector
to uninitialized
-accepting constructors.GenericArray
to uninitialized
-accepting constructors.OffsetArray
to uninitialized
-accepting constructors (base's implementation).Other consensus items? Thanks and best!
Ref https://github.com/JuliaArrays/FillArrays.jl which was created very recently to allow for constructors like
BandedMatrix(Zeros(5,5), 1,1)
Cheers, that approach is quite similar to the OP's second proposal :). For the reasons given in that proposal, expressing e.g. BandedMatrix(Zeros(5,5), 1,1)
as e.g. BandedMatrix(Rep(0), 1,1)
or BandedMatrix(Fill(0), 1,1)
(for Rep
/Fill
proposed shorthands for Iterators.Repeated
) would be advantageous. Looks like now is the time to select and introduce one or the other shorthand? :) Best!
For completeness, FillArrays supports both Fill
and Zeros
:
Matrix(Fill(0.0, 5, 5))
Matrix(Zeros(5, 5))
The reason there are two types is Zeros
is special: we know we can always convert a Zeros
matrix to a sparse matrix without destroying sparsity, where Fill
would default to a dense matrix.
It is important that this happens at compile time not run time: for example, imagine BandedMatrix
on a GPU: one would not want to have to add a check F.x == 0
as that would need to be run on the CPU, where in theory GPUBandedMatrix(Zeros(5,5),3,3))
could be done completely on the GPU as determined at compile time.
PS: I think it would have to be BandedMatrix(Rep(0), (5,5), (1,1))
to indicate the size of the matrix.
Cheers, with those expansions I think I follow now: The latter arguments in BandedMatrix(Zeros(5,5), 1,1)
and BandedMatrix(Rep(0), (5,5), (1,1))
specify bandwidth rather than shape? If so, this approach fits well with the OP's second proposal (the Zeros(5,5)
/Ones(5,5)
being lazy, HasShape
representations of the intended contents, and the trailing (1,1)
argument being additional specs). Though to avoid eltype ambiguities, requiring eltype specification as in Zeros{T}(5,5)
/Ones{T}(5,5)
seems necessary? Additionally, constant propagation might be able to handle the compile- versus run-time consideration with Rep
, and if not then some explicitly compile-time equivalent of Rep
(think along the lines of Val
) would do the trick. Best!
The latter arguments in BandedMatrix(Zeros(5,5), 1,1) and BandedMatrix(Rep(0), (5,5), (1,1)) specify bandwidth rather than shape.
Yes.
Though to avoid eltype ambiguities, requiring eltype specification as in
Zeros{T}(5,5)/Ones{T}(5,5)
seems necessary?
I followed zeros
and default to Zeros{Float64}(5,5)
. Though this is debatable whether it is ideal behaviour.
Additionally, constant propagation might be able to handle the compile- versus run-time consideration with Rep.
That sounds fine, though it can't be like Val{x}
for non-constant x
as Fill(x, n, m)
should not recompile for different values of x
.
That sounds fine, though it can't be like Val{x} for non-constant x as Fill(x, n, m) should not recompile for different values of x.
Could you expand a bit? I was thinking along the lines of BandedMatrix(ExplicitCompileTimeRep(0), shape, bandwidths)
, with ExplicitCompileTimeRep(0)
lifting 0
to the type domain such that you can write, e.g., a BandedMatrix(::ExplicitCompleTimeRep{0}, shape, bandwidths) = ...
method (as I imagine you would similarly handle the Zeros{Float64}(0, 0)
approach). Best!
Sure, that's fine if there's both Rep
and ExplicitCompileTimeRep
. But you could already do that with a ExplicitCompileTimeRep{0}
syntax.
There is still a role for FillArrays, which have dimensions and are AbstractArray
.
A reason to have dimensioned Fill(x,n)
(in addition to dimensionless Rep(x)
) is to support allocation free concatenation:
[1; Zeros(100)]
This should work for free already since Fill <: AbstractArray
.
A reason to have dimensioned Fill(x,n) (in addition to dimensionless Rep(x)) is to support allocation free concatenation
Agreed, particularly given the following note from https://github.com/JuliaLang/julia/issues/24595#issuecomment-345416384 :) :
An observation related to [...] the analysis in https://github.com/JuliaLang/LinearAlgebra.jl/issues/484: Of the ~30% of
ones
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
.
Taking a step back, in BandedMatrix(Rep(v), (5, 5), (1, 1))
, why would lifting v
to the type domain be necessary? Independent of v
you construct a 5x5
BandedMatrix{typeof(v)}
with (1,1)
bandwidths, and populate the stored entries of that BandedMatrix
with v
. No branching on (value) v
is necessary? Best!
Hmm, maybe you are right: I was thinking BandedMatrix(Rep(1), (n,m), (l,u))
and BandedMatrix(Fill(1,n,m), (l,u))
should be errors since it can’t convert a dense matrix to a banded one.
Then I remembered that Diagonal
, LowerTriangular
and Symmetric
just ignore the offending values.
To clarify my comment above, for BandedMatrix(Fill(1, shape), bandwidths)
either behavior you mention strikes me as reasonable, and for the stricter choice I agree that lifting the fill value into the type domain is useful. Rather, I have been thinking about BandedMatrix(Rep(1), shape, bandwidths)
as conceptually distinct from BandedMatrix(Fill(1, shape), bandwidths)
: For arbitrary shapeless iterator iter
, I interpret StructuredMatrixType(iter, shape, bandwidths)
as "build a StructuredMatrixType
with shape shape
and bandwidths bandwidths
, populating its mutable slots from iter
".
This discussion perhaps suggests that having conceptually distinct Rep
and Fill
could be great, the former with the immediately preceding interpretation in constructors and the latter interpreted as a general lazy equivalent of fill
. Thoughts? Thanks for the great discussion @dlfivefifty
! :)
We have these three versions:
MyMatrix(A, pars)
MyMatrix(I, dims, pars)
MyMatrix(Rep(x), dims, pars)
I think for the first constructor, where A
is an AbstractMatrix
, this should always be a projection: that is, if A
has the same entries as some MyMatrix
with parameters pars
then MyMatrix(A, pars)
should return a MyMatrix
with the same entries as A
.
I then think there should be consistency between MyMatrix(I, dims, pars)
and MyMatrix(eye(dims...), pars)
, and similarly between fill
and Rep
.
I think for the first constructor, where A is an AbstractMatrix, this should always be a projection: that is, if A has the same entries as some MyMatrix with parameters pars then MyMatrix(A, pars) should return a MyMatrix with the same entries as A.
Agreed, that seems reasonable.
I then think there should be consistency between MyMatrix(I, dims, pars) and MyMatrix(eye(dims...), pars), and similarly between fill and Rep .
The former seems reasonable, but how do you conclude the latter? Rep(v)
is not an AbstractMatrix
; Fill(v, shape...)
would be. Best!
trues
and falses
should really be deprecated in favor of BitArray
constructors. falses((m,n))
-> BitMatrix(false, (m,n))
isn't so bad, and it is much more clear what is going on, and what you will get back.
Until recently I assumed that falses
/trues
would give me an Array{Bool}
, which is the logical extrapolation from learning what the zeros
and ones
functions does. To be fair I have never used falses
/trues
but I think this illustrates the problem with convenience constructors. It is very confusing when different value specific fill
methods (ones
, zeros
, trues
, falses
) return different things.
Some more morning coffee thoughts: Is there anything that hinders something like this: Array(x, (n...,))
/Array{T}(x, (n...,))
which results in a n₁ × n₂ ...
Array{typeof(x)}
/Array{T}
filled with x
. It avoids the proposed (somewhat ugly) Rep(x)
and should work for immutable x
IIUC. Something like Rep(x)
could then be used essentially to say "I want a copy of x at each entry of the array".
If this is possible, then we might as well just deprecate fill(x, (n...,))
in favor of Matrix(x, (n...,))
at which point we have reached array constructor paradise.
I like this. I was just going to suggest we generalise the constructors to a broadcast rule, so that Array{T}(in, size)
makes a size
-sized Array{T}
called out
followed by out .= in
.
And size
could default to size(in)
if in
has a size and (length(in),)
if in
has no size but has a length. If it has neither than it needs to be specified.
This seems like a constructor pattern/semantic that works for many inputs (including Generator
s or simply making a copy
of another array) and many types of arrays (including immutable arrays and StaticArray
s). (We can generalize the size
to índices
or keys
also).
Ticks a lot of boxes that I’ve been searching for!
I think it's a bad idea to have it depend on whether x
has a size because then it is semantically inconsistent whether one wants to convert x
to be an Array{eltype(x)}
vs wants to create an Array{typeof(x)}
filled with x
. I would suggest the following behaviour:
A = rand(3,3) # or a MyArray
x = 1
Array(A) # Returns an `Matrix{Float64}` whose entries are specified by `A[k,j]`
Array{T}(A) # Returns an `Array{T}` whose entries are specified by `convert(T, A[k,j])`
Array(x) # Errors
Array{T}(x) # Returns a `Vector{T}` of length `x`
Array(A, (n,)) # Returns a `Vector{Matrix{Float64}}` filled with `A`
Array(x, (n,)) # Returns a `Vector{Int}` filled with `A`
Array(x, n) # Errors
Array{T}(x, n) # Returns a `x` by `n` `Matrix{T}`
So the only point of possible confusion is Array{T}(::AbstractArray)
vs Array{T}(::Int)
.
trues
andfalses
should really be deprecated in favor ofBitArray
constructors.
Probably unsurprisingly, I'm quite strongly against this. There really is no point to doing that. The argument that it is unclear what they return is not tenable, it's clearly documented and if you enter them in the REPL the container type will be spelled out. There's no ambiguity and they pose no issues. Also, BitArrays
behave like Array{Bool}
s, except for performance, so even if you assumed that trues
/falses
return an Array{Bool}
it's not like it would be particularly troublesome. Also, I use these constructors all the time.
The argument that it is unclear what they return is not tenable, it's clearly documented and if you enter them in the REPL the container type will be spelled out.
That's not the point, I just meant to illustrate that it is confusing that some value-specific fill
-methods return Array
and some BitArray
.
There's no ambiguity and they pose no issues.
IMO they suffer from the same problems as zeros
and ones
. What if I really want an Array{Bool}
or a MyArray{Bool}
? Then you have to use something else anyway since falses
/trues
don't generalize to other container types. Right now the best way to get an Array{Bool}
is to use something like fill(true, (n,n))
, great, thats not so bad, but that doesn't generalize to MyArray
. I just think the symmetry of
BitArray(true, (n,m))
Array(true, (n,m))
MyArray(true, (n,m))
is way to elegant and generalizes great to other array types. Compare this to the current state
trues((n,m))
fill(true, (n,m))
MyArray(???)
which is not very nice. What to do in the last case? Perhaps something like MyArray(trues((n,m)))
but perhaps MyArray
does not need to allocate the full BitMatrix
that this generates, then you have to come up with your own value-specific filler method just for your array type (compare for instance the sp-
mess, where spzeros
is needed since something like SparseArray(zeros(n,n))
would be a huge waste of memory or even be infeasible in most cases involving sparse matrices).
IMO they suffer from the same problems as
zeros
andones
.
These two will not entirely be deprecated, the basic forms will be kept and they will construct Array{Float64}
s, because it's a common case and they're convenient, and they are not ambiguous in that case. It's not written anywhere that convenience constructor must always be generalizable to arbitrary cases; there are special and frequently used cases for which it's just nice to have a shorthand. The argument that you could write things in a more "elegant" or "nice" way (in the sense of syntactic consistency) doesn't mean that you should necessarily delete the shorthands. (By the way, I'm not exceedingly thrilled by fill
either; that's not the point though.)
I do like the idea of tying this to the behavior of .=
. My main worry is that some strange things lurk in there, and that using iterators is much simpler. For example it seems hard to predict what will happen when making an Any array.
For example it seems hard to predict what will happen when making an Any array.
@JeffBezanson I assume this refers to Array(in, size)
and not Array{Any}(in, size)
? (I guess we write this as <:Any
vs Any
?) For the former, we could try eltype(in)
for the element type - would that be tricky with Generator
s?
I think it's a bad idea to have it depend on whether
x
has a size because then it is semantically inconsistent whether one wants to convertx
to be anArray{eltype(x)}
vs wants to create anArray{typeof(x)}
filled withx
.
@dlfivefifty Yes, I think it's important to get this right.
The main ambiguous case your raise should be when x
is an AbstractArray
, in which case I'd say the operation is a copy (like the equivalent convert
except never lazy). Just like for broadcast
, you'd want to use something like Scalar
(from StaticArrays.jl - it's a single element, zero-dimensional array) to indicate when you want the array x
broadcasted to each output element. (Like Vector(Scalar([1,2,3]), 100)
makes a length-100 vector filled with [1,2,3]
).
Another ambiguity is whether x
is a size or the object to fill the output with. We have Vector{T}(3)
creating a length-3 vector, but 3
has length
of 1
so the result could also be [3]
. Having a special rule when the "fill value" is an integer (or tuple of integers) seems unfortunate.
I have yet to see a good solution for a wide range of arrays including constructing arrays who's elements can't be mutated after construction, so I feel it is worth exploring if these ambiguities can be overcome with some semantically clear and simple rules.
Another ambiguity is whether x is a size or the object to fill the output with. We have Vector{T}(3) creating a length-3 vector, but 3 has length of 1 so the result could also be [3]. Having a special rule when the "fill value" is an integer (or tuple of integers) seems unfortunate.
Actually deprecating Vector{T}(3)
to create a length-3 vector is a goal of this issue (https://github.com/JuliaLang/julia/issues/24595#issuecomment-345468996), so scratch this I guess.
Another ambiguity is whether
x
is a size or the object to fill the output with. We haveVector{T}(3)
creating a length-3 vector, but3
haslength
of1
so the result could also be[3]
. Having a special rule when the "fill value" is an integer (or tuple of integers) seems unfortunate.
This shouldn't be ambiguous anymore since we require uninitialized
to obtain an uninitialized array. So AFAICT we can always consider the first element as the filler?
Yep just realized :)
Like
Vector(Scalar([1,2,3]), 100)
makes a length-100 vector
I think it's never a good design where the type of the data indicates behaviour, because one ends up with code like Vector(x, n)
where it's not always clear what type x
is.
In the case of Vector([1,2,3], 100)
, there is no ambiguity: one doesn't specify a dimension when asking for a copy, hence the only way it makes sense is as a vector filled with [1,2,3]
100 times.
With Matrix{Float64}(::Int, ::Int)
deprecated in favour of Matrix{Float64}(uninitialized, ::Int, ::Int)
, that makes things a lot more consistent (in fact, if the first argument is a fill argument, it makes a lot of sense for "filling" with unitialized
to be the uninitialized matrix). So here is my updated suggestion:
A = rand(3,3) # or a MyArray
x = 1
Array(A) # Returns an `Matrix{Float64} == Array{typeof(x), ndims(x)}` whose entries are specified by `A[k,j]`
Array{T}(A) # Returns an `Matrix{T} == Array{T, ndims(x)}` whose entries are specified by `convert(T, A[k,j])`
Array(x) # Returns an `Array{Int,0} == Array{typeof(x), ndims(x)}` with single entry `x`
Array{T}(x) # Returns an `Array{T,0} == Array{T, ndims(x)}` with single entry `convert(T, x)`
Array{T}(unitialized, x) # Returns a `Vector{T}` of length x
Array(A, (n,)) # Returns a `Vector{Matrix{Float64}}` filled with `A` `n` times
Array(x, (n,)) # Returns a `Vector{Int}` filled with `x` `n` times
Array(x, n) # Returns a `Vector{Int}` filled with `x` `n` times
Array{T}(unitialized, x, n) # Returns an `x` by `n` `Matrix{T}`
Thanks for the great discussion all!
@dlfivefifty, these behaviors
A = rand(3,3) # or a MyArray
x = 1
Array(A) # Returns an `Matrix{Float64} == Array{typeof(x), ndims(x)}` whose entries are specified by `A[k,j]`
Array{T}(A) # Returns an `Matrix{T} == Array{T, ndims(x)}` whose entries are specified by `convert(T, A[k,j])`
Array(x) # Returns an `Array{Int,0} == Array{typeof(x), ndims(x)}` with single entry `x`
Array{T}(x) # Returns an `Array{T,0} == Array{T, ndims(x)}` with single entry `convert(T, x)`
Array{T}(unitialized, x) # Returns a `Vector{T}` of length x
...
Array{T}(unitialized, x, n) # Returns an `x` by `n` `Matrix{T}`
are consistent with the general idea of interpreting the first argument as an iterable from which to populate the result (the working proposal). The following behaviors
Array(A, (n,)) # Returns a `Vector{Matrix{Float64}}` filled with `A` `n` times
Array(x, (n,)) # Returns a `Vector{Int}` filled with `x` `n` times
Array(x, n) # Returns a `Vector{Int}` filled with `x` `n` times
are a bit trickier. Interpreting the first argument as an iterable from which to populate the result, Array(A, (n,))
should yield a Vector{eltype(A)}
of length n
populated from iterating A
(which makes sense where n <= length(A)
), and Array(x, (n,))
/Array(x, n)
should yield a Vector{eltype(x)}
of length n
populated from iterating x
(which makes sense where n <= length(x) == 1
). Best!
Hmm, I think I agree. If keywords become fast, how about
Array(n; fill=x) # returns a length `n` `Vector{typeof(x)}` with entries `x`
I've noticed the following deprecation:
zeros(a::AbstractArray) is deprecated, use fill!(similar(a), 0) instead
I don't think the replacement for zeros(a)
is compatible with working with other array types. In an ApproxFun.jl branch, I went with Array(Zeros, A)
and BandedMatrix(Zeros, A)
, with the latter automatically getting its size and bandwidths from A
whenever A
implements the banded matrix interface. But I'm not sure that's a particularly good solution.
Perhaps if A::AbstractArray
we could deprecate zeros(A)
with
Array(A; fill=0)
Then BandedMatrix(A; fill=0)
would get both its size and bandwidths from A
.
@dlfivefifty, I would expect similar(A::BandedMatrix)
to return a BandedMatrix
similar to A
in all respects but for data, including shape and bandwidths. The banded matrix types in Base
(namely Diagonal
, Bidiagonal
, Tridiagonal
, and SymTridiagonal
) behave in this manner, and with that behavior fill!(similar(A), 0)
serves as a direct replacement for zeros(A::AbstractArray)
on these types:
julia> fill!(similar(Tridiagonal([1,2,3], [1,2,3,4], [1,2,3])), 0)
4×4 Tridiagonal{Int64,Array{Int64,1}}:
0 0 ⋅ ⋅
0 0 0 ⋅
⋅ 0 0 0
⋅ ⋅ 0 0
How does similar(A::BandedMatrix)
behave?
Alternative rewrites for zeros(A::AbstractArray)
exist and may serve better in various situations, for example zero(A)
, fill!(copy(A), 0)
, or (forthcoming) fillstored!([copy|similar](A), 0)
. zeros(A::AbstractArray)
rewrite questions having popped up a couple times now (https://github.com/JuliaLang/LinearAlgebra.jl/issues/484, https://github.com/JuliaLang/julia/pull/24656), so I have made a note to expand the deprecation warnings with additional rewrites and explanation.
Note that this discussion highlights the ambiguity in zeros(A::AbstractArray)
, and that zeros(A::AbstractArray, ...) = fill!(similar(A, ...), 0)
was how this function was defined in Base
:). Thanks and best!
Sorry, let me clarify: sometimes when I call zeros(A::BandedMatrix)
I’m actually after a Matrix
of the same dimensions as A
. But sometimes I want a BandedMatrix
with the same dimensions and bandwidths as A
. fill!(similar(A), 0)
only gives me the latter.
Ah, I see :). fill!(Matrix(A), 0)
for the latter operation? Best!
No because that copies all the entries first
You mean fill!(Matrix{eltype(A)}(size(A)), 0)
(on stable, and with uninitialized
in the Matrix
constructor on master)?
That's fine for Matrix
, though very long, but not for BandedMatrix
where one would need to test if A
satisfies the banded matrix interface (isbanded(A)
) to decide whether to call `bandwidth or not.
And in ApproxFun I'm doing code like
for TYP in (:Matrix, :BandedMatrix, :RaggedMatrix, :BlockMatrix, :BandedBlockBandedMatrix, :BlockBandedMatrix, :AlmostBandedMatrix)
@eval function convert(::Type{$TYP}, A::YetAnotherType)
ret = $TYP(Zeros, A)
# populate ret
ret
end
end
I see no way to do this general pattern where each matrix type has different parameters using something like fill!(Matrix{eltype(A)}(size(A)), 0)
.
Thanks for the clarifying code! I think I see what you are after now; I touched on an extension to the OP's second proposal in https://github.com/JuliaLang/julia/issues/11557#issuecomment-341512515 that is quite similar to what you are looking for and I agree could be lovely and useful. These ideas also head into JuliaLang/julia#18161 territory. Note that the operations you are after were not covered by zeros(A::AbstractArray)
's contract. Best!
In https://github.com/JuliaLang/julia/issues/16029#issuecomment-342577866, @Nalimilan proposed to capture the cases where collect(X)
could possibly return something else than an Array
depending on X
by AbstractArray(X)
, which is understood to collect to the most appropriate mutable container type (with Array
as a fallback).
There is a similar subproposal above for e.g. SomeArray(Rep(NaN), A)
. The difference is that SomeArray(...)
collects to SomeArray
s and AbstractArray(...)
collects to whatever it can infer from the argument or Iterator
-traits. A possible use case is
AbstractArray(NaN for a in A)
which can return a MVector
filled with NaN
s (e.g.) when A
is a static vector and this is known via a trait.
The introduction of AbstractArray(...)
with this meaning allows to recover the generality which is lost by replacing collect
with Vector
as non-breaking change even later on, so one can proceed with JuliaLang/julia#16029 .
There is a similar subproposal above for e.g. SomeArray(Rep(NaN), A). The difference is that SomeArray(...) collects to SomeArrays and AbstractArray(...) collects to whatever it can infer from the argument or Iterator-traits.
Please note that AbstractArray(Rep(NaN), A)
in that subproposal provides precisely the behavior you seek in AbstractArray(NaN for a in A)
:).
They go together, like Array(it, shape...)
and Array(it)
have both their place.
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 ones()
etc., and the third razor in the OP seems to imply that it should be done, properly, in Base. Is there some reason it couldn't simply be a callable instance?
(uninitialized)(T::DataType,dims...)=Array{T}(uninitialized,dims)
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? JuliaLang/julia#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 JuliaLang/LinearAlgebra.jl#484 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 JuliaLang/LinearAlgebra.jl#484, 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 JuliaLang/julia#16116, whetherone(T)
returned a multiplicative identity or additive generator forT
was ambiguous. JuliaLang/julia#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 https://github.com/JuliaLang/LinearAlgebra.jl/issues/484 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, JuliaLang/julia#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...)
. (#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 (#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! :)