How to do rename with Accessors? This is too long.
julia> let x = (;a=1)
@delete (@insert x.b = x.a).a
end
(b = 1,)
How to do rename with Accessors? This is too long.
julia> let x = (;a=1)
@delete (@insert x.b = x.a).a
end
(b = 1,)
You could just put the @delete @insert
into a dedicated method / macro:
julia> macro rename(x, old_name, new_name)
return esc(:(@delete (@insert $x.$new_name = $x.$old_name).$old_name))
end
julia> let x = (;a=1,c=3)
@rename x a b
end
(c = 3, b = 1)
Or if you want some fancier syntax
julia> using MacroTools: @capture, postwalk
julia> macro rename(ex)
postwalk(ex) do x
if @capture(x, t_.old_ --> t_.new_)
return :( @delete (@insert $t.$new = $t.$old).$old )
else
return x
end
end |> esc
end
julia> let x = (;a=1,c=3)
@rename x.a --> x.b
end
(c = 3, b = 1)
Thereās @replace
in AccessorsExtra.jl:
julia> let x = (; a = 1, c = 3)
@replace(x.b = x.a)
end
(b = 1, c = 3)
Yes, @replace
was introduced to AccessorsExtra.jl basically for this usecase. Itās also somewhat more general and supports nesting and arbitrary optics, not just property access:
julia> x = (a=(b=1, c=2), d=3)
(a = (b = 1, c = 2), d = 3)
julia> @replace x.f = x.a.b
(a = (c = 2,), d = 3, f = 1)
julia> @replace x.a.f = x[2]
(a = (b = 1, c = 2, f = 3),)
Tbh, I donāt find myself using it almost everā¦ Curious to hear about your usage scenarios!
I saw @replace
was in my namespace but didnāt try it out because
help?> @replace
No documentation found for public symbol.
I want to change the column names of a StructArray
. Ideally Iād be able to (1) manually set each as youāve demonstrated, or (2) use a function to e.g. lowercase all of them.
I myself only used it a few times, and didnāt bother adding any docs because considered it unlikely many others would have usecases for it A docs PR would definitely be welcome.
@replace A.newcol = A.oldcol
, or @set propertynames(A)[3] = :newcol
, depending on what you mean by āsetā.
@modify(lowercase, propertynames(A)[ā])
.
Just a thought before we go documenting it:
Is @replace
definitely the right name for this macro, not @rename
? Iām just thinking replace
already has a meaning (in Base) thatās quite unrelated to this, and rename
has a quite related meaning in both NamedDims.jl and DataFrames.jl.
Yeah, it doesnāt have a docstring and wasnāt in the Pluto file that serves as docs for AccessorsExtra either, I had to go to the packageās tests to see its usage. But it is an exported name, and that Pluto doc also mentions it (and suggests doing just that - looking up tests - to learn more about it).
As an aside, the message No documentation found for public symbol.
is an additional reason to love the new public
keyword and related work. It immediately makes it clear that this is a public, okay-to-use part of the API, despite it not having docs. Technically it doesnāt add any information (since the name being exported already meant that even back then), but makes for a much smoother dev experience.
My take on this is that a function isnāt okay to use if it doesnāt have any defined semantics. The only thing we know here is that the symbol is defined in the module.
That stance makes sense in theory, but the reality is that a huge number of functions and macros in Julia - including many in Base - are so poorly defined by their docs in terms of semantics. This is better today than it was a few years ago, at least in Base, but still remains true in many cases. There are many docs that give a sentence or two of text that vaguely point towards what the function is intended to do, and a few examples that demonstrate other parts of the semantics (often things the text says nothing about).
And what people end up relying on for the most part are āreasonableā extrapolations of those docs, based on the observed behaviour of the function. And the assumption that that behaviour will not change for public names, excepting extreme edge cases/unintended interactions. And by āpeopleā I donāt mean the unwary or the careless, this is pretty much what every Julia user does in practice.
The Pluto doc here references the tests for this macro, and those tests demonstrate the semantics of it at least as well as the usual docs do, if not better. So my take is that, at least in this case, the export
makes it okay to use, and the tests give the semantics to base our understanding of the macro on, to the same extent or better as compared to basing it on the usual kind of docs.
(Iām not especially attached to this macro, just giving my philosophical take on this and also taking the chance to air out some frustration with our usual level of docs.)
Maybeā¦ And if it ends up widely useful, may make sense to upstream to Accessors.jl proper (with well-defined semantics).
Iām all for better names, but IMO rename
sounds strange as soon as we move beyond single-level property replacing. Is it really ārenameā in these examples?
@replace x.a = x.b.c[3]
@replace x.a = last(x.b.c)
@replace x.a = dirname(x.b)
...
Btw, for tables specifically, one can use columntable()
to operate on columns even for tables that donāt support property access:
julia> tbl = [(a=1, b=2), (a=3, b=4)]
2-element Vector{@NamedTuple{a::Int64, b::Int64}}:
(a = 1, b = 2)
(a = 3, b = 4)
julia> @set columntable(tbl).b = [10, 20]
2-element Vector{@NamedTuple{a::Int64, b::Int64}}:
(a = 1, b = 10)
(a = 3, b = 20)
Should work with basically any table, and within @replace
as well.
Am I getting this? In general we have to pattern match a @replace
RHS e_n(e_{n-1}(...(e_1(e_0))))
into f(optic(obj))
? So dirname(x.b.c)
is f=dirname
and optic=@o _.b.c
?
Not really, thereās no special handling to f(optic(obj))
vs optic(obj)
. Tbh, I donāt even understand what you mean by that split f
vs optic
.
Letās look at @o
and simpler functions, like set
and delete
, first:
julia> optic = @o last(_.b.c)
(@o last(_.b.c))
julia> dump(optic)
(@o last(_.b.c)) (function of type ComposedFunction{ComposedFunction{typeof(last), PropertyLens{:c}}, PropertyLens{:b}})
outer: (@o last(_.c)) (function of type ComposedFunction{typeof(last), PropertyLens{:c}})
outer: last (function of type typeof(last))
inner: PropertyLens{:c} (@o _.c)
inner: PropertyLens{:b} (@o _.b)
julia> x = (a=1, b=(c=[2,3,4], d=5))
(a = 1, b = (c = [2, 3, 4], d = 5))
julia> delete(x, optic)
(a = 1, b = (c = [2, 3], d = 5))
julia> set(x, optic, 100)
(a = 1, b = (c = [2, 3, 100], d = 5))
I thought
x = (b = (c = ["/2", "/3/3/", "/4/4/4"],), d = 5)
@replace x.a = dirname(last(x.b.c))
was going to be equivalent to
oget = (@o last(_.b.c))
oset = (@o _.a)
f = dirname
insert(
delete(x, oget),
oset,
(fāoget)(x),
)
but it turns out not quite the same
julia> insert(
delete(x, oget),
oset,
(fāoget)(x),
)
(b = (c = ["/2", "/3/3/"],), d = 5, a = "/4/4")
julia> @replace x.a = dirname(last(x.b.c))
(b = (c = ["/2", "/3/3/", "4"],), d = 5, a = "/4/4")
Everything is even easier:
@replace f(x) = g(x)
is equivalent to
insert(delete(x, g), f, g(x))
No matter if g = dirname
, g = @o _.a
, or g = @o last(_.a.b)
.
I see, thatās cool.