-
Notifications
You must be signed in to change notification settings - Fork 29
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
Adds @returned_quantities
macro
#696
base: master
Are you sure you want to change the base?
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #696 +/- ##
==========================================
- Coverage 79.22% 77.78% -1.45%
==========================================
Files 30 30
Lines 4212 3938 -274
==========================================
- Hits 3337 3063 -274
Misses 875 875 ☔ View full report in Codecov by Sentry. |
Pull Request Test Coverage Report for Build 11627641486Warning: This coverage report may be inaccurate.This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.
Details
💛 - Coveralls |
Co-authored-by: Xianda Sun <[email protected]>
@torfjelde I suggest we change the prefix feature to a # submodel prefixing
julia> @model function demo2(x, y, z)
a = @returned_quantities prefix_variables(demo1(x), "sub1")
b = @returned_quantities prefix_variables(demo1(y), "sub2")
return z ~ Uniform(-a, b)
end;
julia> rand(demo2(missing, missing, 0.4))
(var"sub1.x" = 0.5865756059371534, var"sub2.x" = -0.25563799658500047)
# rand prefixing
julia> ret = rand(prefix_variables(demo1(1.), "prior_sample"))
# generated quantities / predict
julia> returned_quantities(prefix_variables(demo1(1.), "generated_var_"), chain)
This would also help unify the syntax of This could be further unified with |
We already have prefix(model::Model, x) = contextualize(model, PrefixContext(model.context, Symbol(x))) or something as an additional definition. However, I'm a bit worred about
|
I like the
Point taken, but this is very minor and a bit subjective. |
But this means that the user needs to be careful and do |
It is a standard Julia performance trick, so it is okay. By default, we can print a performance warning message when users call |
I'm also happy to turn @returned_quantities @prefix(model, :prefix_) |
make things easier to basic users
before wrapping in `Val`
contexts in `model.context`
Added a |
…cro' into torfjelde/returned-quantities-macro
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Thanks, @torfjelde; I'm happy with the changes. To minimise interface confusion ( Thoughts? @mhauru and @penelopeysm |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For prefix
/@prefix
, maybe keep both but only export the macro? It sounds like unless you know what you are doing, you should use @prefix
. And if you know what you're doing, you don't need it be exported. I do generally think it's a good idea to have a macro-free option available if possible.
For returned_quantities
/@returned_quantities
we still need both, because one is to be used outside of @model
, the other inside, right? I forget what we concluded about this in our call, but I do worry users will mix the two up and get confusing errors.
src/submodel_macro.jl
Outdated
true | ||
julia> # Or using some arbitrary expression. | ||
@model submodel_prefix_expr() = a = @returned_quantities prefix=1 + 2 inner() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found
@returned_quantities prefix=1 + 2 inner()
hard and unintuitive to parse. I think
@returned_quantities prefix=(1 + 2) inner()
would be much clearer. Not sure if this a documentation issue, or if we should disallow the former.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a documentation issue IMO, as this is not doing any special parsing but reliying on Julia's expression parsing.
@@ -14,10 +14,18 @@ These statements are rewritten by `@model` as calls of [internal functions](@ref | |||
@model | |||
``` | |||
|
|||
One can nest models and call another model inside the model function with [`@submodel`](@ref). | |||
One can nest models and call another model inside the model function with [`@submodel`](@ref) and [`@returned_quantities`](@ref). | |||
|
|||
```@docs | |||
@submodel |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the plan to keep @submodel
indefinitely, even though @returned_quantities
does the same job?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would think we'd remove @submodel
at some point. @yebai ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, let's depreciate-then-delete @submodel
in favour of @returned_quantities
Then, |
Just so we're all on the same page: |
Deprecated |
The best we can do is @doc """
@returned_quantities(model::Model, chain::MCMCChains.Chains)
...
""" :(DynamicPPL.@returned_quantities) I suppose, which gives the false sense of the macro having the capability of multiple dispatch.. |
A specific technical point: constant propagation has gotten much better in recent versions of Julia, so I'm not entirely sure that @torfjelde 's point regarding the need to use julia> struct Foo{T} end
julia> @noinline make_foo(T::Symbol) = Foo{T}() # do not inline, rely entirely on constant propagation
make_foo (generic function with 1 method)
julia> @code_warntype (() -> make_foo(:hello))()
MethodInstance for (::var"#71#72")()
from (::var"#71#72")() @ Main REPL[21]:1
Arguments
#self#::Core.Const(var"#71#72"())
Body::Foo{:hello}
1 ─ %1 = Main.make_foo(:hello)::Core.Const(Foo{:hello}())
└── return %1
julia> @code_warntype optimize=true (() -> make_foo(:hello))()
MethodInstance for (::var"#3#4")()
from (::var"#3#4")() @ Main REPL[4]:1
Arguments
#self#::Core.Const(var"#3#4"())
Body::Foo{:hello}
1 ─ return $(QuoteNode(Foo{:hello}())) Similarly, if I add a method of julia> @noinline make_foo(x::String) = make_foo(Symbol(x))
make_foo (generic function with 2 methods)
julia> @code_warntype (() -> make_foo("hello"))()
MethodInstance for (::var"#5#6")()
from (::var"#5#6")() @ Main REPL[6]:1
Arguments
#self#::Core.Const(var"#5#6"())
Body::Foo{:hello}
1 ─ %1 = Main.make_foo("hello")::Core.Const(Foo{:hello}())
└── return %1
julia> @code_warntype optimize=true (() -> make_foo("hello"))()
MethodInstance for (::var"#7#8")()
from (::var"#7#8")() @ Main REPL[7]:1
Arguments
#self#::Core.Const(var"#7#8"())
Body::Foo{:hello}
1 ─ return $(QuoteNode(Foo{:hello}())) Since our implementation of In short, I think writing |
Indeed @willtebbutt seems to be correct that this should work as intended even without requiring Buuuut this then means that we're raising this discussion of macros vs. functions again... |
If a macro is not necessary, then I am entirely of the view that we should use a function. It seems like the I'm firmly in favour of dispensing with the |
Seems @willtebbutt is very much correct here 🎉 The following causes no issues (the using Revise, DynamicPPL, Distributions
@model inner0() = x ~ Normal()
@model inner1() = @returned_quantities DynamicPPL.prefix(inner0(), "1")
@model inner2() = @returned_quantities DynamicPPL.prefix(inner1(), "2")
@model inner3() = @returned_quantities DynamicPPL.prefix(inner2(), "3")
@model inner4() = @returned_quantities DynamicPPL.prefix(inner3(), "4")
@model inner5() = @returned_quantities DynamicPPL.prefix(inner4(), "5")
@model inner6() = @returned_quantities DynamicPPL.prefix(inner5(), "6")
@model inner7() = @returned_quantities DynamicPPL.prefix(inner6(), "7")
@model inner8() = @returned_quantities DynamicPPL.prefix(inner7(), "8")
model = inner8()
DynamicPPL.DebugUtils.model_warntype(model, optimize=true) # infers nicely |
A small detail, but is there a particular reason to call it |
Summary of discussion from yesterday's meeting. The discussion can be broken down into two stages:
Should we unify the name of
|
I think we agreed that The remaining issue is whether we would like to enforce consistency, which would force one to use
My rule for this PR: (1) replace |
Oh then I definitively misunderstood the sentiment; I thought we landed on (1) but naming
Really, really not a fan of this. My experience with Turing.jl has been that people much prefer explicit methods that does one thing rather than depending on kwargs. It's also an issue that
At the moment, I don't think there's a clear path to finishing this. It has issues that are unclear how to fix. See #710 and related discussion. |
I'll clarify a few things.
Both cases return a single value but possibly of different types, i.e.
The choice of A more substantial point is using input arguments to determine output types. This is a very standard practice in multi-dispatching, IIUC. In fact, we already do this in several places, using positional arguments with default values, see
The point is transferring |
My point still stands here I think. A flag (whether it's positional or not) which alters the return-type is generally discouraged.
Aye, but IMO the key difference there is that the return-type itself is an explicit argument to the function (+ in both those cases, the underlying "behavior" is exactly the same; it's just a matter of type-difference. In contrast
Gotcha; I misunderstood the purpose of that PR / the comment then 👍 |
Let's merge this PR in the next 2-3 days. As I see it, two issues remain here:
During Monday's meeting, @sunxd3 raised an interesting point regarding (1), which deserved more attention retrospectively: the # simple case,
julia> z := returned_quantities(submodel)
# more general case
# `coloneq` recursively check for `returned_quantities`
julia> z := returned_quantities(submodel) + 3 Here, The Thoughts? For issue (2), I'd be happy to keep |
I'm new to the (Note that this would have to fail in some clear and understandable way if I'm less convinced about this allowing us to drop the |
This can be avoided if we make EDIT: it is possible to make |
I'm not sure I understand. How would |
Perhaps I shouldn't stress that Here is what it means:
Maybe this is slightly unusual at first glance, but I think it is a good idea once we get used to it. EDIT: Some additional details need to be spelt out during the implementation, so my code snippets above should be taken as illustrations rather than the actual function signature. |
Trying to digest this.
It's not clear to me what the clarity is that is gained. What's a possible confusion that using |
Semantically, |
In principle, we could do that (assuming it is consistent with other parts of the codebase). |
I don't quite understand how
|
Also how I understand it, in which case it's just like the current |
Sorry, I was ambiguous. By @model inner_model(params)
blahblah
return x, y, z
end
inner_model_instance = inner_model(params)
@model outer_model()
a, b, c := inner_model_instance()
end EDIT: The
However, this is treating objects differently based on their type, rather than by their variable name, which I'm much more okay with. |
But this would incorrectly call the model unless you somehow captured this fact with the |
That would be equivalent and I am happy with it. But one would need to write @model outer_model()
a, b, c := inner_model(params)() # okay, but one could easily forget including `()`
a, b, c := generated_quantities(inner_model(params)) # slightly more verbose, but hard to get wrong.
end |
Sorry, but why would |
@torfjelde, forgot to comment on this, see the edit I made while you responded. In short: Yes, |
But this comes with a host of issues, no? How are we to recognize that a callable is a model at parse time? Are we then always assuming that whatever is on the RHS of |
Would need to be recognized at runtime. |
But how? 😕 |
Something like "any call in the RHS is converted by Haven't though through whether this has type stability implications. |
This adds the
@returned_quantities
macro as discussed @yebai @mhauruThis is meant to be a replacement for
@submodel
macro, but without the ability to do automatic prefixing. It ends up looking likeLikely TODOs:
@submodel
telling the user to use@returned_quantities
.generated_quantities
toreturned_quantities
in this PR?Fix #691