-
Notifications
You must be signed in to change notification settings - Fork 17
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
WIP: Implement mutating versions of @set and @lens (#71) #92
Conversation
Here is the diff wrt #91 (for easy review): constbase...colinxs:bangbang |
@@ -1,7 +1,8 @@ | |||
__precompile__(true) | |||
module Setfield | |||
using MacroTools | |||
using MacroTools: isstructdef, splitstructdef, postwalk | |||
using MacroTools: isstructdef, splitstructdef | |||
using BangBang: setproperty!!, setindex!! |
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.
BangBang is not a super small library and it probably will accumulate more code over time, as it has to re-implement some Base/stdlib functions. I'm not sure if it is a good approach for Setfield.jl to depend on it. Maybe we should factor out setproperty!!
and setindex!!
to ConstructionBase.jl or some small library under JuliaObjects? (Also, we probably should create a new API setproperties!!
instead.)
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.
Makes sense! What's your timeline on registering ConstructionBase.jl?
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.
We are just waiting for it JuliaRegistries/General#4063
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.
Oh, awesome!! Glad to hear.
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.
If we do refactor, need that block this PR? I'm happy to make the changes, just looking to get this merged sooner-than-later so I can start developing around it :).
Also, is #91 just waiting on JuliaRegistries/General#4063?
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.
Just pushed the changes to use MutationPolicy
.
In principal, I like the idea of making Setfield extensible. At one point I was considering manipulating deeply nested C structs using unsafe_store!
for cases where (1) the compiler can't optimize away object construction and (2) thread-safe field manipulation. I recently found mchristianl/MemoryMutate.jl which does just this and could be re-implemented in terms of Setfield.jl.
The separation of BangBang is obviously up to you all.
My selfish opinion would be if you want to refactor BangBang, then make Setfield extensible, otherwise either is fine :)
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 am fine with
Setfield
depending onBangBang
temporarily if we have a plan how to fix that in near future. - If we move the required things from
BangBang
toConstructionBase
, how much code/dependencies would that add toConstructionBase
@tkf? - I like the idea to split
@set
more cleanly in a "parsing" step and an "interpretation" step that can be overloaded by other macros. But I feel developing this is quite some effort and should be done independently of this PR.
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 am fine with
Setfield
depending onBangBang
temporarily if we have a plan how to fix that in near future.
I think fixing it (= separating out the base part of BangBang
) is hard.
- If we move the required things from
BangBang
toConstructionBase
, how much code/dependencies would that add toConstructionBase
Reviewing my code in BangBang.jl, I realized that there are quite a few interface methods in BangBang.jl (may
, possible
, pure
, ismutable
, ismutablestruct
, and _asbb
). I think it would be too much to add those methods as-is in ConstructionBase.jl. (Although I think it makes sense to have ismutablestruct(::Type{<:StructType})
and even ismutable(::Type{<:CollectionType})
there.) What ConstructionBase.jl has at the moment are a few functions that are definitely the basic things you want. However, I feel what BangBang.jl has are rather opinionated set of functions (even though I think this is a nice direction to explore).
- But I feel developing this is quite some effort and should be done independently of this PR.
I do agree this is a hard task. But I think it's the most reasonable option for implementing @set!!
and @set!
(If I were forced choose refactoring BangBang.jl or making Setfield.jl extensible, I'll definitely choose the latter).
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 did a POC here.
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.
src/lens.jl
Outdated
@@ -92,8 +100,10 @@ get(obj, ::IdentityLens) = obj | |||
set(obj, ::IdentityLens, val) = val | |||
|
|||
struct PropertyLens{fieldname} <: Lens end | |||
struct PropertyLens!{fieldname} <: Lens end | |||
struct PropertyLens!!{fieldname} <: Lens end |
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 implement it using something like this:
abstract type MutationPolicy end
struct Immutable <: MutationPolicy end # no bang
struct AlwaysMutate <: MutationPolicy end # !
struct TryMutate <: MutationPolicy end # !!
struct PropertyLens{fieldname, TP <: MutationPolicy} <: Lens
policy::TP
end
PropertyLens(fieldname, policy = Immutable()) =
PropertyLens{fieldname, typeof(policy)}(policy)
It would also be handy define accessor functions
MutationPolicy(::Lens) = Immutable() # should it be this?
MutationPolicy(l::PropertyLens) = l.policy
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.
Yeah that seems more concise (and also replaces the fairly redundant BangStyle
trait). To maintain PropertyLens
being a Singleton, what about:
struct PropertyLens{fieldname, TP} end
PropertyLens(fieldname, policy::MutationPolicy = Immutable()) = PropertyLens{fieldname, policy}()
MutationPolicy(::PropertyLens{<:Any, TP}) = TP
Also, just curious, what does TP
stand for?
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.
To maintain
PropertyLens
being a Singleton
It would be a singleton if plicy
is a singleton:
struct Demo1 end
struct Demo2{T}
demo::T
end
julia> sizeof(Demo2(Demo1()))
julia> Base.issingletontype(typeof(Demo2(Demo1())))
true
If we want to enforce the policy to be a singleton, I think your approach would be better.
what does
TP
stand for?
I was thinking Type-of-Policy. It can have a better name, too :)
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.
Ahh, I didn't know that! I was operating off of the definition in the docs:
Immutable composite types with no fields are singletons
which I took to imply "singleton if-and-only-if immutable composite type with no fields". Thanks for the explanation!
Perhaps MP
for Mutation Policy?
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.
Maybe? We can also call it simply Policy
, as done in e.g. Broadcasted
: https://github.com/JuliaLang/julia/blob/d5d5718b85d829aff6c61ab23ad2d91a50f8688b/base/broadcast.jl#L169-L173
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 like the verbosity.
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 rename
Immutable -> NeverMutate
. It is more consistent with the other variants and we might want the nameImmutable
for another trait. - I think
MutationPolixy(l::Lens) = NeverMutate()
is the right default. I thinkSetfield
still is mainly about manipulation of immutable structs. - The design should be so that an ad hoc defined lens only needs to implement
get(obj, lens)
andset(obj, lens, val)
and still works if it is not interested in mutating things.
I think the part where it gets really challenging is to implement |
I was assuming that the answer was that "users should be careful." Why not just say that you'd loose the pureness property if there is at least one mutating-lens? That is to say, isn't it OK to have x = (a=[1],)
y = set(x, (@lens _.a) ∘ (@lens! _[1]), 2)
@assert x.a === y.a
@assert x.a[1] == 2 |
Yes you example is the only reasonable behavior I can imagine. What I was talking about is struct A; a end
x = A([1])
y = set(x, @lens!! _.a[1], 2)
@assert x.a === y.a
@assert x.a[1] == 2 Does this do a constructor call to Edit: About composition of |
We had some half baked per |
Ah, that's a good point. Maybe something like this would work? function set(obj, l::ComposedLens, val)
inner_obj = get(obj, l.outer)
if MutationPolicy(l.inner) isa AlwaysMutate
set(inner_obj, l.inner, val)
obj
else
# Do what we are doing:
inner_val = set(inner_obj, l.inner, val)
set(obj, l.outer, inner_val)
end
end But, if it made the code better, it would mean that there might be some non-trivial invariant checking in the constructor. Maybe we shouldn't be skipping it? If you know that it is safe to skip those checks then you probably know the type of the outer object reasonably well so that you can directly modify mutable inner object? |
A plain
That is an interesting point. Maybe we should examine some performance/IR and check if in the case of plain structs without inner constructor julia manages to elide the reconstruction and just mutates the innermost. If julia manages to optimize that well, that would be great. If this is the case, then I agree the invariant checking is a nice feature. |
Actually, I know from experience that re-constructing |
So I'm almost done incorporating the above feedback, but ran across the following which I thought was a bug. Using your example: struct A; a end
x = A([1])
y = set(x, @lens!! _.a[1], 2)
@assert x.a === y.a
@assert x.a[1] == 2 You also have (without making the above modification to Edit: This behavior stems from x = A([1])
a = x.a
y = setproperty!!(x, :a, a)
@assert x === y |
Isn't it expected? That's equivalent to julia> a = [1];
julia> x = (a=a,);
julia> y = (a=a,);
julia> x === y
true Right? |
Ahh I guess so! Nevermind :) |
|
||
import Base: get | ||
using Base: setindex, getproperty | ||
|
||
""" | ||
Lens | ||
Lens |
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.
Can you please use four spaces for indentation? Looks like you are using two tabs?
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.
Definitely! Unsure how that happened as I usually use 4 spaces.
I think we should also deprecate the current |
A bit crazy plan is to rename the whole package :) |
Since you pointed out that my favorite name is already taken in I am somewhat depressed about it. But yeah the package needs to be renamed, we can discuss that further in #54. |
As of 5035991, we have Lines 142 to 143 in 5035991
and Lines 212 to 213 in 5035991
I should have noticed this earlier, but it means that FYI what I call mutate-if-mutable in #71 (comment) would be something like this if ismutablestruct(obj)
setproperty!(obj, name, val)
obj
else
set(obj, PropertyLens(name, NeverMutate()), val)
end We could use this for |
I was thinking how to make Here is an implementation that "replaces" PropertyLens with using Setfield
using Setfield: ComposedLens, PropertyLens, parse_obj_lens
using BangBang
struct Lens!!{T <: Lens} <: Lens
lens::T
end
Setfield.get(obj, lens::Lens!!) = get(obj, lens.lens)
# Default to immutable:
Setfield.set(obj, lens::Lens!!, value) =
set(obj, lens.lens, value)
Setfield.set(obj, lens::Lens!!{<:ComposedLens}, value) =
set(obj, Lens!!(lens.lens.outer) ∘ Lens!!(lens.lens.inner), value)
Setfield.set(obj, ::Lens!!{<:PropertyLens{fieldname}}, value) where fieldname =
setproperty!!(obj, fieldname, value)
macro set!!(ex)
ref, val = ex.args
obj, lens = parse_obj_lens(ref)
val = esc(val)
quote
$obj = let compose = $Setfield.compose,
IndexLens = $Setfield.IndexLens
$Setfield.set($obj, $Lens!!($lens), $val)
end
end
end
mutable struct Mutable
a
b
end
x = orig = Mutable(1, 2)
@set!! x.a = 10
@assert orig.a == 10
x = orig = Mutable((1, 2), 3)
@set!! x.a[1] = 10
@assert orig.a == (10, 2) There is a bit of hack needed ( diff --git a/src/sugar.jl b/src/sugar.jl
index d735047..8a5b4f2 100644
--- a/src/sugar.jl
+++ b/src/sugar.jl
@@ -133,7 +133,7 @@ struct _UpdateOp{OP,V}
end
(u::_UpdateOp)(x) = u.op(x, u.val)
-function atset_impl(ex::Expr; overwrite::Bool)
+function atset_impl(ex::Expr; overwrite::Bool, preprocess=identity)
@assert ex.head isa Symbol
@assert length(ex.args) == 2
ref, val = ex.args
@@ -142,14 +142,14 @@ function atset_impl(ex::Expr; overwrite::Bool)
val = esc(val)
ret = if ex.head == :(=)
quote
- lens = $lens
+ lens = $preprocess($lens)
$dst = set($obj, lens, $val)
end
else
op = get_update_op(ex.head)
f = :(_UpdateOp($op,$val))
quote
- $dst = modify($f, $obj, $lens)
+ $dst = modify($f, $obj, $preprocess($lens))
end
end
ret We can then do this: macro set!!(ex)
atset_impl(ex; overwrite=true, preprocess=Lens!!)
end |
Nice @tkf I actually had very much the same idea, coming from a different angle. I thought about splitting AST.ComposedLens(AST.IndexLens(:myindex), AST.PropertyLens(:myproperty)) But this structure is (almost) isomorphic to its standard interpretation ( So the outcome is very much the same as yours. The only thing I would have done differently is that instead of |
Is
I'm just curious. What will be lost when going from AST to the standard interpretation? |
It's not for (@lens _.a.b.c) ∘ settingas𝕀 insert "non-constant annotation" for Zygote (see this (@lens _.a.b.c) ∘ converting(fromfield=unwrap, tofield=uncut) ∘ settingas𝕀 That is to say, insert The question is if it makes sense to do this kind of transformation for |
We would have
Yes I think giving a lot of freedom makes sense, even if I also have no application in mind.
I am also curious. This is a bit fuzzy in my head. I cannot define in full rigor, what I mean by "this structure", "isomorphic" or "standard interpretation". But one difference is that |
Oh, I see. I was thinking that what you meant was
Me too :) It can be better. |
Yeah. Though of course a compile time transform can just insert a runtime transform, but not the other way round. But I think we should stick with the runtime version, it is more convenient. And only switch to the compile time version if we have a good reason why we need that.
I like |
Hey sorry all! I was pretty swamped these last two days. Catching up with your comments now. |
@jw3126 Oh, that's right. I missed that.
@colinxs Re JuliaObjects/ConstructionBase.jl#25 (comment), I think my approach above #92 (comment) can already be used for this. |
@tkf I'll test that out later today! Thanks for taking the time to put that together. If it works then all my needs are satisfied for now. I'll keep an eye on this to see what cool implementation you come up with. |
Thanks for all the hard work! With #95 I think my use-case is satisfied. |
@tkf @jw3126 in reference to JuliaLang/julia#21912, Setfield.jl implements the Using #95, this would be pretty straightforward to implement using |
I'm a bit confused here. If you have multiple threads, don't you want to keep objects immutable so that you don't need to worry about data races? BTW, here is a PR that implements |
@tkf If I have a struct with a I'll checkout the PR! Thanks again for all your help. |
Hmm... Is it OK to call it a data race though? I thought data race needs one or more thread(s) writing to some memory location. There is no "write" in By the way, did you check https://github.com/RelationalAI-oss/Blobs.jl? It seems to be more appropriate for mutating heap-allocated |
Oh Blobs.jl looks awesome! That might just help out quite a bit. Although some of these fields are pointers so it's not just a single flat memory region. I'll take a look :) Yeah it's not quite a "race condition" but the outcome is similar in that it depends on the order of execution. |
No description provided.