Skip to content
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

RFC: curry underscore arguments to create anonymous functions #24990

Open
wants to merge 3 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: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------------
Expand Down
29 changes: 29 additions & 0 deletions doc/src/manual/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 24 additions & 6 deletions src/julia-syntax.scm
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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 '())))))
Expand Down
17 changes: 17 additions & 0 deletions test/syntax.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading