JuliaLang / julia

The Julia Programming Language
https://julialang.org/
MIT License
45.63k stars 5.48k forks source link

types for keyword collections of args #29937

Open StefanKarpinski opened 5 years ago

StefanKarpinski commented 5 years ago

It's not uncommon to want to share a set of keyword arguments across a large number of function signatures. The repetition of the signature is quite annoying; you can use splatting to pass keywords through to a deeper function but that's not idea since you want to check arguments as soon as possible, not deep in the call stack at a function boundary that may not mean much to the user. If everything doesn't get inlined that can also lead to very inefficient splatting and slurping code being generated. At the same time, it's often quite convenient to use a struct type to capture and pass around a bunch of keyword options.

Combining these two trains of thought led me to consider a feature like this, illustrated with an example taken from the open function, which has a fairly fancy set of keyword arguments that it can take which are translated into a structure of boolean flag values. See:

https://github.com/JuliaLang/julia/blob/1f2b16fd964aa03ccd648c3/base/iostream.jl#L214-L291

Instead, we would express this code as follows:

struct OpenFlags
    read     :: Bool
    write    :: Bool
    create   :: Bool
    truncate :: Bool
    append   :: Bool

    function OpenFlags(;
        read     :: Union{Bool, Nothing} = nothing,
        write    :: Union{Bool, Nothing} = nothing,
        create   :: Union{Bool, Nothing} = nothing,
        truncate :: Union{Bool, Nothing} = nothing,
        append   :: Union{Bool, Nothing} = nothing,
    )
        if write === true && read !== true && append !== true
            create   === nothing && (create   = true)
            truncate === nothing && (truncate = true)
        end

        if truncate === true || append === true
            write  === nothing && (write  = true)
            create === nothing && (create = true)
        end

        write    === nothing && (write    = false)
        read     === nothing && (read     = !write)
        create   === nothing && (create   = false)
        truncate === nothing && (truncate = false)
        append   === nothing && (append   = false)

        return new(read, write, create, truncate, append)
    end
end

That just defines an immutable structure for flags to open and similar functions. On the using side you would declare a function like this:

function open(fname::AbstractString; (flags...)::OpenFlags)
    # code that uses `fname::AbstractString` and `flags::OpenFlags`
end

In other words, having (flags...)::OpenFlags means that any keyword arguments to this open method are given to the OpenFlags constructor instead in order to construct an instance which is then available within the function as flags. Later where we have:

https://github.com/JuliaLang/julia/blob/1f2b16fd964aa03ccd648c/base/iostream.jl#L366-L373

We could instead write this instead of slurping and splatting keyword args through a try/catch:

function open(f::Function, args...; (flags...)::OpenFlags)
    io = open(args...; flags) # not sure about calling syntax here, to splat or not to splat...
    try
        f(io)
    finally
        close(io)
    end
end

As an added benefit, this could provide a single centralized place to handle deprecation of keyword arguments: just deprecate a keyword argument in the constructor of the appropriate options structure and every place that uses it will get the deprecation automatically, supporting the old keyword with the appropriate warning.

JeffBezanson commented 5 years ago

io = open(args...; flags) # not sure about calling syntax here, to splat or not to splat...

I think that would have to be a splat.

StefanKarpinski commented 5 years ago

I think that would have to be a splat.

Yes, makes sense. Would we be able to specialize the splatting code on the type and avoid the dynamic overhead since we know what the fields of flags are? Hopefully.