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: required keyword arguments #25830

Merged
merged 5 commits into from Feb 2, 2018
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion NEWS.md
Expand Up @@ -44,6 +44,9 @@ New language features
* Values for `Enum`s can now be specified inside of a `begin` block when using the
`@enum` macro ([#25424]).

* Keyword arguments can be required: if a default value is omitted, then an
exception is thrown if the caller does not assign the keyword a value ([#25830]).

Language changes
----------------

Expand Down Expand Up @@ -1260,4 +1263,4 @@ Command-line option changes
[#25622]: https://github.com/JuliaLang/julia/issues/25622
[#25634]: https://github.com/JuliaLang/julia/issues/25634
[#25654]: https://github.com/JuliaLang/julia/issues/25654
[#25655]: https://github.com/JuliaLang/julia/issues/25655
[#25655]: https://github.com/JuliaLang/julia/issues/25655
4 changes: 4 additions & 0 deletions base/boot.jl
Expand Up @@ -139,6 +139,7 @@ export
InterruptException, InexactError, OutOfMemoryError, ReadOnlyMemoryError,
OverflowError, StackOverflowError, SegmentationFault, UndefRefError, UndefVarError,
TypeError, ArgumentError, MethodError, AssertionError, LoadError, InitError,
UnassignedKeyword,
# AST representation
Expr, GotoNode, LabelNode, LineNumberNode, QuoteNode,
GlobalRef, NewvarNode, SSAValue, Slot, SlotNumber, TypedSlot,
Expand Down Expand Up @@ -253,6 +254,9 @@ end
struct ArgumentError <: Exception
msg::AbstractString
end
struct UnassignedKeyword <: Exception
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

Should this end with Error or Exception for consistency with most (all) other errors?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, I can do that. UnassignedKeywordError is a bit long. Maybe UndefKeywordError?

Copy link
Member

Choose a reason for hiding this comment

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

KeywordError? Or KeywordException?

Copy link
Member Author

Choose a reason for hiding this comment

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

We also have UndefVarError and UndefRefError, so UndefKeywordError seems consistent. KeywordError seems too vague, since there are other ways you could get errors from keyword args.

Copy link
Member

Choose a reason for hiding this comment

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

But UndefKeywordError feels a bit like something that would be thrown for cases like

foo(;a = 1) = a
foo(b=2)

Copy link
Sponsor Member

Choose a reason for hiding this comment

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

I would consider that an unexpected keyword argument error. "undef" is pretty consistently used to mean something that is used as a value without having been given one.

var::Symbol
end

struct MethodError <: Exception
f
Expand Down
7 changes: 7 additions & 0 deletions base/docs/basedocs.jl
Expand Up @@ -1019,6 +1019,13 @@ A symbol in the current scope is not defined.
"""
UndefVarError

"""
UnassignedKeyword(var::Symbol)

The required keyword argument `var` was not assigned in a function call.
"""
UnassignedKeyword

"""
OverflowError(msg)

Expand Down
3 changes: 3 additions & 0 deletions base/errorshow.jl
Expand Up @@ -134,6 +134,9 @@ showerror(io::IO, ex::ArgumentError) = print(io, "ArgumentError: $(ex.msg)")
showerror(io::IO, ex::AssertionError) = print(io, "AssertionError: $(ex.msg)")
showerror(io::IO, ex::OverflowError) = print(io, "OverflowError: $(ex.msg)")

showerror(io::IO, ex::UnassignedKeyword) =
print(io, "UnassignedKeyword: keyword argument $(ex.var) not assigned")

function showerror(io::IO, ex::UndefVarError)
if ex.var in [:UTF16String, :UTF32String, :WString, :utf16, :utf32, :wstring, :RepString]
return showerror(io, ErrorException("""
Expand Down
1 change: 1 addition & 0 deletions doc/src/base/base.md
Expand Up @@ -298,6 +298,7 @@ Base.ParseError
Core.StackOverflowError
Base.SystemError
Core.TypeError
Core.UnassignedKeyword
Core.UndefRefError
Core.UndefVarError
Base.InitError
Expand Down
11 changes: 11 additions & 0 deletions doc/src/manual/functions.md
Expand Up @@ -518,6 +518,17 @@ function f(x; y=0, kwargs...)
end
```

If a keyword argument is not assigned a default value in the method definition,
then it is *required*: an [`UnassignedKeyword`](@ref) exception will be thrown
if the caller does not assign it a value:
```julia
function f(x; y)
###
end
f(3, y=5) # ok, y is assigned
f(3) # throws UnassignedKeyword(:y)
```

Inside `f`, `kwargs` will be a named tuple. Named tuples (as well as dictionaries) can be passed as
keyword arguments using a semicolon in a call, e.g. `f(x, z=1; kwargs...)`.

Expand Down
24 changes: 17 additions & 7 deletions src/julia-syntax.scm
Expand Up @@ -644,12 +644,22 @@
(if (pair? invalid)
(if (and (pair? (car invalid)) (eq? 'parameters (caar invalid)))
(error "more than one semicolon in argument list")
(cond ((symbol? (car invalid))
(error (string "keyword argument \"" (car invalid) "\" needs a default value")))
(else
(error (string "invalid keyword argument syntax \""
(deparse (car invalid))
"\" (expected assignment)"))))))))
(error (string "invalid keyword argument syntax \""
(deparse (car invalid)) "\""))))))

; replace unassigned kw args with assignment to throw() call (forcing the caller to assign the keyword)
(define (throw-unassigned-kw-args argl)
(define (throw-unassigned argname)
`(call (core throw) (call (core UnassignedKeyword) (inert ,argname))))
(if (has-parameters? argl)
(cons (cons 'parameters
(map (lambda (x)
(cond ((symbol? x) `(kw ,x ,(throw-unassigned x)))
((decl? x) `(kw ,x ,(throw-unassigned (cadr x))))
(else x)))
(cdar argl)))
(cdr argl))
argl))

;; method-def-expr checks for keyword arguments, and if there are any, calls
;; keywords-method-def-expr to expand the definition into several method
Expand All @@ -658,7 +668,7 @@
;; which handles optional positional arguments by adding the needed small
;; boilerplate definitions.
(define (method-def-expr name sparams argl body rett)
(let ((argl (remove-empty-parameters argl)))
(let ((argl (throw-unassigned-kw-args (remove-empty-parameters argl))))
(if (has-parameters? argl)
;; has keywords
(begin (check-kw-args (cdar argl))
Expand Down
12 changes: 12 additions & 0 deletions test/keywordargs.jl
Expand Up @@ -308,3 +308,15 @@ end
((1, 3, 5, 6, 7),
(a = 2, b = 4, c = 8, d = 9, f = 10))
end

@testset "required keyword arguments" begin
f(x; y, z=3) = x + 2y + 3z
@test f(1, y=2) === 14 === f(10, y=2, z=0)
@test_throws UnassignedKeyword f(1)
@test_throws UnassignedKeyword f(1, z=2)
g(x; y::Int, z=3) = x + 2y + 3z
@test g(1, y=2) === 14 === g(10, y=2, z=0)
@test_throws TypeError g(1, y=2.3)
@test_throws UnassignedKeyword g(1)
@test_throws UnassignedKeyword g(1, z=2)
end