-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Interfaces for Abstract Types #6975
Comments
Generally, I think this is a good direction to better interface-oriented programming. However, something is missing here. The signatures of the methods (not just their names) are also significant for an interface. This is not something easy to implement and there will be a lot of gotchas. That's probably one of the reasons why Concepts was not accepted by C++ 11, and after three years, only a very limited lite version gets into C++ 14. |
The I should add that we already have one part of |
That macro is pretty cool. I've manually defined error-triggering fallbacks, and its worked pretty well for defining interfaces. e.g. JuliaOpt's MathProgBase does this, and it works well. I was toying around with a new solver (https://github.com/IainNZ/RationalSimplex.jl) and I just had to keep implementing interface functions until it stopped raising errors to get it working. Your proposal would do a similar thing, right? But would you have to implement the entire interface? |
How does this deal with covariant / contravariant parameters? For example, abstract A has foo(::A, ::Array)
type B <: A
...
end
type C <: A
...
end
# is it ok to let the arguments to have more general types?
foo(x::Union(B, C), y::AbstractArray) = .... |
@IainNZ Yes, the proposal is actually about making |
@lindahua Thats an interesting example. Have to think about that. |
@lindahua One would probably want your example to just work. So this might have to be implemented a little deeper in the compiler. On the abstract type definition one has to keep track of the interface names/signatures. And at that point where currently a "... not defined" error is thrown one has to generate the appropriate error message. |
It is very easy to change how Another thing this could get us is a function in |
Thanks @ivarne. So the implementation could look as follows:
Most of the logic will then be in |
I have been experimenting a little with this and using the following gist https://gist.github.com/tknopp/ed53dc22b61062a2b283 I can do:
when defining
Not that this does currently not take the signature into account. |
I updated the code in the gist a bit so that function signatures can be taken into account. It is still very hacky but the following now works:
|
I should have add that the interface cache in the gist now operates on symbols instead of functions so that one can add an interface and declare the function afterwards. I might have to do the same with the signature. |
Just saw that #2248 already has some material on interfaces. |
I was going to hold off on publishing thoughts on more speculative features like interfaces until after we get 0.3 out the door, but since you've started the discussion, here's something I wrote up a while ago. Here's a mockup of syntax for interface declaration and the implementation of that interface: interface Iterable{T,S}
start :: Iterable --> S
done :: (Iterable,S) --> Bool
next :: (Iterable,S) --> (T,S)
end
implement UnitRange{T} <: Iterable{T,T}
start(r::UnitRange) = oftype(r.start + 1, r.start)
next(r::UnitRange, state::T) = (oftype(T,state), state + 1)
done(r::UnitRange, state::T) = i == oftype(i,r.stop) + 1
end Let's break this down into pieces. First, there's function type syntax: Second, there's the declaration of the interface Third, there's the The primary advantage of the Another idea that I've had bouncing around is the separation of the "inner" and "outer" versions of interface functions. What I mean by this is that the "inner" function is the one that you supply methods for to implement some interface, while the "outer" function is the one you call to implement generic functionality in terms of some interface. Consider when you look at the methods of the julia> methods(sort!)
sort!(r::UnitRange{T<:Real}) at range.jl:498
sort!(v::AbstractArray{T,1},lo::Int64,hi::Int64,::InsertionSortAlg,o::Ordering) at sort.jl:242
sort!(v::AbstractArray{T,1},lo::Int64,hi::Int64,a::QuickSortAlg,o::Ordering) at sort.jl:259
sort!(v::AbstractArray{T,1},lo::Int64,hi::Int64,a::MergeSortAlg,o::Ordering) at sort.jl:289
sort!(v::AbstractArray{T,1},lo::Int64,hi::Int64,a::MergeSortAlg,o::Ordering,t) at sort.jl:289
sort!{T<:Union(Float64,Float32)}(v::AbstractArray{T<:Union(Float64,Float32),1},a::Algorithm,o::Union(ReverseOrdering{ForwardOrdering},ForwardOrdering)) at sort.jl:441
sort!{O<:Union(ReverseOrdering{ForwardOrdering},ForwardOrdering),T<:Union(Float64,Float32)}(v::Array{Int64,1},a::Algorithm,o::Perm{O<:Union(ReverseOrdering{ForwardOrdering},ForwardOrdering),Array{T<:Union(Float64,Float32),1}}) at sort.jl:442
sort!(v::AbstractArray{T,1},alg::Algorithm,order::Ordering) at sort.jl:329
sort!(v::AbstractArray{T,1}) at sort.jl:330
sort!{Tv<:Union(Complex{Float32},Complex{Float64},Float64,Float32)}(A::CholmodSparse{Tv<:Union(Complex{Float32},Complex{Float64},Float64,Float32),Int32}) at linalg/cholmod.jl:809
sort!{Tv<:Union(Complex{Float32},Complex{Float64},Float64,Float32)}(A::CholmodSparse{Tv<:Union(Complex{Float32},Complex{Float64},Float64,Float32),Int64}) at linalg/cholmod.jl:809 Some of these methods are intented for public consumption, but others are just part of the internal implementation of the public sorting methods. Really, the only public method that this should have is this: sort!(v::AbstractArray) The rest are noise and belong on the "inside". In particular, the sort!(v::AbstractArray{T,1},lo::Int64,hi::Int64,::InsertionSortAlg,o::Ordering)
sort!(v::AbstractArray{T,1},lo::Int64,hi::Int64,a::QuickSortAlg,o::Ordering)
sort!(v::AbstractArray{T,1},lo::Int64,hi::Int64,a::MergeSortAlg,o::Ordering) kinds of methods are what a sorting algorithm implements to hook into the generic sorting machinery. Currently # module Sort
interface Algorithm
sort! :: (AbstractVector, Int, Int, Algorithm, Ordering) --> AbstractVector
end
implement InsertionSortAlg <: Algorithm
function sort!(v::AbstractVector, lo::Int, hi::Int, ::InsertionSortAlg, o::Ordering)
@inbounds for i = lo+1:hi
j = i
x = v[i]
while j > lo
if lt(o, x, v[j-1])
v[j] = v[j-1]
j -= 1
continue
end
break
end
v[j] = x
end
return v
end
end The separation we want could then be accomplished by defining: # module Sort
sort!(v::AbstractVector, alg::Algorithm, order::Ordering) =
Algorithm.sort!(v,1,length(v),alg,order) This is very close to what we're doing currently, except that we call |
@StefanKarpinski Thanks a lot for your writeup! This is surely not 0.3 stuff. So sorry that I brought this up at this time. I am just not sure if 0.3 will happen soon or in a half year ;-) From a first look I really (!) like that the implementing section is defined its own code block. This enables to directly verify the interface on the type definition. |
No worries – there's not really any harm in speculating about future features while we're trying to stabilize a release. |
Your approach is a lot more fundamental and tries to also solve some interface independent issues. It also kind of brings a new construct (i.e. the interface) into the language that makes the language a little bit more complex (which is not necessary a bad thing). I see "the interface" more as an annotation to abstract types. If one puts the As I said I would really like if the interface could be directly validated on its declaration. The least invasive approach here might be to allow for defining methods inside a type declaration. So taking your example something like
One would still be allowed to define the function outside the type declaration. The only difference would be that inner function declarations are validated against interfaces. But again, maybe my "least invasive approach" is too short sighted. Don't really know. |
One issue with putting those definition inside of the type block is that in order to do this, we'll really need multiple inheritance of interfaces at least, and it's conveivable that there may be name collisions between different interfaces. You might also want to add the fact that a type supports an interface at some point after defining the type, although I'm not certain about that. |
@StefanKarpinski It is great to see that you are thinking about this. The Graphs package is one that needs the interface system most. It would be interesting to see how this system can express the interfaces outlined here: http://graphsjl-docs.readthedocs.org/en/latest/interface.html. |
@StefanKarpinski: I don't fully see the issue with multiple inheritance and in-block function declarations. Within the type block all inherited interfaces would have to be checked. But I kind of understand that one might want to let the interface implementation "open". And in-type function declaration might complicate the language too much. Maybe the approach I have implemented in #7025 is sufficient. Either put a |
This issue is that different interfaces could have generic function by the same name, which would cause a name collision and require doing an explicit import or adding methods by a fully qualified name. It also makes it less clear which method definitions belong to which interfaces – which is why the name collision can happen in the first place. Btw, I agree that adding interfaces as another "thing" in the language feels a little too non-orthogonal. After all, as I mentioned in the proposal, they're a little bit like modules and a little bit like types. It feels like some unification of concepts might be possible, but I'm not clear on how. |
I prefer the interface-as-library model to the interface-as-language-feature model for a few reasons: it keeps the language simpler (admittedly preference and not a concrete objection) and it means that the feature remains optional and can be easily improved or entirely replaced without mucking with the actual language. Specifically, I think the proposal (or at least the shape of the proposal) from @tknopp is better than the one from @StefanKarpinski - it provides definition-time checking without requiring anything new in the language. The main drawback I see is the lack of ability to deal with type variables; I think this can be handled by having the interface definition provide type predicates for the types of required functions. |
One of the major motivations for my proposal is the large amount of confusion caused by having to import generic functions – but not export them – in order to add methods to them. Most of the time, this happens when someone is trying to implement an unofficial interface, so this makes it look like that's what's happening. |
That seems like an orthogonal problem to solve, unless you want to entirely restrict methods to belonging to interfaces. |
No, that certainly doesn't seem like a good restriction. |
@StefanKarpinski you mention that that you'd be able to dispatch on an interface. Also in the This seems a bit at odds with multiple dispatch, as in general methods don't belong to a particular type, they belong to a tuple of types. So if methods don't belong to types, how can interfaces (which are basically sets of methods) belong to a type? Say I'm using library M:
now I want to write a generic function that takes an A and B
In this example the Other modules that want to provide concrete subtypes of
Admittedly this example feels pretty contrived, but hopefully it illustrates that (in my mind at least) it feels like there's a fundamental mis-match between multiple dispatch and the concept of a particular type implementing an interface. I do see your point about the |
@abe-egnor I also think that a more open approach seems more feasible. My prototype #7025 lacks essentially two things: As I am not so much a parametric type guru I am kind of sure that b) is solvable by someone with deeper experience. |
New Swift for tensorflow compiler AD usecase for protocols: |
I think this presentation deserves attention when exploring the design space for this problem. |
For discussion of non-specific ideas and links to relevant background work, it would be better to start a corresponding discourse thread and post and discuss there. Note that almost all of the problems encountered and discussed in research on generic programming in statically typed languages is irrelevant to Julia. Static languages are almost exclusively concerned with the problem of providing sufficient expressiveness to write the code they want to while still being able to statically type check that there are no type system violations. We have no problems with expressiveness and don't require static type checking, so none of that really matters in Julia. What we do care about is allowing people to document the expectations of a protocol in a structured way which the language can then dynamically verify (in advance, when possible). We also care about allowing people to dispatch on things like traits; it remains open whether those should be connected. Bottom line: while academic work on protocols in static languages may be of general interest, it's not very helpful in the context of Julia. |
that's the 🎫 |
Aside from avoiding breaking changes, would the elimination of abstract types and the introduction of golang-style implicit interfaces be feasible in julia? |
No, it would not. |
well, isn't that what protocols / traits are all about? There was some discussion whether protocols need to be implicit or explicit. |
I think that since 0.3 (2014), experience has shown that implicit interfaces (ie not enforced by the language/compiler) work just fine. Also, having witnessed how some packages evolved, I think that the best interfaces were developed organically, and were formalized (= documented) only at a later point. I am not sure that a formal desciption of interfaces, enforced by the language somehow, is needed. But while that is decided, it would be great to encourage the following (in the documentation, tutorials, and style guides):
The following would benefit from clarification as I am not sure what the best practice is:
|
Why is that so? Some aspects of types may be factored out into interfaces (for dispatch purposes), such as iteration. Otherwise you would have to rewrite code or impose unnecessary structure.
Perhaps it's not necessary, but would it be better? I can have a function dispatch on an iterable type. Shouldn't a tiled iterable type fulfill that implicitly? Why should the user have to draw these around nominal types when they only care about the interface? What's the point of nominal subtyping if you are essentially just using them as abstract interfaces? Traits seem to be more granular and powerful, so would be a better generalization. So it just seems like types are almost traits, but we have to have traits to work around their limitations (and vice versa). |
Dispatch—you can dispatch on the nominal type of something. If you don't need to dispatch on whether a type implements an interface or not, then you can just duck type it. This is what people typically use Holy traits for: the trait lets you dispatch to call an implementation that assumes that some interface is implemented (e.g. "having a known length"). Something that people seem to want is to avoid that layer of indirection but it's that seems like it's merely a convenience, not a necessity. |
I believe @tpapp was saying that you only need the type to determine whether or not something implements an interface, not that all interfaces can be represented with type hierarchies. |
Just a thought, while using It's sometimes annoying to forward a lot methods @forward Foo.x a b c d ... what if we could use I know we could never came up with a list what is going to inherit (this is also why static |
@datnamer: as others have clarified, interfaces should not be more granular than types (ie implementing the interface should never depend on the value, given the type). This is meshes well with the compiler's optimization model and is not a constraint in practice. Perhaps I was not clear, but the purpose of my response was to point out that we have interfaces already to the extent that is useful in Julia, and they are lightweight, fast, and becoming pervasive as the ecosystem matures. A formal spec for describing an interface adds little value IMO: it would amount to just documentation and checking that some methods are available. The latter is part of an interface, but the other part is the semantics implemented by these methods (eg if However, what I would love to see is
|
@tpapp Makes sense to me now, thanks. @StefanKarpinski I don't quite understand. Traits are not nominal types (right?), nevertheless, they can be used for dispatch. My point is basically the one made by @tknopp and @mauro3 here: https://discourse.julialang.org/t/why-does-julia-not-support-multiple-traits/5278/43?u=datnamer That by having traits and abstract typing, there is additional complexity and confusion by having two very similar concepts.
Can sections of trait hierarchy be dispatched upon grouped by things like unions and intersections, with type parameters, robustly ? I haven't tried it, but it feels like that requires language support. IE expression problem in the type domain. Edit: I think the problem was my conflation of interfaces and traits, as they are used here. |
Just posting this here cause it's fun: it looks like Concepts has definitely been accepted and will be a part of C++20. Interesting stuff! https://herbsutter.com/2019/02/23/trip-report-winter-iso-c-standards-meeting-kona/ |
I think that traits are a really good way of solving this issue and holy traits certainly have come a long way. However, I think what Julia really needs is a way of grouping functions that belong to a trait. This would be useful for documentation reasons but also for readability of the code. From what I have seen so far, I think that a trait syntax like in Rust would be the way to go. |
I think this is super important, and the most important use case would be for indexing iterators. Here's a proposal for the kind of syntax that you might hope would work. Apologies if it's already been proposed (long thread...).
|
Please excuse me, if I don't add anything new. One aspect I really like about the C++ approach: It decouples the interface definition, the interface instantiation and the constrained parametric method part. When the list of required methods is part of an abstract type, a type can only adhere to an interface/concept/trait/typeclass/... when the interface is known to the author. This makes it hard to make a type from library A adhere to an interface from library B. When the list of required methods is separate, but still 'attached' to the type itself, this problem is circumvented. This is the case with Haskell typeclasses, rust traits and also the C++ goes one step further. Basically, you define a list of functions with the desired signatures and give it a name. And whenever those functions exist and return the desired types, a type adheres to an interface. This means, to implement an interface instance for some type, it's sufficient to just implement the necessary functions. This is basically what julia already does (e.g., with the iterable or the indexable concepts) minus the ability to dispatch on those concepts. I think that the last way would feel the most julian way, as it plays very well with the idea of duck-typing. Basically, naming the duck and thereby enabling multiple dispatch on the duck is all that is required for it. |
This is basically only available at compile time (for generic functions/methods), afaict. |
On 14 April 2021 08:08:12 CEST, sighoya ***@***.***> wrote:
>C++ goes one step further. Basically, you define a list of functions
>with the desired signatures and give it a name. And whenever those
>functions exist and return the desired types, a type adheres to an
>interface.
This is basically only available at compile time (for generic
functions/methods), afaict.
Yes. Apart from virtual functions that dispatch on the `this` argument, C++ only does overload resolution. As such, concepts (and templates in general) are only available at compile time in C++.
|
Is this related to #32732 ? I need something like |
No, this issue is about better error messages not about preventing the error. What I usually do is to use the If you want to prevent an error you can write a fallback implementation acting in the abstract type that does not error. |
For friendly error messages, see |
I think this feature request has not yet its own issue although it has been discussed in e.g. #5.
I think it would be great if we could explicitly define interfaces on abstract types. By interface I mean all methods that have to be implemented to fulfill the abstract type requirements. Currently, the interface is only implicitly defined and it can be scattered over several files so that it is very hard to determine what one has to implement when deriving from an abstract type.
Interfaces would primary give us two things:
Base.graphics has a macro that actually allows to define interfaces by encoding an error message in the fallback implementation. I think this is already very clever. But maybe giving it the following syntax is even neater:
Here it would be neat if one could specify different granularities. The
print
andpush!
declarations only say that there have to be any methods with that name (andMyType
as first parameter) but they don't specify the types. In contrast thesize
declaration is completely typed. I think this gives a lot of flexibility and for an untyped interface declaration one could still give quite specific error messages.As I have said in #5, such interfaces are basically what is planed in C++ as
Concept-light
for C++14 or C++17. And having done quite some C++ template programming I am certain that some formalization in this area would also be good for Julia.The text was updated successfully, but these errors were encountered: