I’d like to announce the package ArgumentModes.jl (also available in the registry) which provides type Mode
for usage as a function argument type and in function calls. Conceptually it is similar to Val
type, but working only with symbols. The main advantage of Mode
is that it could contain a set of symbols, and the dispatch process can refer to exact combination of the symbols when the method is being chosen. Moreover, Mode
type of a function argument could be defined to be able to accept only prescribed list of symbols – then the method would be chosen only if a Mode
instance has all or any subset of the prescribed symbols. As an optional feature – each of symbols might also have additional parameters with prescribed types, and the dispatch process considers the types of the parameters’ types.
As I see, the main advantages of using Mode
could be:
- Specialization and dispatch based on the value of a
Mode
instance - in many case no additional code or runtime overhead is needed. - Since the list of allowed symbols for a method is defined directly in a function signature, in most cases the dispatch process would control for typos in symbols names. Moreover, the “Closest candidates are:” section of
MethodError
would contain meaningful description of possible symbols for corresponding methods.
Here an example:
julia> f(x::Real) = println("Number: $x")
f(x::Mode[:iterator => Any]) = println("Values from iterator: $(x[]...)")
f(x::Mode[:fromargs], y...) = println("Fromargs: $(y...)")
function f(x::Union{Int, Mode[:a, :b, :c => Int]})
checkmode(x, :a) do _; println(":a") end
checkmode(x, :c) do c; println(":c = $c") end
checkmode(x, :b) && println(":b")
if x isa Int; println("x = $x") end
end
f (generic function with 4 methods)
julia> methods(f)
# 4 methods for generic function "f":
[1] f(x::Mode[:iterator => Any]) in Main at REPL[34]:1
[2] f(x::Mode[:fromargs], y...) in Main at REPL[36]:1
[3] f(x::Union{Int64, Mode[:c => Int64, :a, :b]}) in Main at REPL[38]:1
[4] f(x::Real) in Main at REPL[33]:1
julia> f(25.0)
Number: 25.0
julia> f(Mode(:iterator)=> 1:5)
Values from iterator: 12345
julia> f(Mode(:fromargs), 1, 2, 3, 4, 5)
Fromargs: 12345
julia> f(1)
x = 1
julia> f(Mode(:a, :c => 125))
:a
:c = 125
julia> f(Mode(:fromargs, :iterator => 1:5), 2)
ERROR: MethodError: no method matching f(::Mode[==, :iterator => UnitRange, :fromargs], ::Int64)
Closest candidates are:
f(::Mode[:fromargs], ::Any...) at REPL[36]:1
Stacktrace:
[1] top-level scope
@ REPL[59]:1
The specialization of Mode
with the list of accepted symbols for function arguments is generated using the call of form Mode[:symbol1, :symbol2, symbol3 => Tuple{Int, AbstractString}, ... ]
. An instance of Mode
for a function call is created with constructor Mode(:symbol1, symbol2, :symbol3 => (1, "Hello"), ... )
. Another form for the instance construction (which is a product of my dislike of the nested parentheses) is Mode(:symbol1) ~ Mode(:symbol2) ~ Mode(:symbol3)=>(1, "Hello") ~ ...
.
The value of an additional parameter for a symbol could be extracted from a Mode
instance as m[:symbol1]
. Or for several symbols (as a tuple): m[:symbol1, :symbol2, ...]
. A call m[]
would test whether the instance m
has exactly one symbol and return the value associated with it.
A function checkmode
is also defined to test a Mode
instance for the symbols. For example:
julia> m = Mode(:a => 1, :b => 2.5, :c)
Mode[==, :b => Float64, :a => Int64, :c]((a = 1, b = 2.5, c = nothing))
julia> checkmode(m, :a)
true
julia> checkmode(m, :d)
false
julia> checkmode(m, |, :a, :d)
true
julia> checkmode(m, &, :a, :d)
false
julia> checkmode(m, ==, :a, :b)
false
julia> checkmode(m, ==, :a, :b, :c)
true
julia> checkmode(m, :a) do a; @show a end
a = 1
1
julia> checkmode(m, :a, :b) do a, b; @show a, b end
(a, b) = (1, 2.5)
(1, 2.5)
julia> checkmode(m, :a, :e) do x, y; @show x, y end
julia> checkmode(12, :a)
false
Please, refer to the in-julia documentation of the type and the function for more detailed information.
Series of tests showed that the current implementation of Mode
fully compiles out when it used for function arguments and method dispatch, so it seems that there is no runtime overhead for using it (at least for the use cases considered in the tests).
I’m not sure if the concept is actually any good, I guess I would know myself only from attempts of using it. As of now I see 2 uses of Mode
:
-
Replacement of using ordinary
Symbol
as a function argument as a flag
parameter. HereMode
allows to explicitly declare a function argument as a
set of symbols (flags) with a distinct list of accepted symbols for each of
a function’s methods. For example,open(f, Mode(:read))
,open(f, Mode(:write))
,
open(f, Mode(:write, :sync))
might correspond (depending on the design)
to 3 different methods. -
The way to explicitly show in which meaning a value to a function argument
is provided. This might be useful when it is not possible to distinct the
meaning of an argument only by its type.For example, suppose a function f processes array-typed objects with
arbitrary dimensions number. We might want to declare methods for both
processing a single object and an iteratable collection of objects. It
would be difficult to distinct the methods using only the type of the
argument sincef([x,y])
could both mean to process a single object[x,y]
or to process two objectsx
andy
. However, usingMode
in the
declaration of method for a collection, the user would be allowed to
explicitly indicate what is passed in the call:f([x,y])
for a single
object andf(Mode(:collection)=> [x, y])
for a collection of objects.