Skip to content
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

Feature request: @fill macro for expressions that need to be evaluated for every entry #38845

Open
jkrumbiegel opened this issue Dec 12, 2020 · 7 comments
Labels
domain:arrays [a, r, r, a, y, s] kind:feature Indicates new feature / enhancement requests

Comments

@jkrumbiegel
Copy link
Contributor

I think the fill function could use an accompanying @fill macro, because I relatively often want to fill an array of a specific size with the result of an expression evaluated for every entry.

This came up recently in a Discourse post, where someone was suprised fill(Int[], 5) gives a vector with five times the same empty Int Vector.

One use-case I often encounter is in MakieLayout, where I'd like to generate a matrix of LAxis objects. They are obviously not all supposed to be the same. So a common thing to write is:

axs = [LAxis(scene) for _ in 1:4, _ in 1:3]
# or maybe
axs = [LAxis(scene) for _ in CartesianIndices((4, 3))]

I propose the syntax:

axs = @fill(LAxis(scene), 4, 3)
# or just like fill supports tuples
axs = @fill(LAxis(scene), (4, 3))

Here is a preliminary implementation:

macro fill(exp, args...)
    :([$(esc(exp)) for _ in fill_iterator(($(esc.(args)...)))])
end

function fill_iterator(ii::Integer...)
    CartesianIndices((ii...,))
end

function fill_iterator(t::Tuple)
    CartesianIndices(t)
end
@rfourquet
Copy link
Member

#16769 is very related, it motivates me to revive it as I also need that once in a while. I will update that PR with a function fill(filler::Function, ...) which fills A with repeated invocations of filler(), i.e. your proposed @fill(val, ...) would be equivalent to fill(()->val, ...).

@thofma
Copy link
Contributor

thofma commented Dec 12, 2020

Yes, this functionality would be great. But why call it @fill? How should one remember or notice that the macro version @fill does something different to fill? That invites only bugs that are hard to track down. Macro versions of functions usually do the same without subtle differences (see @code_warntype vs. code_warntype).

P.S.: Why does it need to be a macro at all?

@mcabbott
Copy link
Contributor

@thofma By the time fill is called, its arguments have already been evaluated, just once. Although I agree that @fill doesn't fit the pattern elsewhere very closely. It's possible that fill could call copy on them, which in cases like fill(Int[], 5) would be the same as the macro version, although fill(rand(), 5) would differ.

One problem with fill(::Function, size...) is that not every callable is a function, although I see that for example get!(default::Union{Function, Type}, h::Dict{K,V}, key::K) doesn't take f::Any. Another is that currently this would make an array of functions, even if that's not a very useful thing.

@thchr
Copy link
Contributor

thchr commented Dec 12, 2020

I think you were trying to explain this above @mcabbott , but what would a macro version be able to do that a higher-order function version, taking a thunk (e.g. () -> LAxis(scene)), couldn't?
Seems to me that every case could be covered by letting fill(f, args...) take a thunk f = ()-> ... (or even letting f take, say, the array index at that element; but then it seems very close in spirit to something like map, albeit with array output).

@johnnychen94
Copy link
Sponsor Member

johnnychen94 commented Dec 12, 2020

As @thchr suggested(If I understand your words correctly), an array version ntuple meets the requirements:

julia> function mapfill(f, inds...)
           map(CartesianIndices(inds)) do idx
               f(idx.I...)
           end
       end
mapfill (generic function with 1 methods)

julia> mapfill((i, j)->Int[], 2, 3)
2×3 Array{Array{Int64,1},2}:
 []  []  []
 []  []  []

@mcabbott
Copy link
Contributor

mcabbott commented Dec 12, 2020

That wasn't so clear, sorry. I agree that a version which takes a function would be pretty natural, and would serve the same role as the macro proposal.

But it seems awkward to make this a method of fill, since right now fill(() -> rand(), 3) does do something, even if it's not so useful. I can't think why you'd want an array of the same function, but maybe someone does. If you decided it was OK to break that, it would still need to be fill(::Function, size...) rather than accepting any other callable struct. Unlike say map, but like get! which similarly takes a zero-argument function just as a way to delay execution, so perhaps that's not so bad. (And IIRC this get! replaced a macro @get!.)

Another possibility is to make a method of collect. Right now collect(Float64, 1:3) works with types, but not with functions. Base.collect(f, xs...) = map(Base.splat(f), Iterators.product(xs...)) would let you write collect((_...) -> LAxis(scene), 1:3, 1:4).

@jkrumbiegel
Copy link
Contributor Author

jkrumbiegel commented Dec 12, 2020

The point of my proposal is mostly that it's quite convenient to have a short syntax for this. Of course you can do the same with higher order functions, or list comprehensions as I wrote in my post above. I personally think @fill is fine, because obviously the macro version would do something related to but different from the non-macro function. I find the difference similar to edit vs @edit

@brenhinkeller brenhinkeller added domain:arrays [a, r, r, a, y, s] kind:feature Indicates new feature / enhancement requests labels Nov 20, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
domain:arrays [a, r, r, a, y, s] kind:feature Indicates new feature / enhancement requests
Projects
None yet
Development

No branches or pull requests

7 participants