Skip to content

add @__FUNCTION__ macro #841

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

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "Compat"
uuid = "34da2185-b29b-5c13-b0c7-acf172513d20"
version = "4.17.0"
version = "4.18.0"

[deps]
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ changes in `julia`.

## Supported features

* `@__FUNCTION__` is a macro that returns the innermost enclosing function ([#58940]) (since Compat 4.18.0)

* `filter` can now act on a `NamedTuple` [#50795]. (since Compat 4.17.0)

* `insertdims(D; dims)` is the opposite of `dropdims` ([#45793]) (since Compat 4.16.0)
Expand Down Expand Up @@ -201,3 +203,4 @@ Note that you should specify the correct minimum version for `Compat` in the
[#50105]: https://github.com/JuliaLang/julia/issues/50105
[#50795]: https://github.com/JuliaLang/julia/issues/50795
[#54653]: https://github.com/JuliaLang/julia/issues/54653
[#58940]: https://github.com/JuliaLang/julia/issues/58940
54 changes: 54 additions & 0 deletions src/Compat.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1229,6 +1229,60 @@ else
using Base: Fix, Fix1, Fix2
end

# https://github.com/JuliaLang/julia/pull/58940
@static if !isdefined(Base, Symbol("@__FUNCTION__"))
_function_macro_error() = throw(ArgumentError("`Compat.@__FUNCTION__` is not available in this context"))

macro __FUNCTION__()
esc(quote
$(Expr(:isdefined, :var"#self#")) || $(_function_macro_error)()
var"#self#"
end)
end

@doc """
@__FUNCTION__

Get the innermost enclosing function object.

!!! warning
The Compat.jl version of `@__FUNCTION__` differs from the Base version in
VERSION < v"1.13.0-DEV.880" in two contexts:
(1) it _will_ throw an error when used in certain contexts (such as callable structs), and
(2) it will _not_ throw an error when used inside a comprehension or generator.

!!! note
`@__FUNCTION__` has the same scoping behavior as `return`: when used
inside a closure, it refers to the closure and not the outer function.
Some macros, including [`@spawn`](@ref Threads.@spawn), [`@async`](@ref), etc.,
wrap their input in closures. When `@__FUNCTION__` is used within such code,
it will refer to the closure created by the macro rather than the enclosing function.

# Examples

`@__FUNCTION__` enables recursive anonymous functions:

```jldoctest
julia> factorial = (n -> n <= 1 ? 1 : n * (@__FUNCTION__)(n - 1));

julia> factorial(5)
120
```

`@__FUNCTION__` can be combined with `nameof` to identify a function's
name from within its body:

```jldoctest
julia> bar() = nameof(@__FUNCTION__);

julia> bar()
:bar
```
""" :(@__FUNCTION__)

export @__FUNCTION__
end

include("deprecated.jl")

end # module Compat
191 changes: 190 additions & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1079,4 +1079,193 @@ end
longnt = NamedTuple{ntuple(i -> Symbol(:a, i), 20)}(ntuple(identity, 20))
@test filter(iseven, longnt) === NamedTuple{ntuple(i -> Symbol(:a, 2i), 10)}(ntuple(i -> 2i, 10))
@test filter(x -> x<2, (longnt..., z=1.5)) === (a1=1, z=1.5)
end
end

# https://github.com/JuliaLang/julia/pull/58940
@testset "@__FUNCTION__" begin
@testset "Basic usage" begin
# @__FUNCTION__ in regular functions
test_function_basic() = @__FUNCTION__
@test test_function_basic() === test_function_basic
end

@testset "Recursion" begin
# Factorial with @__FUNCTION__
factorial_function(n) = n <= 1 ? 1 : n * (@__FUNCTION__)(n - 1)
@test factorial_function(5) == 120

# Fibonacci with @__FUNCTION__
struct RecursiveCallableStruct; end
@eval (::RecursiveCallableStruct)(n) = n <= 1 ? n : (@__FUNCTION__)(n-1) + (@__FUNCTION__)(n-2)
@test RecursiveCallableStruct()(10) === 55

# Anonymous function recursion
@test (n -> n <= 1 ? 1 : n * (@__FUNCTION__)(n - 1))(5) == 120
end

@testset "Closures and nested functions" begin
# Prevents boxed closures
function make_closure()
fib(n) = n <= 1 ? 1 : (@__FUNCTION__)(n - 1) + (@__FUNCTION__)(n - 2)
return fib
end
Test.@inferred make_closure()
closure = make_closure()
@test closure(5) == 8
Test.@inferred closure(5)

# Complex closure of closures
function f1()
function f2()
function f3()
return @__FUNCTION__
end
return (@__FUNCTION__), f3()
end
return (@__FUNCTION__), f2()...
end
Test.@inferred f1()
@test f1()[1] === f1
@test f1()[2] !== f1
@test f1()[3] !== f1
@test f1()[3]() === f1()[3]
@test f1()[2]()[2]() === f1()[3]
end

@testset "Do blocks" begin
function test_do_block()
result = map([1, 2, 3]) do x
return (@__FUNCTION__, x)
end
# All should refer to the same do-block function
@test all(r -> r[1] === result[1][1], result)
# Values should be different
@test [r[2] for r in result] == [1, 2, 3]
# It should be different than `test_do_block`
@test result[1][1] !== test_do_block
end
test_do_block()
end

@testset "Keyword arguments" begin
# @__FUNCTION__ with kwargs
foo(; n) = n <= 1 ? 1 : n * (@__FUNCTION__)(; n = n - 1)
@test foo(n = 5) == 120
end

@testset "Callable structs" begin
# @__FUNCTION__ in callable structs
@gensym A
@eval module $A
import ..@__FUNCTION__

struct CallableStruct{T}; val::T; end
(c::CallableStruct)() = @__FUNCTION__
end
@eval using .$A: CallableStruct
c = CallableStruct(5)
if isdefined(Base, Symbol("@__FUNCTION__"))
@test c() === c
else
@test_throws ArgumentError("`Compat.@__FUNCTION__` is not available in this context") c()
end

# In closures, var"#self#" should refer to the enclosing function,
# NOT the enclosing struct instance
struct CallableStruct2; end
@eval function (obj::CallableStruct2)()
function inner_func()
@__FUNCTION__
end
inner_func
end

let cs = CallableStruct2()
@test cs()() === cs()
@test cs()() !== cs
end

# Accessing values via self-reference
struct CallableStruct3
value::Int
end
@eval (obj::CallableStruct3)() = @__FUNCTION__
@eval (obj::CallableStruct3)(x) = (@__FUNCTION__).value + x

let cs = CallableStruct3(42)
if isdefined(Base, Symbol("@__FUNCTION__"))
@test cs() === cs
@test cs(10) === 52
else
@test_throws ArgumentError("`Compat.@__FUNCTION__` is not available in this context") cs()
end
end

# Callable struct with args and kwargs
struct CallableStruct4
end
@eval function (obj::CallableStruct4)(x, args...; y=2, kws...)
return (; func=(@__FUNCTION__), x, args, y, kws)
end
c = CallableStruct4()
if isdefined(Base, Symbol("@__FUNCTION__"))
@test c(1).func === c
@test c(2, 3).args == (3,)
@test c(2; y=4).y == 4
@test c(2; y=4, a=5, b=6, c=7).kws[:c] == 7
else
@test_throws ArgumentError("`Compat.@__FUNCTION__` is not available in this context") c(1)
end
end

@testset "Special cases" begin
# Generated functions
let @generated foo2() = :(@__FUNCTION__)
@test foo2() === foo2
end

# Struct constructors
let
@eval struct Cols{T<:Tuple}
cols::T
operator
Cols(args...; operator=union) = (new{typeof(args)}(args, operator); string(@__FUNCTION__))
end
if isdefined(Base, Symbol("@__FUNCTION__"))
@test occursin("Cols", Cols(1, 2, 3))
elseif VERSION > v"1.9.0-"
@test_throws ArgumentError("`Compat.@__FUNCTION__` is not available in this context") Cols(1, 2, 3)
end
end

# Should not access arg-map for local variables
@gensym f
@eval begin
function $f end
function ($f::typeof($f))()
$f = 1
@__FUNCTION__
end
end
if isdefined(Base, Symbol("@__FUNCTION__"))
@test @eval($f() === $f)
else
@test_throws ArgumentError("`Compat.@__FUNCTION__` is not available in this context") @eval($f())
end
end

if isdefined(Base, Symbol("@__FUNCTION__"))
@testset "Error upon misuse" begin
@gensym B
@test_throws(
"\"@__FUNCTION__\" can only be used inside a function",
@eval(module $B; @__FUNCTION__; end)
)

@test_throws(
"\"@__FUNCTION__\" not allowed inside comprehension or generator",
@eval([(@__FUNCTION__) for _ in 1:10])
)
end
end
end
Loading