diff --git a/NEWS.md b/NEWS.md index 10e0ac7aed1cc..20a995165cc7e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,6 +3,8 @@ Julia v1.7 Release Notes New language features --------------------- +* An underscore `_` as a function argument, e.g. `f(_, y)`, is now shorthand + for the "curried" anonymous function `x -> f(x, y)` ([#24990]). Language changes ---------------- diff --git a/doc/src/manual/functions.md b/doc/src/manual/functions.md index 6dcec615d2994..d30fa6394efc4 100644 --- a/doc/src/manual/functions.md +++ b/doc/src/manual/functions.md @@ -285,6 +285,35 @@ get(()->time(), dict, key) The call to [`time`](@ref) is delayed by wrapping it in a 0-argument anonymous function that is called only when the requested key is absent from `dict`. +As a shorthand to create simple anonymous functions from other functions, if you pass an underscore +`_` as a function *argument* it automatically constructs an anonymous function where `_` is +the *parameter*. For example, the expression `f(_,y)` is equivalent to `x -> f(x,y)`. This +includes infix functions like `_ + 1` (equivalent to `x -> x + 1`), indexing +([`getindex`](@ref)) `_[i]` (equivalent to `x -> x[i]`), and +field access `_.a` (equivalent to `x -> x.a`). (This is called [partial application](https://en.wikipedia.org/wiki/Partial_application) of a function, and +is sometimes informally referred to by the related term "[currying](https://en.wikipedia.org/wiki/Currying)".) +For example, the following code averages the second element of each array in a collection, by +passing the anonymous function `_[2]` (equivalent to `x -> x[2]`) as the first argument +to [`sum`](@ref): + +```jldoctest +julia> sum(_[2], [ [1,3,4], [1,2,5], [3,1,2], [4,4,4] ]) +10 +``` + +More generally, if `_` is passed multiple times to the *same* function call, +then each appearance of `_` is converted into a *different* argument of the +anonymous function, in the order they appear. For example, `f(_,y,_)` is +equivalent to `(x,z) -> f(x,y,z)`. + +The `_` construction only applies to a *single* function call, +not to nested function calls: `f(g(_))` is equivalent to `f(x -> g(x))`, not +to `x -> f(g(x))`. For example, the expression `2*_ + 1`, or +equivalently the nested call `(+)((*)(2,_), 1)`, only converts `2*_` +into a function, so the whole expression becomes `(x->2*x) + 1`, which +will give an error because no method is defined to add `+ 1` to a function. +Similarly, `f(g(_),_)` is converted into `y -> f(x -> g(x), y)`. + ## Tuples Julia has a built-in data structure called a *tuple* that is closely related to function diff --git a/src/julia-syntax.scm b/src/julia-syntax.scm index b9043f11b8b94..54cfc596a55f3 100644 --- a/src/julia-syntax.scm +++ b/src/julia-syntax.scm @@ -1826,6 +1826,19 @@ (call (top broadcasted) (top identity) ,e))))))) +; Convert f(_,y) into x -> f(x,y) etcetera. That is, an _ in e is changed into the +; argument of an anonymous function. Multiple underscores are turned into +; multiple anonymous-function args. +(define (curry-underscore e) + (expand-forms + (let* ((args '()) ; n-arg case just becomes anon func + (enew (map (lambda (y) (if (eq? '_ y) + (let ((x (gensy))) + (set! args (cons x args)) + x) + y)) e))) + `(-> (tuple ,@(reverse args)) ,enew)))) + (define (expand-where body var) (let* ((bounds (analyze-typevar var)) (v (car bounds))) @@ -2227,6 +2240,8 @@ ;; "(.op)(...)" ((and (length= f 2) (eq? (car f) '|.|)) (expand-fuse-broadcast '() `(|.| ,(cadr f) (tuple ,@(cddr e))))) + ((memq '_ (cddr e)) + (curry-underscore e)) ((eq? f 'ccall) (if (not (length> e 4)) (error "too few arguments to ccall")) (let* ((cconv (cadddr e)) @@ -2265,12 +2280,15 @@ (let ((x (car a))) (if (and (length= x 2) (eq? (car x) '...)) - (if (null? run) - (list* (cadr x) - (tuple-wrap (cdr a) '())) - (list* `(call (core tuple) ,.(reverse run)) - (cadr x) - (tuple-wrap (cdr a) '()))) + (begin + (if (eq? (cadr x) '_) ; _... is not currently allowed (meaning TBD) + (error (string "invalid underscore argument \"" (deparse x) "\""))) + (if (null? run) + (list* (cadr x) + (tuple-wrap (cdr a) '())) + (list* `(call (core tuple) ,.(reverse run)) + (cadr x) + (tuple-wrap (cdr a) '())))) (tuple-wrap (cdr a) (cons x run)))))) (expand-forms `(call (core _apply_iterate) (top iterate) ,f ,@(tuple-wrap argl '()))))) diff --git a/test/syntax.jl b/test/syntax.jl index 085794b72d859..9ad80889d1671 100644 --- a/test/syntax.jl +++ b/test/syntax.jl @@ -1226,6 +1226,23 @@ end # issue #9972 @test Meta.lower(@__MODULE__, :(f(;3))) == Expr(:error, "invalid keyword argument syntax \"3\"") +@testset "underscore currying" begin + @test div(_, 3)(13) === 4 + @test (_+1)(3) === 4 + @test (_.re)(5+6im) === 5 + @test (_[2])([7,8,9]) === 8 + @test_broken div.(10,_)([1,2,3,4]) == [10,5,3,2] + @test_broken (_ .+ 1)([1,2,3,4]) == [2,3,4,5] + @test (_ // _)(3,4) === 3//4 + let _round(x,d; kws...) = round(x; digits=d, kws...) # test a 2-arg func with keywords + @test _round(_, 2, base=10)(pi) == 3.14 + @test _round(_, 2, base=2)(pi) === _round(_, _, base=2)(pi, 2) == 3.25 + end + @test split(_)("a b") == ["a","b"] + @test split(_, limit=2)("a b c") == ["a","b c"] + @test Meta.lower(@__MODULE__, :(f(_...))) == Expr(:error, "invalid underscore argument \"_...\"") +end + # issue #25055, make sure quote makes new Exprs function f25055() x = quote end