Syntax Idea, automated argument composition

Hi, I was wondering about a possible syntax extension “automated argument composition” and if something like this already exists in Julia’s dispatch? The closest I could find is function composition and negation but it’s not quite the same:

I feel there is maybe some headroom in the function dispatch syntax to perhaps support a useful automated argument conversion:

MWE

These are not default arguments, but auto-composition analogous to

f(x::Int=g(x::Symbol), args...)

producing both the “direct” f(::Int, args...) and “derived” forms f(::Symbol, args...) from dispatch, but only the argument x::Int exists in the local scope of f.

Proposed Usage Example II

Very similar functions but with a “direct” and “derived” argument list, where the convenience
conversion occurs via a (small) helper function.

# as example a simple bridge table...
bridgetable = Dict{Symbol, Int}(:x1=>13, :p56=>402)

helper = (s::Symbol)->bridgetable[s]::Int

# enhanced logical function (with auto-argument-composition)
funtion doSomething!(container::T, id::Int=helper(sym::Symbol), data::AbstractVector; kargs...) where T
  container[id] # do stuff...
end

# The user can now simply call any one of at least two auto-composed functions
doSomething!(mycontainer, :x1, rand(5)) # should work just fine but was auto-generated
doSomething!(mycontainer, 13, rand(5)) # equivalent call but is the "direct" format

Current Usage Requires

# the logical function
function doSomething!(container::T, id::Int, data; kargs...) where T
  container[id] # do stuff...
end

# the derived function API which user currently has to write themselves.
doSomething!(container::T, sym::Symbol, data; kwargs...) where T = doSomething!(container, helper(sym), data, kwargs...)

Error Handling

Errors in the following case would produce the output

# error on:
doSomething!(mycontainer, rand(5))
## Undef method error since doSomething!(::T, !!!, ::Vector) does not exist.
# either `id::Int` or `sym::Symbol` are not defined

This seems like quite a predictable dispatch case…

Another Example III

This is closer to my own use-case

using IncrementalInference

# this is essentially a LightGraphs.jl object
fg = generateCanonicalFG_lineStep(5)
doSomething!(vari::DFGVariable, data) = # do something with vari
doSomething!(fg::AbstractDFG, sym::Symbol, data) = doSomething!(getVariable(fg, sym), data)

So I find a lot of repetition which cannot really be avoided unless some kind of automated argument composition could occur during dispatch. Would be great if one could instead just write:

doSomething!(vari::DFGVariable=getVariable(fg::AbstractDFG, sym::Symbol), data) = # do somethng with vari

Again, only vari is visible inside the auto-composed function, so it does not change the body of a function in any way.

Doesn’t this just punt the problem one step further? You would still need to define methods for g(x) that convert all possible types. It’s the same number of signatures ultimately.

Consider:

f(x::Int) = do_something(x)
f(x) = f(g(x))

g(x::Int) = x
g(x::AbstractFloat) = round(Int, x)
# etc...

There’s no getting around the need to write methods that deal with getting x to be the type that matches your “main” signature. Creating a syntax for f(x::Int = g(x)) really only saves you the trouble of writing f(x) = f(g(x)), not the rest of it (the “real” hassle).

That said, you could surely write a macro to do exactly what you want, I’m just not sure it would be worth the trouble…

2 Likes

Agreed that g(x) still needs to be written by the user. I figured the effort saving would occur each time a user needs to write a macro for a particular dispatch case. There is also some combinatorics when multiple arguments to a “primary/direct” method have this structure – for which various conversions / promotions / lookups are required as “derived composition wrapper” methods that funnel towards the “primary/direct” method implementation.

I guess the punchline for me is that ergonomics of a human friendly API (or SDK) can leverage the type information in a way that expands the expressiveness of some package API – e.g. humans are less precise in their description of something but in combination with the type information the identification of an object or method becomes unique.

Example III above is a simplistic case we encounter quite often, but across our project we are defining 10’s and expect possibly 100’s of wrapper methods (as described in the reply). If something like the proposed “default composition” behavior were nearer to Base that may be worth the hassle. Perhaps other folks have interest in this as time goes on. In the mean time, we are likely to use multiple macros and will post again if a clearer example comes up. Thanks for the reply!

Sorry, but I remain unconvinced that the proposal actually solves this… Would you do:

doSomething!(vari::DFGVariable = getVariable(fg::AbstractDFG, sym::Symbol), data) = # ...

and this would parse into the following?

doSomething!(vari::DFGVariable, data) =  # ...
doSomething(fg::AbstractDFG, sym::Symbol data) = doSomething!(getVariable(fg, sym), data)

You can design a macro to do this if you really want it. It would parse the function definition and look for equalities (avoiding positional and keyword arguments), extract type parameters, and so on. Probably pretty tedious to design but far from impossible.
But for all this work and a much less readable function signature, you only get the effect of one additional method. Consider the following design pattern

# This is the "main" method. T1,2,3 are the exact types you 
# want this function to have. Keep in mind if the function 
# doesn't *need* to be strictly  typed, it *shouldn't* be 
foo(x::T1, y::T2, z::T3) = # do something. 

# convert, promote, _my_private_convert, etc.
# whatever makes the most sense to get what you want
foo(x, y, z) = foo(_convert(T1, x), _convert(T2, y), _convert(T3, z))

## User side:
# To get whatever new method the user wants, they
# can just overload `_my_private_convert`, or whatever 
# you used (careful not to commit type piracy here...)
SomeLib._convert(T1, (fg, s)::Tuple{<:AbstractDFG, Symbol}) = getVariable(fg, s)

## now the user has enabled this signature:
foo((fg, :x), y, z)

Keep in mind that automating the above with a macro is also much easier to do! All you would need to do is duplicate the method signature, applying _convert to each typed argument.

1 Like