Shorter way to rename property, with Accessors

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)
6 Likes

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 :slight_smile: 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)[āˆ—]).

1 Like

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.)

1 Like

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)) :slight_smile:
No matter if g = dirname, g = @o _.a, or g = @o last(_.a.b).

2 Likes

I see, thatā€™s cool.