Supertype in Julia

Imagine someone else has implemented something, say a type and a function:

struct Foo
    a::Int
end

function bar(x::Foo)
    return x.a
end

I would like to build on what he has implemented, I would like to use the function bar also for my new type. However I can not create a subtype of Foo because it is not abstract and I don’t know how to supertype Foo without editing his code.
What do I do?

This is not possible. Your only options is to duplicate their code. For this reason, it’s a good idea to write highly generic code, and create abstract types for your custom types, so others can extend them.

3 Likes

More context to extend @jakobnissen’s answer:

Nevertheless, depending on how you want to build on Foo, it might be possible to create other types that, without being subtypes of Foo, compose with it, e.g.:

struct AnotherType
   foo::Foo
   # and other things, maybe
end

bar(x::AnotherType) = bar(x.foo)

If there are other methods that you want to extend, you should repeat this for all of them, though, perhaps using metaprogramming to avoid repeating a lot of code.

Conversion and promotion can also be used to facilitate using your new type in contexts were Foo is used.

5 Likes

I think you could set this up with a bit of a hack even though it’s not formally part of the language itself.

First, note how deepcopy is implemented for all types, without even you having to define deepcopy(x::Foo). How is this done? It recursively goes through all fields of types until it finds a type it actually knows how to copy: julia/deepcopy.jl at 61ccb32b6501a311be84bb86c3a5ffbc993c3be0 · JuliaLang/julia · GitHub.

I think you could do the same thing here. You would define a generic version of bar that recursively walks through all fields of the input type until it finds a type of Foo, then it calls the bar function on that.

1 Like

I realize you want a solution that does not involve editing the original code base, but I think that is the best solution in the case. Adding an abstract type should not introduce a breaking change. Instead it will make the code more extensible.

My best advice is as follow : who is this someone else ? If you can go to them, propose that they change their definition of bar to

function bar(x)
    return x.a
end

So you can use it. If this someone else is an open source contributor or package manager on GitHub, they might do it gladly (or accept your PR if you do it for them). If this someone else is your boss that manages some code in your organisation, then make your case to them, they might listen too.

1 Like

Absolutely, its unlikely a PR adding an abstract type will be rejected, because it’s rarely breaking and can only broaden the user base.

Or even without types at all, why bother with a restriction to an abstract type if you do not need it ?

Well, often you want to add an abstract type layer because methods will be defined on a concrete type for e.g. Base functions or other functions not owned by the package with the type.

If you add an abstract layer (and move the methods to it), then everyone else can also benefit from your methods on external functions without using your exact concrete object.

And then what is the issue ? Until someone calls the method, I think that no overhead comes from a definition without abstract type. I don’t see the problem, could you rephrase ?

When you don’t define an abstract type and define all your methods on a concrete type, it is hard for other developers to extend the behavior of your object because they can’t use its methods, they have to wrap the object and forward methods to it. Which is fine, just clunky.

As an example, my package DimensionalData.jl defines a DimArray <: AbstractDimArray. Nearly all the methods are on AbstractDimArray, so other packages can use all the functionality on their own array types without a layer of indirection - by inheriting from AbstractDimArray. They can just drop some of the fields of a DimArray if they don’t need them, or add their own. Rasters.jl Raster does this, plus a few other packages people have written like AstroImages.jl.

This only needs a few method definitions to work. Most other designs need a lot more method definitions to get DimArray functionality. There are like 50 base methods you would have to forward, a lot of them pretty hard to understand like the broadcast machinery.

1 Like

You could also just construct instances of Foo on the fly. This is cheap if Foo is an immutable struct. For example:

bar(x::AnotherType) = bar(Foo(x.somefield))
1 Like

I was more thinking about defining the functions without any type restriction. So they basically work on every object, without the need to define an abstract type. I get that defining methods on concrete type does not allow code reuse, but what do you have against defining methods without any type restriction ?

Nothing against omitting types! its just that often you can’t do that in practice.

If Base or another package owns a function you implement methods of, you have to use type restriction because you also don’t own Any and would be committing type piracy.

Or if you have multiple types of your own that implement the same method differently, same problem, you have to use types in the signature to distinguish them.

3 Likes

One common (usability-related) issue I’ve seen from defining methods without any type restriction is bad and inscrutable error messages. When your method doesn’t enforce any type restrictions itself, any type errors are going to come some place deeper in the stack, that the user might not even be aware of. So the error messages appear to make no sense, and it takes quite some experience in Julia to start decoding these error messages and trace them back.

1 Like

Personally, I much prefer duck typing as far as I can get away with it. It may take a little practice, but i find the deep MethodErrors that result from duck typing much more instructive than one further up the stack in some higher-level function.

For example, with duck typing you may get an error No method matching one(::Foo) rather than hitting No method matching super_complicated_function(::Int64,::String,::Foo,::Vector{Int64}) earlier in the call. The first error message indicates that if I can define one(::Foo) then there’s a good chance that super_complicated_function will just work – or I’ll hit the next missing method and can fix that too. It’s only if I can’t define the concept of one(::Foo) that I have to worry about functions further up the stack. If I hit the error at super_complicated_function, I now have to look at and understand the full function and try to write an alternative. If the author of super_complicated_function was engaging in recreational overtyping, I probably have to copy-paste the whole body with some trivial modification to make it support ::Foo. Code duplication is a horror for maintainability.

A major strength of overloading and multiple dispatch is defining generic behaviors where possible and specific behaviors where necessary so that things “just work”.

2 Likes