Extending functionality of an Abstract Type in a new repo

How can I define a new type in a new package that extends an abstract type in an old package such that functions in the old package work when passed objects of the new type?

I have a type AbstractBody in WaterLily and I have a routine measure! that should work for any subtype as long as they implement a few functions (sdf and measure). I define AutoBody<:AbstractBody within the same package, then define sdf and measure for that type, and everything works fine.

But I want to define other special-purpose types like a ParametricBody<:WaterLily.AbstractBody in their own packages. I’ve defined the sdf and measure functions, but WaterLily still can’t use them. For example:

using WaterLily,ParametricBodies,StaticArrays
R = 6
surf(θ,t) = R*SA[cos(θ+t),sin(θ+t)]
locate(x::SVector{2},t) = atan(x[2],x[1])-t
body = ParametricBody(surf,locate)
sdf(body,SA[1.,1.],0.) # no problem, returns -4.58
Simulation((8,8),(1,0),R;body) # throws error

The sdf functions works fine in the REPL, but the Simulation doesn’t know how to deal with a ParametricBody!

MethodError: no method matching sdf(::ParametricBody{typeof(surf), typeof(locate), ParametricBodies.var"#12#13", Float64}, ::SVector{2, Float64}, ::Int64)
Closest candidates are: sdf(::AutoBody, ::Any, ::Any) 

It must be possible to extend Abstract types like this… What am I doing wrong?

I think it’s a problem in the method definition: you need to tell Julia that these functions were named in another package, using an explicit prefix.
So in your new package ParametricBodies.jl, instead of defining

function sdf(pb::ParametricBody)

you should do

using WaterLily
function WaterLily.sdf(pb::ParametricBody)

or another version which I like less

import WaterLily: sdf
function sdf(pb::ParametricBody)
2 Likes

Thanks. That’s changed the error, but it’s still there. Now it seems like the new measure function can’t find the new sdf. Is there documentation or examples from other repos for this I can look at?

Did you do the same with measure?

Yes, I did the same for both - but the compiler says sdf is undefined. Perhaps the problem is the way I defined the original sdf

"""
    d = sdf(body::AutoBody,x,t) = body.sdf(x,t)
"""
sdf(body::AutoBody,x,t) = body.sdf(x,t)

because when I check in the REPL, the function doesn’t appear.

> using WaterLily
help?> WaterLily.sdf
  No documentation found.

  Binding WaterLily.sdf does not exist.

Could the compiler be inlining the function, so I can’t extend it?

Are your modules short enough, or could be made into a short enough MWE that replicates the issue, to post here? gdalle’s suggestions are good so it’s likely there’s some other issue that requires a proper look.

Inlining is an optimization, those do not interfere with language features.

1 Like

WaterLily isn’t short enough for a MWE. However, it does seem like the problem was that sdf wasn’t available for being redefined. I just started a new branch fix_sdf and made two changes. I exported sdf and I changed the definition of sdf to:

function sdf(body::AutoBody,x,t) body.sdf(x,t) end

and now everything is running without throwing errors.

help?> sdf
search: sdf isdefined @isdefined ComposedFunction symdiff setdiff symdiff! setdiff! searchsortedfirst

  d = sdf(body::AutoBody,x,t) = body.sdf(x,t)

julia> sdf(body,SA[1.,1.],0.)  # still working
-4.585786437626905
julia> Simulation((8,8),(1,0),R;body); # no error

So it seems like the inline-able version of sdf was problem. But maybe not for the reason I think…

1 Like

Not exporting sdf would explain it, your import statement wasn’t importing anything by name explicitly so it would only grab what’s in your export statement.

That’s not a change at all, it’s alternate syntax for the same function you posted before. f(x) = x syntax is a one-line assignment form, it has nothing to do with whether a function call is inlined during compilation. The macros @inline and @noinline are used to suggest to the compiler whether a method or a call should be inlined or not, but the compiler can choose differently if it’s unfeasible e.g. a dynamically dispatched function call cannot be inlined.

3 Likes

Got it. Thanks!

So, to summarize: You need to export func in the old module, then import OldMod: func and define the new version and export func in the new module. Correct?

1 Like

It’s clearer with using in my opinion, cause when you extend a function from the old module you need to prefix it with OldModule

using is the import statement that works with export statements, not import. You don’t need to export a function in a module order to extend the function in that module. Exports only specify what names using will make available, they do not do anything like passing methods.

1 Like

So the only thing I need to do is import OldMod: AbstractType, func and then I can define the new type and func in the new module and it works.

Personally, the import version seems much more direct, since it doesn’t rely on things being exported before they can be extended. (Refer to confusion starting 18h ago.)

In fact, you do not even need to do import OldMod: AbstractType, func, you can just do import OldMod, the disadvantage is that you need to refer to AbstractType and func as OldMod.AbstractType and OldMod.func in every reference to them in your code.

3 Likes