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

Argument order for rand! #8246

Closed
simonster opened this issue Sep 5, 2014 · 38 comments
Closed

Argument order for rand! #8246

simonster opened this issue Sep 5, 2014 · 38 comments
Labels
domain:randomness Random number generation and the Random stdlib needs decision A decision on this change is needed

Comments

@simonster
Copy link
Member

Is there a reason that the output array comes last instead of first as is the case for most other mutating functions? Right now we could change the argument order and add a deprecation, but we should decide whether we want to change this before #6003.

@andreasnoack
Copy link
Member

This has been discussed somewhere before and I think @johnmyleswhite gave the answer. Personally, I don't like the rule that the mutated argument should be the first one. It could be awkward for some functions and for all the matrix multiplication and division functions I'd prefer that we followed the BLAS conventions strictly and that would mean that the mutating argument was last (as well as adding scalar α and β arguments). For some linear algebra functions you'd also want to give a workspace array as argument to avoid reallocation and such an argument would make more sense as the last argument.

@johnmyleswhite
Copy link
Member

I'm happy with any rule personally. Whether the mutated arguments are first or last doesn't matter much to me, although the argument against putting mutated arguments last is that it breaks functions with varargs.

@simonster
Copy link
Member Author

I don't have a preference on the rule, just that there should be one, or that we should get #249. But I also think at this point we don't want to break all code that uses mutating functions by moving the output last.

As far as matrix functions, maybe α, β, and workspace should be keyword arguments? Although right now that incurs kwsorter overhead.

@iamed2
Copy link
Contributor

iamed2 commented Sep 5, 2014

Perhaps there could be a convention where the names of mutating arguments end in an exclamation point?

rand!{T}(r::Range{T},A::AbstractArray{T,N})

would become

rand!{T}(r::Range{T},A!::AbstractArray{T,N})

in docs and when calling methods().

@timholy
Copy link
Sponsor Member

timholy commented Sep 5, 2014

I don't think there are any established conventions, here or elsewhere. While BLAS puts it last, more often than not libc puts them first (memcpy, fgets, strcpy). But even libc is inconsistent (sscanf, although there's a reason for that violation). I have a slight preference for first, but above all would prefer reasonable consistency (whatever that may be)

I would also conceptually distinguish output from mutating. mutating to me means it's OK to destroy it. output means just that, it's acting as a return value. As discussed elsewhere, there are sometimes performance advantages in passing in a dedicated output, even if you could just use the input to return the output. This is one of the things I definitely don't like about the BLAS/LAPACK API.

@timholy
Copy link
Sponsor Member

timholy commented Sep 5, 2014

@iamed2, it's a good suggestion, but keyword arguments have an amusing gotcha on this: #8020.

@timholy
Copy link
Sponsor Member

timholy commented Sep 5, 2014

Better link for the reason for separating output from input: #7513 (comment)

@andreasnoack
Copy link
Member

@timholy I can't see the performance advantage for triangular solve on my machine

julia> minimum([@elapsed (for i = 1:100;MyTest.mysolve1(A, b, x);end) for j = 1:10])
0.306543869
julia> minimum([@elapsed (for i = 1:100;copy!(x,b);MyTest.mysolve2(A, x);end) for j = 1:10])
0.305641564

I'm not that surprised about the result. Efficiency seems to have been a pretty high priority in the development of BLAS.

The code is

module MyTest

    function mysolve1{T,S}(A::Triangular{T,S,:L,false}, b, x = b)
        n = size(A, 1)
        Ad = A.data
        @inbounds begin
            for j = 1:n
                xj = b[j]
                for i = 1:j-1
                    xj -= Ad[j,i]*x[i]
                end
                x[j] = xj/A[j,j]
            end
        end
        x
    end

    function mysolve2{T,S}(A::Triangular{T,S,:L,false}, b)
        n = size(A, 1)
        Ad = A.data
        @inbounds begin
            for j = 1:n
                bj = b[j]
                for i = 1:j-1
                    bj -= Ad[j,i]*b[i]
                end
                b[j] = bj/A[j,j]
            end
        end
        b
    end
end

@andreasnoack
Copy link
Member

@simonster I really think it is a mistake that we haven't followed the BLAS convention completely and I also hope that we can still break code to make things right.

Actually, I began the work to change the convention some time ago, but the development stalled in discussion.

@timholy
Copy link
Sponsor Member

timholy commented Sep 6, 2014

Sure, for this kind of algorithm it won't matter. Compare the running time to that of copy!ing x---for this algorithm it's access to the elements of A, not x, that matters.

But that doesn't mean that it's a good model to apply to other algorithms that may be cache-limited on the output.

@rfourquet
Copy link
Member

All rand methods are of the form rand([rng], [out]), with:

  • rng is the the generator, and defaults to the global one
  • out is the type information of the output:
    • first the type of randoms, which defaults to Float64
    • and the dimensions of the generated array, or nothing for a scalar

Note that if the rng is given, the type of randoms can not be specified, it is assumed that it's the responsibility of rng.

So it makes sense that an output array A comes last in rand!. Using @timholy's terminology (#8246 (comment)), A can be seen as a "mutating" argument which provides both the type and dimensions (out above) to the rand! function. It also happens to be an "output" argument. Hence the signature: rand!([rng], out).

@rfourquet
Copy link
Member

To insist on the "output" characteristic of A, why not A[:] = randgen([rng], T) where randgen([rng]]) an infinite iterator producing random T's (or maybe with a bit of magic in the relevant getindex to deduce automatically T=eltype(A)) ?

@simonster
Copy link
Member Author

I suppose there is also some analogy to fill!, which takes the array last.

@simonster
Copy link
Member Author

Actually that's wrong: the argument order for fill is fill(x, sz) or fill!(out, x), which supports the idea that the output of rand! should come first even if it means inverting the order of "generator" and "output" between rand and rand!.

@simonster
Copy link
Member Author

It's also arguable that any variant of rand that takes an explicit generator object should be rand! since it modifies the state of the generator...

@ivarne
Copy link
Sponsor Member

ivarne commented Sep 16, 2014

@simonster The same argument was tried on IO functions, but I think it was decided that even though most of them modify a IO object, only those utilizing pre-allocated memory, should have the ! suffix.

@rfourquet
Copy link
Member

And it would be less useful: the bang ! would loose a bit of its warning power as it would be often used in a harmless context, e.g. print!(io, 'c') or rand!(rng, Int).

It would be nice if rand! and fill! had the same rules, I prefer that of rand!.

@timholy
Copy link
Sponsor Member

timholy commented Sep 16, 2014

Here's my personal list of advantages for either order. Please add to it. Since there is no convention among C libraries, I'm not including "BLAS does it that way" because one can equally well say "libc does it the other way".

Advantages of output first:

  1. func!(out, A) looks more like out = func(A), and having the output next to the ! associates the two.
  2. func!(out, input, [temporaryworkspacevariable]) clearly distinguishes (optional) mutating arguments from output arguments. It's slightly less clear where to stop with func!(input, out, [temporaryworkspacevariable]).
  3. Multidimensional functions with a varargs input cannot take the output last. setindex! is the poster child for this behavior: you simply can't write this function as setindex!(val, indexes..., output). (Many people might also wish it were setindex!(output, indexes..., val) but that's also not possible, since indexes must come last.)

Advantages of input first:

  1. Semantically, people almost always talk about input before they talk about output.
  2. If you think about the output as an optional argument, it makes sense for it to be last. HOWEVER, this is a bit of a red herring, because we have two separate functions, fill and fill!, so there's really no sense in which an output argument is actually an optional argument.

To me, the varargs/setindex! example (which I only just noticed now, long after I had developed my vague preference for output first) basically settles the argument: outputs should be first. All the other points are wishy-washy subjective issues, but the varargs/setindex! is a clear technical constraint.

rfourquet added a commit to rfourquet/julia that referenced this issue Sep 16, 2014
@johnmyleswhite
Copy link
Member

Completely agree with @timholy: the varargs case strongly argues for putting all output arguments at the front.

@timholy
Copy link
Sponsor Member

timholy commented Sep 16, 2014

Ah, and reading up I see you made that same point at the top. Could have saved myself some time...

rfourquet added a commit to rfourquet/julia that referenced this issue Sep 17, 2014
rfourquet added a commit to rfourquet/julia that referenced this issue Sep 17, 2014
rfourquet added a commit to rfourquet/julia that referenced this issue Sep 30, 2014
rfourquet added a commit to rfourquet/julia that referenced this issue Sep 30, 2014
@ViralBShah
Copy link
Member

Given that we are on this path, I guess we just need to go ahead and change the calling sequences for rand! to be consistent with the outputs first rule.

@johnmyleswhite
Copy link
Member

+1

@ViralBShah
Copy link
Member

@rfourquet Since you have considerably improved the RNG codebase and are the one most familiar with it, would it be possible for you to create a PR for this change?

@rfourquet
Copy link
Member

Yes I will do that. But now that objects can be call'ed, I just wanted to mention this alternative: change rand /rand! to call (kind of like in C++11) and use rand/rand! only for the global RNG, e.g.:

rng = MersenneTwister()
x = rng()
a = Array(Int, 10)
rng(a)
rand!(a) # similar to Base.Random.GLOBAL_RNG(a)
...

This would imply having the rng as first argument of call, but is mostly independant of this issue otherwise.

@ViralBShah
Copy link
Member

I like the suggestion. Would love to hear what others have to say.

@andreasnoack
Copy link
Member

I also like it. How would this fit into Distributions.jl?

@ivarne
Copy link
Sponsor Member

ivarne commented Nov 19, 2014

I don't like overloading call for this purpose (yet). It is a step away from the current Julian APIs with documented generic functions (like rand) that packages can extend for their own types.

@ViralBShah
Copy link
Member

@simonbyrne
Copy link
Contributor

This could be interesting, but I'm slightly skeptical. What does rng(a) do in the above example?

@rfourquet
Copy link
Member

Yes rng(a) looks ambiguous now (rand(rng, A) vs rand!(rng, A)), but this could probably be sorted out.

@rfourquet
Copy link
Member

I asked if it would be possible to have the rng!(A) syntax for rand!(rng, A), but it wasn't. In C++, rand(rng, A) would be written as something like distribution(A)(rng) (cf. e.g. n3551.pdf), but this probably belongs to another discussion.

@rfourquet
Copy link
Member

The current syntax to fill an array A with values from 1:9 is rand!([rng,] 1:9, A), with two possible ways to change the argument order: rand!([rng,] A, 1:9) and rand!(A, [rng,] 1:9).
I prefer the first one because rng is the state allowing rand! to work, so I see the pair (rand!, rng) as a "function object", and I would give rng the highest priority to be first argument of rand!; and like in fill!(A, 9), the output array still comes before the source of values 1:9.
What do you think?

@johnmyleswhite
Copy link
Member

Given the proposed rule that outputs must always come first, I'd prefer rand!(A, [rng,] 1:9) unless there's a function rand!(A, 1:9) that produces a modified rng object as its output.

@ViralBShah
Copy link
Member

While I like the consistency, Putting the state argument second doesn't appeal aesthetically. I would love to have rand!(rng, A, 1:9). I wish I could come up with a better justification.

@toivoh
Copy link
Contributor

toivoh commented Nov 20, 2014

Well, the call does actually mutate both of the two first arguments :)
I also think that putting the rng first feels right. Maybe its by analogy
to println, where the optional io argument goes first.

@StefanKarpinski
Copy link
Sponsor Member

I feel like having the RNG anywhere but first or as a keyword arg looks really odd.

@ivarne
Copy link
Sponsor Member

ivarne commented Nov 20, 2014

If we look at this from a dot oriented language, it would clearly be written as rng.rand!(A, 1:10), not A.rand!(rng, 1:10). Therefore I think the "rng object first" trumps the "mutated array first" rule (in this case).

@lindahua
Copy link
Contributor

rand is often used within tight loops. RNGs as keyword arguments would cause substantial performance hit, as we have RNGs of different types.

ViralBShah pushed a commit that referenced this issue Nov 21, 2014
change argument order for rand! (fix #8246)
bjarthur pushed a commit to bjarthur/julia that referenced this issue Nov 21, 2014
The API to fill randomly an array A is changed
from rand!([rng], [::Range], A) to rand!([rng], A, [::AbstractArray]).
Enabling [::AbstractArray] instead of only [::Range] depended on choosing first
the argument order.
@ViralBShah ViralBShah added the domain:randomness Random number generation and the Random stdlib label Nov 22, 2014
waTeim pushed a commit to waTeim/julia that referenced this issue Nov 23, 2014
The API to fill randomly an array A is changed
from rand!([rng], [::Range], A) to rand!([rng], A, [::AbstractArray]).
Enabling [::AbstractArray] instead of only [::Range] depended on choosing first
the argument order.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
domain:randomness Random number generation and the Random stdlib needs decision A decision on this change is needed
Projects
None yet
Development

No branches or pull requests