Provide an implementation of f *only* if g is available

Hi,

The interface to my package includes two functions, f, and g that can be implemented by users for their types. In the package, f and g are empty generic functions, so MethodErrors will be thrown if they are not implemented for the user’s type. f is part of the core interface, and, if f is implemented, there is an easy way to construct g:

function g(x)
    y = f(x)
    return modify(y)
end

However, some functionality of the package only requires g, and f is essentially impossible to implement for some user types while g (skipping the intermediate y) is easy, so I want the user to only have to implement g if they only want to use that part of the package. So my question is is there a way to provide a default implementation of g using fonly when f is available?

If you want details, the actual use case is POMDPs.jl and GenerativeModels.jl where f is transition and g is generate_s.

What I have tried so far

I have tried using generated functions and SimpleTraits.jl, but they both have the side effect of making julia think that there is a method of g for all types.

What I would like to do is this

@generated function g(x)
    if method_exists(f, Tuple{x})
        return quote
            y = f(x)
            return modify(y)
        end
    else
        # don't return anything and leave g without a method for typeof(x)
    end
end

The reason I don’t want Julia to think there are methods available is because I want to be able to use method_exists to print out a nice table of the functions that the users still need to implement to use the package.

The users of my package are usually non-CS engineers new to Julia, are not very experienced with object-oriented programming, and I’m trying to show them how powerful the language is in addition to introducing them to the package, so I’m trying to keep things as simple as possible.

Why can’t you simply implement

function f(args...)
    # analyse the types in `args...`
    error("""
         `f(::T1, ::T2, ...)` is not implemented.
          . . .  some instructions on what to do . . . 
      """)
end

Any new implementation of g (or f) with more specific types will anyhow take precedence?

To build on cortner’s answer, in addition to providing the runtime failure case with an informative error (rather than MethodError), you could use which() != the_error_method instead of method_exists to build the table of which methods still need to be overridden.

@jameson, thanks I was literally just typing ?which to try to figure out how to do that, haha

@cortner, thanks for the reply! That’s actually what we have been doing. There are a few problems with this

  1. It suppresses or messes up the very useful MethodErrors that give similar methods for debugging. Of course we can throw one, but then the method that throws the error is included in the list of similar methods
  2. There is now a method of f for the types so method_exists returns true even though the user has not implemented it.
  3. It is not so easy to give instructions on what to do. As I said above, f is nearly impossible to implement in some cases. If I just say implement f, the user will likely decide that this package is not suitable for him and miss out on the package.

@jameson’s reply below addresses 2, so maybe I can get it to work

On the other hand, you can potentially directly throw a useful error message (“your solver T doesn’t support method f”), rather than falling back on the MethodError heuristics.

@jameson, what do you think is the most robust way to see if the method returned by which() is my default definition? Say m is the method returned by which(). Do you think I should look at

  1. m.module (the default implementation is the only one in the module)
    or
  2. functionloc(m)
    or
  3. compare to the actual default implementation method with !=

In case 3 I’m not sure how I would get a reference to the default implementation method.

For 3, you could look up the default method by again using which (and probably cache it somewhere). Option 1 sounds simplest though.