Nested required interfaces?
gdalle opened this issue · 13 comments
Say I have two abstract types A
and B <: A
defining interfaces:
A
requires methodf
B
requires methodsf
andg
Is there a way to specify that B
only requires g
and let subtyping take care of f
through A
?
In theory, that is already required through declaring B <: A
- do you have a particular operation in mind that's not exposed with the current interface? For example:
julia> using RequiredInterfaces
julia> abstract type A end
julia> @required A f(::A)
f (generic function with 1 method)
julia> abstract type B end
julia> @required B g(::B)
g (generic function with 1 method)
julia> const RI = RequiredInterfaces
RequiredInterfaces
julia> struct C <: B end
julia> f(::C) = 1
f (generic function with 2 methods)
julia> g(::C) = 1
g (generic function with 2 methods)
julia> RI.check_interface_implemented(A, C)
true
julia> RI.check_interface_implemented(B, C)
true
Ah, do you mean something like:
julia> struct D <: B end
julia> g(::D) = 2
g (generic function with 3 methods)
julia> RI.check_interface_implemented(B, D)
true
Returning the missing methods for A
as well?
No I mean something like this:
julia> using RequiredInterfaces
julia> const RI = RequiredInterfaces
RequiredInterfaces
julia> abstract type A end
julia> @required A f(::A)
f (generic function with 1 method)
julia> abstract type B <: A end
julia> @required B g(::B)
g (generic function with 1 method)
julia> struct C <: B end
julia> g(::C) = 1
g (generic function with 2 methods)
julia> RI.check_interface_implemented(B, C)
true # I expect false here cause C is missing f to implement B <: A
Yeah, that's what I meant in the second example with D
:) You're right, check_interface_implemented
should report that as well.
Yeah, that's what I meant in the second example with D :)
But in yours B didnt subtype A so I wasnt sure
Ah it did, that was in the same session/a continuation of my second comment - sorry, that could have been clearer!
then yeah we're on the same page :)
Ok, I pushed a fix to feat/report_inherited_interface
, which should work - at least it does with your MWE and tests don't complain locally. Please test it with your real code (in which you've presumably found this) and if that fixes your issue, I'll tag a release!
OK, will do!
I was also wondering: for MyStruct
to implement MyInterface
, is it required
- by your package
- by your philosophy
that MyStruct
is actually defined as MyStruct <: MyInterface
?
Basically if you don't require it then we don't even need MyInterface
to be a type... and your package is another version of traits
I noticed that the fix wasn't actually proper, so I'll push a fix for the fix to that branch shortly!
As for your questions - it's not required by the package, all the checks work just as well if T
doesn't subtype MyInterface
. This is also mentioned in the (new) manual page on multiple functions in an interface, which I pushed together with implementing the begin
block for @required
.
Your second question is actually much more interesting - basically, the issue comes down to what exactly is meant with T <: S
. If you view it as "T
dispatches just like S
", then yes, it means that in some form, T
was declared to subtype S
. I personally consider this to be preferable, which is why I talk about Meet
in the philosophy section of the docs; after all, types exist so we can punt guarantees about objects into some pre-calculatable stage, instead of littering our code with lots and lots of checks everywhere (which we in reality do anyway to serve as some form of "inline documentation"). The issue is, in Julia we can't subtype multiple types, and we can't dispatch like types that we didn't explicitly subtype. So consider these types, where the contract is "has a type parameter":
abstract type Foo{T} end
abstract type Bar{T} end
struct Baz{T} <: Foo{T} end
foo(::Foo{T}) where T = T
bar(::Bar{T}) where T = T
and now we'd like to have Baz
dispatch like Bar
too, because it has a type parameter. Without explicitly declaring ourselves <: Baz
, we currently can't take advantage of the fallback in Bar
, and will have to implement our own bar(::Baz)
.
This unfortunately generalizes to other dispatches we might want to do - I touch on this in the Multiple Abstract Subtyping section of the philosophy chapter.
What I don't touch on is what happens if we think about allowing types to be extended with external interfaces after they have been defined (i.e., don't declare MyStruct <: SomeInterface
, but rather "tag the interface on" later on, either by implementing the required methods or somehow declaring MyStruct
to be a subtype after the fact) - and that's quite intentional, because it means that all of a sudden you absolutely cannot cache anything about interface compliance anymore, since that may change at any point. It's a similar reasoning to why eval
in Julia is only allowed to be noticeable once you hit the global scope, because otherwise you can't really optimize functions anymore.
On top of this, if you think about what it means to declare being a subtype.. If you take <:
to communicate API guarantees, that's not something e.g. a third party should be able to do for your package after the fact; they didn't write it after all and don't have the maintenance burden of keeping those additional guarantees. So while it might sometimes be nice, I don't think it's really always a good idea - the one edgecase I can see being desirable is having some type of your package only conditionally subtype some third party interface, if that interface package is actually loaded (though there's also an argument to be made regarding just providing a dedicated type for that interface package, wrapping the internal type.. which seems much easier to do engineering wise than actually modifying the subtyping relationship post-declaration!).
Basically if you don't require it then we don't even need MyInterface to be a type... and your package is another version of traits
Well.. we want to be able to dispatch, and the whole general idea behind having any kind of interface exactly is to have a type :) Traits are types too (just not 100% the types we currently have in Julia)! It's totally fair to say that this package is a version of traits - it's more or less my argument for why Holy Traits are not enough and what we'd have to change in the type system to no longer need that pattern.
Whew, that's quite a wall of text - apologies, if anything is unclear, feel free to ask 😅 If you have more questions, I can also open the "Discussions" feature on the repo?
Alright, I fixed the fix - now, getInterface
should report the inherited interface as well. Let me know if it works as expected!
I'm using this in HiddenMarkovModels.jl v0.3.0 and it seems to work, thanks!