Skip to content

Commit

Permalink
Use begin block and allow variable export (#50)
Browse files Browse the repository at this point in the history
  • Loading branch information
jkrumbiegel authored Jun 21, 2022
1 parent 5bfc333 commit aad7f90
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 30 deletions.
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# v0.5

**Breaking**: The `@chain` macro now creates a `begin` block, not a `let` block.
This means that variables that are assigned within the macro are available outside.
Technically, situations are imaginable where this could lead to overwritten variables if someone used large expressions with intermediate variable names in begin blocks spliced into the chain.
It is however quite unlikely for the normal way that `@chain` is intended to be used.

Additionally, it is now possible to use the syntax `variable = some_expression` to make use of the feature that variables can be exported.
The `some_expression` part is handled exactly like before.
This enables you to carry parts of a computation forward to a later step in the chain or outside of it:

```julia
@chain df begin
transform(...)
select(...)
intermediate = subset(...)
groupby(...)
combine(...)
join(intermediate)
end

@show intermediate
```
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "Chain"
uuid = "8be319e6-bccf-4806-a6f7-6fae938471bc"
authors = ["Julius Krumbiegel"]
version = "0.4.10"
version = "0.5.0"

[compat]
julia = "1"
Expand Down
39 changes: 27 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,14 @@ result = @chain df begin
end
```

The pipeless block is equivalent to this:
The chain block is equivalent to this:

```julia
result = let
var1 = dropmissing(df)
var2 = filter(r -> r.weight < 6, var1)
var3 = groupby(var2, :group)
var4 = combine(var3, :weight => sum => :total_weight)
result = begin
local var"##1" = dropmissing(df)
local var"##2" = filter(r -> r.weight < 6, var"##1")
local var"##3" = groupby(var"##2", :group)
local var"##4" = combine(var"##3", :weight => sum => :total_weight)
end
```

Expand Down Expand Up @@ -151,6 +151,21 @@ This works well for short sequences that are still easy to parse visually withou
@chain 1:10 filter(isodd, _) sum sqrt
```

## Variable assignments in the chain

You can prefix any of the expressions that Chain.jl can handle with a variable assignment.
The previous value will be spliced into the right-hand-side expression and the result will be available afterwards under the chosen variable name.

```julia
@chain 1:10 begin
_ * 3
filtered = filter(iseven, _)
sum
end

filtered == [6, 12, 18, 24, 30]
```

## The `@aside` macro

For debugging, it's often useful to look at values in the middle of a pipeline.
Expand All @@ -172,12 +187,12 @@ end
Which is again equivalent to this:

```julia
result = let
var1 = dropmissing(df)
var2 = filter(r -> r.weight < 6, var1)
var3 = groupby(var2, :group)
println("There are $(length(var3)) groups after step 3.")
var4 = combine(var3, :weight => sum => :total_weight)
result = begin
local var"##1" = dropmissing(df)
local var"##2" = filter(r -> r.weight < 6, var"##1")
local var"##3" = groupby(var"##2", :group)
println("There are $(length(var"##3")) groups after step 3.")
local var"##4" = combine(var"##3", :weight => sum => :total_weight)
end
```

Expand Down
23 changes: 16 additions & 7 deletions src/Chain.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ is_aside(x) = false
is_aside(x::Expr) = x.head == :macrocall && x.args[1] == Symbol("@aside")


insert_first_arg(symbol::Symbol, firstarg) = Expr(:call, symbol, firstarg)
insert_first_arg(any, firstarg) = insertionerror(any)
insert_first_arg(symbol::Symbol, firstarg; assignment = false) = Expr(:call, symbol, firstarg)
insert_first_arg(any, firstarg; assignment = false) = insertionerror(any)

function insertionerror(expr)
error(
Expand Down Expand Up @@ -35,12 +35,21 @@ function is_moduled_symbol(e::Expr)
e.args[2].value isa Symbol
end

function insert_first_arg(e::Expr, firstarg)
function insert_first_arg(e::Expr, firstarg; assignment = false)
head = e.head
args = e.args

# variable = ...
# set assignment = true and rerun with right hand side
if !assignment && head == :(=) && length(args) == 2
if !(args[1] isa Symbol)
error("You can only use assignment syntax with a Symbol as a variable name, not $(args[1]).")
end
variable = args[1]
righthandside = insert_first_arg(args[2], firstarg; assignment = true)
:($variable = $righthandside)
# Module.SubModule.symbol
if is_moduled_symbol(e)
elseif is_moduled_symbol(e)
Expr(:call, e, firstarg)

# f(args...) --> f(firstarg, args...)
Expand Down Expand Up @@ -104,7 +113,7 @@ function rewrite(expr, replacement)
new_expr = insert_first_arg(new_expr, replacement)
end
replacement = gensym()
new_expr = Expr(Symbol("="), replacement, new_expr)
new_expr = :(local $replacement = $new_expr)
end

(new_expr, replacement)
Expand Down Expand Up @@ -188,7 +197,7 @@ function rewrite_chain_block(block)
# we just do the firstvar transformation for the first non LineNumberNode
# we encounter
if !(did_first || expr isa LineNumberNode)
expr = Expr(Symbol("="), firstvar, expr)
expr = :(local $firstvar = $expr)
did_first = true
push!(rewritten_exprs, expr)
continue
Expand All @@ -198,7 +207,7 @@ function rewrite_chain_block(block)
push!(rewritten_exprs, rewritten)
end

result = Expr(:let, Expr(:block), Expr(:block, rewritten_exprs..., replacement))
result = Expr(:block, rewritten_exprs..., replacement)

:($(esc(result)))
end
Expand Down
63 changes: 53 additions & 10 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,6 @@ end
sum
end
end)

# variable defined in chain block doesn't leak out
z = @chain [1, 2, 3] begin
@aside inside_var = 5
@aside @test inside_var == 5
sum(_) + inside_var
end
@test z == 11
@test_throws UndefVarError inside_var
end

@testset "nested chains" begin
Expand Down Expand Up @@ -441,4 +432,56 @@ end
@test 36 == @chain 1:3 begin
@chain _ sum _ ^ 2
end
end
end

@testset "variable assignment syntax" begin
result = @chain 1:10 begin
x = filter(iseven, _)
y = sum
sqrt
end
@test x == filter(iseven, 1:10)
@test y == sum(x)
@test result == sqrt(y)
end

module TestModule
using Chain
end

@testset "no variable leaks" begin

allnames() = Set(names(TestModule, all = true))
_names = allnames()

TestModule.eval(quote
@chain 1:10 begin
sum
sqrt
end
end)

@test setdiff(allnames(), _names) == Set()

TestModule.eval(quote
@chain begin
1:10
sum(_)
sqrt(_)
end
end)

@test setdiff(allnames(), _names) == Set()

TestModule.eval(quote
@chain begin
1:10
x = sum(_)
y = sqrt(_)
end
end)

@test setdiff(allnames(), _names) == Set([:x, :y])
end


2 comments on commit aad7f90

@jkrumbiegel
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/62804

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.5.0 -m "<description of version>" aad7f90e21c0b313fc53327f93634a439fa00e5e
git push origin v0.5.0

Please sign in to comment.