Dispatch and junk values

I have the following situation, where I have a function, where depending on the types of the supplied objects, I may or may not use all arguments. I then wish to define a shortcut function for some types of the supplied objects, which automatically fills the unused arguments by junk-values, so I can still make use of multiple dispatch.

I’ve made a slightly weird MWE, but I hope it clearly conveys what exactly I’m after. I’m also certain that this situation should perhaps typically be avoided, but in the specific situation I’m using, I think it is reasonable to do (at least for quick diagnostic purposes)

abstract type Foo end
struct Foo1 <: Foo end
struct Foo2 <: Foo end

abstract type Bar end
struct Bar1 <: Bar end
struct Bar2 <: Bar end

f(a::Foo1, b::Bar) = magic1(a, b) # Some function that crucially uses 'a' and 'b'
f(a::Foo2, b::Bar) = magic2(a) # Some function that only uses 'a', but which may be so complicated that it is easy to miss a typo, which means that 'b' is erroneously used
g(a::Foo2) = f(a, Bar1()) # If 'a' is of type 'Foo2', we know that 'f' won't use 'b', so we may supply any value here. 

Now I know that I will only be using the function g on objects of type Foo2, but as in my use-case the underlying types and functions are a bit complicated, I want to rule out that I make a mistake and Bar1() actually gets used.

I think my question is therefore: is there some way to tell Julia to pretend we’re using f with objects of type Foo2 and Bar for the purpose of dispatching f, but you must not use the actual supplied value for Bar at any point? Something like Junk(Bar), which creates an object with type Bar but that cannot be referenced?

I’m well aware that this can be achieved in some sense by defining

struct JunkBar <: Bar end

and using

g(a::Foo2) = f(a, JunkBar())

but this means I’ll (potentially) need to add a lot of such concrete types, which feels a bit wasteful.

You might want to restructure your computation to look something like this

# fallback
get_y(a::Foo, b::Bar) = a.y
# implement this for each concrete type <: Bar which has a field .y
get_y(a::Foo, b::Bar1) = b.y

f(a::Foo, b::Bar) = a.x + get_y(a, b)
1 Like

For the specific MWE, that would be correct. In my case, the computations are however a bit harder and closer to:

f(a::Foo1, b::Bar1) = magic1(a, b)
f(a::Foo2, b::Bar) = magic2(a)

Why not use y = get_y(a,b) inside magic1 and replace instances of b.y with y there?


I must admit I don’t understand the actual problem.
Bar1 is a concrete type which we know has a field y, so the solution is clear no?

1 Like

I see that my original MWE may be a bit misleading, let me update it (I’ve also updated the OP):

abstract type Foo end
struct Foo1 <: Foo end
struct Foo2 <: Foo end

abstract type Bar end
struct Bar1 <: Bar end
struct Bar2 <: Bar end

f(a::Foo1, b::Bar) = magic1(a, b) # Some function that crucially uses 'a' and 'b'
f(a::Foo2, b::Bar) = magic2(a) # Some function that only uses 'a', but which may be so complicated that it is easy to miss a typo, which means that 'b' is erroneously used
g(a::Foo2) = f(a, Bar1()) # If 'a' is of type 'Foo2', we know that 'f' won't use 'b', so we may supply any value here. 

I’m also lost but isn’t the fact that you don’t pass b to magic2 enough to guarantee that magic2 won’t use b?

But if you mean that magic2(a) rather is a shorthand for a direct implementation of a complicated function you can just skip naming the Bar argument:

function f(a::Foo2, ::Bar)
    # complicated stuff
end
1 Like

I see. Some ideas:

  • You can either use the JunkBar <: Bar like you already suggested.
  • Alternatively declare f with Union{Nothing,<:Bar} and pass along nothing in g. If you have many types for which this is needed then you can declare a shorthand const Maybe{T} = Union{Nothing,T} and write it as f(::Foo1, ::Maybe{<:Bar}) etc.
  • Or just use the fact that magic2 does not depend on b and separate dispatch and implementation by directly calling magic2 instead of f from g.

However, I must say from my personal experience, its often simpler to just repeat some simple pattern and stick with it, in particular when using a more complicated solution only safes you about 5 min of changing your code, but is more difficult to reason about. Just my 2 cents.

1 Like

Yes, magic2(a) is shorthand for a direct implementation of a complicated function. Not naming the Bar argument seems like a good way to ascertain this, that at least fixes my safety concerns.

However, I’d still need to pass some concrete instance of a subtype of Bar to use the function, right? I’d like to instead write something like Junk(Bar)::Bar and dispatch using that, if that is possible?

I see, thank you! I agree that the easy copy-paste solutions are usually fine for this kind of stuff, but it felt like a problem that might be sufficiently common to have its own solution.

I’m a bit wary of using the Union solution for legibility purposes, but I agree that sticking with JunkBar is probably sufficient!
Thank you both for your suggestions!

There is no mechanism in Julia to conjure up an unspecified instance of an abstract type but you can of course define an empty subtype yourself, as already noted, and use that:

abstract type Bar end
struct JunkBar <: Bar end
Junk(::Type{Bar}) = JunkBar()

If you truly have lots of such types so it becomes an annoying amount of boilerplate you could define a macro like

@junkable abstract type Bar end

which expands to the three lines above.

1 Like