Skip to content

Commit

Permalink
RFC: required keyword arguments (#25830)
Browse files Browse the repository at this point in the history
* required keyword arguments
  • Loading branch information
stevengj committed Feb 2, 2018
1 parent c6f056b commit 6eb7f3d
Show file tree
Hide file tree
Showing 9 changed files with 58 additions and 11 deletions.
3 changes: 3 additions & 0 deletions 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
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,
UndefKeywordError,
# 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 UndefKeywordError <: Exception
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

"""
UndefKeywordError(var::Symbol)
The required keyword argument `var` was not assigned in a function call.
"""
UndefKeywordError

"""
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::UndefKeywordError) =
print(io, "UndefKeywordError: 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.UndefKeywordError
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 [`UndefKeywordError`](@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 UndefKeywordError(: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 UndefKeywordError) (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 UndefKeywordError f(1)
@test_throws UndefKeywordError 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 UndefKeywordError g(1)
@test_throws UndefKeywordError g(1, z=2)
end
4 changes: 0 additions & 4 deletions test/syntax.jl
Expand Up @@ -500,10 +500,6 @@ let m_error, error_out, filename = Base.source_path()
error_out = sprint(showerror, m_error)
@test startswith(error_out, "ArgumentError: invalid type for argument number 1 in method definition for method_c6 at $filename:")

m_error = try @eval method_c6(A; B) = 3; catch e; e; end
error_out = sprint(showerror, m_error)
@test error_out == "syntax: keyword argument \"B\" needs a default value"

# issue #20614
m_error = try @eval foo(types::NTuple{N}, values::Vararg{Any,N}, c) where {N} = nothing; catch e; e; end
error_out = sprint(showerror, m_error)
Expand Down

0 comments on commit 6eb7f3d

Please sign in to comment.