ANN: AddToField.jl, a macro to make constructing NamedTuples and setting properties easier

I’m announcing the release of AddToField.jl, a small package with two macros to generate NamedTuples from blocks of code.

To create NamedTuples, use @addnt:

julia> using AddToField;

julia> @addnt begin 
           @add a = 1
           @add b = a + 2
       end
(a = 1, b = 3)

julia> @addnt begin 
           @add "Variable a" a = 1
           @add b = a + 2
       end
(Variable a = 1, b = 3)

To modify existing structures, use @addto!

julia> D = Dict();

julia> @addto! D begin 
           @add a = 1
           @add b = a + 2
           @add "Variable c" c = b + 3
       end
Dict{Any,Any} with 3 entries:
  :a                   => 1
  :b                   => 3
  Symbol("Variable c") => 6

AddToField makes working with DataFrames easier. First, makes the creation of publication-quality tables easier.

using DataFrames, PrettyTables, Chain
julia> df = DataFrame(
           group = repeat(1:2, 50),
           income_reported = rand(100),
           income_imputed = rand(100));

julia> @chain df begin 
           groupby(:group)
           combine(_; keepkeys = false) do d
               @addnt begin 
                   @add "Group" first(d.group)
                   @add "Mean reported income" m_reported = mean(d.income_reported)
                   @add "Mean imputed income" m_imputed = mean(d.income_imputed)
                   @add "Difference" m_reported - m_imputed
               end
           end
           pretty_table(;nosubheader = true)
       end
β”Œβ”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Group β”‚ Mean reported income β”‚ Mean imputed income β”‚ Difference β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚     1 β”‚             0.523069 β”‚             0.53696 β”‚ -0.0138915 β”‚
β”‚     2 β”‚             0.473178 β”‚             0.41845 β”‚  0.0547277 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

It also makes constructing data frames easier

julia> using DataFrames

julia> df = DataFrame();

julia> @addto! df begin
           x = ["a", "b", "c"]
           @add x = x .* "_x"
           @add x_y = x .* "_y"
       end
3Γ—2 DataFrame
 Row β”‚ x       x_y    
     β”‚ String  String 
─────┼────────────────
   1 β”‚ a_x     a_x_y
   2 β”‚ b_x     b_x_y
   3 β”‚ c_x     c_x_y

Current limitation

You cannot use @add in new scopes created with body. The following will fail

@addnt begin
  let
      a = 1
      @add a
  end
end

This is because @addnt and @addto! create anonymous variables, then constructs and modifies objects at the end of the block. The same applies for for loops and functions inside the @addnt and @addto! blocks. In theory, @addto! should not have this limitation. However I implementing this feature in @addnt is more complicated, and At the moment maintaining simple feature parity is important.

I have submitted it for registration and should be available for download in 3 days.

8 Likes

AddToField.jl is now registered! ] add it and see if it helps your workflows!

1 Like

This is a really nice little package. I think the simplicity and predictability makes it preferable over StaticModules.jl in many contexts.

I really like the concept of just explicitly declaring what variables get passed out of the named tuple.

1 Like

Can I ask why you opted to do name clobbering rather than just create a let block? i.e. currently we have

@macroexpand @addnt begin 
    @add a = 1
    @add b = a + 2
    c = 2a + b
end

#+RESULTS:
quote
    begin
        #= In[32]:2 =#
        begin
            var"##283" = (a = 1)
        end
        #= In[32]:3 =#
        begin
            var"##284" = (b = a + 2)
        end
        #= In[32]:4 =#
        c = 2a + b
    end
    (; :a => var"##283", :b => var"##284")
end

but wouldn’t it be better for this to produce instead

quote
    let
        #= In[32]:2 =#
        a = 1
        #= In[32]:3 =#
        b = a + 2
        #= In[32]:4 =#
        c = 2a + b
    end
    (; a = a, b = b)
end

?

I opted for the clobbering for two reasons

First, I was worried about re-assignment. For clarity, I thought that adding the value exactly as it was at the @add command would be the most intuitive.

@addnt begin 
    @add x = 1
    x = 200
end

I really wanted to avoid some sort of container, like a Dict which stored values behind the scenes, since that could have performance drawbacks. So the only container to really use was just anonymous variables.

Second, a let seems like a slightly orthogonal construct for this. You shouldn’t need to make a new scope just to add a named tuple. Plus a major feature of @add x = 1 is that x could be used later. Given I like that feature so much, it seemed limiting to make x disappear after the named tuple was created.

EDIT: There is also the edge case of @add "non valid identifier" heavy_function() which needs an anonymous variable anyways.

I was about to say β€œokay, I guess I can just wrap it in a let block myself”, but then I realized to my delight that there’s nothing about your implementation that demands I use begin at all!

@macroexpand @addnt let
    @add a = 1
    @add b = a + 2
    c = 2a + b
end

#+RESULTS:
quote
    let
        #= In[37]:2 =#
        begin
            var"##294" = (a = 1)
        end
        #= In[37]:3 =#
        begin
            var"##295" = (b = a + 2)
        end
        #= In[37]:4 =#
        c = 2a + b
    end
    (; :a => var"##294", :b => var"##295")
end

I like this a lot!

Who knew!

Ill make sure to add that to the docs.

2 Likes