Skip to content
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

Open
tknopp opened this issue May 26, 2014 · 177 comments
Open

Interfaces for Abstract Types #6975

tknopp opened this issue May 26, 2014 · 177 comments
Milestone

Comments

@tknopp
Copy link
Contributor

tknopp commented May 26, 2014

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:

  • self documentation of interfaces at a single place
  • better error messages

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:

abstract MyType has print, size(::MyType,::Int), push!

Here it would be neat if one could specify different granularities. The print and push! declarations only say that there have to be any methods with that name (and MyType as first parameter) but they don't specify the types. In contrast the size 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.

@lindahua
Copy link
Contributor

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.

@tknopp
Copy link
Contributor Author

tknopp commented May 26, 2014

The size method in my example contained the signature. Further @mustimplement from Base.graphics also takes the signature into account.

I should add that we already have one part of Concept-light which is the ability to restrict a type to be a subtype of a certain abstract type. The interfaces are the other part.

@IainNZ
Copy link
Member

IainNZ commented May 26, 2014

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?

@lindahua
Copy link
Contributor

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) = ....

@tknopp
Copy link
Contributor Author

tknopp commented May 26, 2014

@IainNZ Yes, the proposal is actually about making @mustimplement a little more versatile such that e.g. the signature can but does not have to be provided. And my feeling is that this is such a "core" that it is worth to get its own syntax. It would be great to enforce that all methods are really implemented but the current runtime check as is done in @mustimplement is already a great thing and might be easier to implement.

@tknopp
Copy link
Contributor Author

tknopp commented May 26, 2014

@lindahua Thats an interesting example. Have to think about that.

@tknopp
Copy link
Contributor Author

tknopp commented May 26, 2014

@lindahua One would probably want your example to just work. @mustimplement would not work as it defines more specific method signatures.

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.

@ivarne
Copy link
Member

ivarne commented May 26, 2014

It is very easy to change how MethodError print, when we have a syntax and API to express and access the information.

Another thing this could get us is a function in base.Test to verify that a type (all types?) fully implements the interfaces of the parent types. That would be a really neat unit test.

@tknopp
Copy link
Contributor Author

tknopp commented May 27, 2014

Thanks @ivarne. So the implementation could look as follows:

  1. One has a global dictionary with abstract types as keys and functions (+ optional signatures) as values.
  2. The parser needs to be adapted to fill the dict when a has declaration is parsed.
  3. MethodError needs to look up if the current function is part of the global dictionary.

Most of the logic will then be in MethodError.

@tknopp
Copy link
Contributor Author

tknopp commented May 28, 2014

I have been experimenting a little with this and using the following gist https://gist.github.com/tknopp/ed53dc22b61062a2b283 I can do:

julia> abstract A
julia> addInterface(A,length)
julia> type B <: A end
julia> checkInterface(B)
ERROR: Interface not implemented! B has to implement length in order to be subtype of A ! in error at error.jl:22

when defining length no error is thrown:

julia> import Base.length
julia> length(::B) = 10
length (generic function with 34 methods)
julia> checkInterface(B)
true

Not that this does currently not take the signature into account.

@tknopp
Copy link
Contributor Author

tknopp commented May 28, 2014

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:

julia> abstract A
julia> type B <: A end

julia> addInterface(A,:size,(A,Int64))
1-element Array{(DataType,DataType),1}:
 (A,Int64)
julia> checkInterface(B)
ERROR: Interface not implemented! B has to implement size in order to be subtype of A !
in error at error.jl:22

julia> import Base.size
julia> size(::B, ::Integer) = 333
size (generic function with 47 methods)
julia> checkInterface(B)
true

julia> addInterface(A,:size,(A,Float64))
2-element Array{(DataType,DataType),1}:
 (A,Int64)
 (A,Float64)
julia> checkInterface(B)
ERROR: Interface not implemented! B has to implement size in order to be subtype of A !
 in error at error.jl:22
 in string at string.jl:30

@tknopp
Copy link
Contributor Author

tknopp commented May 28, 2014

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.

@tknopp
Copy link
Contributor Author

tknopp commented May 28, 2014

Just saw that #2248 already has some material on interfaces.

@StefanKarpinski
Copy link
Member

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: A --> B is the type of a function that maps objects of type A to type B. Tuples in this notation do the obvious thing. In isolation, I'm proposing that f :: A --> B would declare that f is a generic function, mapping type A to type B. It's a slightly open question what this means. Does it mean that when applied to an argument of type A, f will give a result of type B? Does it mean that f can only be applied to arguments of type A? Should automatic conversion occur anywhere – on output, on input? For now, we can suppose that all this does is create a new generic function without adding any methods to it, and the types are just for documentation.

Second, there's the declaration of the interface Iterable{T,S}. This makes Iterable a bit like a module and a bit like an abstract type. It's like a module in that it has bindings to generic functions called Iterable.start, Iterable.done and Iterable.next. It's like a type in that Iterable and Iterable{T} and Iterable{T,S} can be used wherever abstract types can – in particular, in method dispatch.

Third, there's the implement block defining how UnitRange implements the Iterable interface. Inside of the implement block, the the Iterable.start, Iterable.done and Iterable.next functions available, as if the user had done import Iterable: start, done, next, allowing the addition of methods to these functions. This block is template-like the way that parametric type declarations are – inside the block, UnitRange means a specific UnitRange, not the umbrella type.

The primary advantage of the implement block is that it avoids needing the explicitly import functions that you want to extend – they are implicitly imported for you, which is nice since people are generally confused about import anyway. This seems like a much clearer way to express that. I suspect that most generic functions in Base that users will want to extend ought to belong to some interface, so this should eliminate the vast majority of uses for import. Since you can always fully qualify a name, maybe we could do away with it altogether.

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 sort! function (excluding deprecated methods):

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 Sort.Algorithm is an abstract type, and InsertionSortAlg, QuickSortAlg and MergeSortAlg are concrete subtypes of it. With interfaces, Sort.Algorithm could be an interface instead and the specific algorithms would implement it. Something like this:

# 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 Algorithm.sort! instead of just sort! – and when implementing various sorting algorithms, the "inner" definition is a method of Algorithm.sort! not the sort! function. This has the effect of separating the implementation of sort! from the its external interface.

@tknopp
Copy link
Contributor Author

tknopp commented May 29, 2014

@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.

@StefanKarpinski
Copy link
Member

No worries – there's not really any harm in speculating about future features while we're trying to stabilize a release.

@tknopp
Copy link
Contributor Author

tknopp commented May 29, 2014

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 has to it one can specify an interface but one does not have to.

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

type 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

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.

@StefanKarpinski
Copy link
Member

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.

@lindahua
Copy link
Contributor

@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.

@tknopp
Copy link
Contributor Author

tknopp commented May 29, 2014

@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 verify_interface after the function declarations (or in a unit test) or defer it to the MethodError.

@StefanKarpinski
Copy link
Member

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.

@abr-egn
Copy link

abr-egn commented Jul 8, 2014

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.

@StefanKarpinski
Copy link
Member

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.

@abr-egn
Copy link

abr-egn commented Jul 8, 2014

That seems like an orthogonal problem to solve, unless you want to entirely restrict methods to belonging to interfaces.

@StefanKarpinski
Copy link
Member

No, that certainly doesn't seem like a good restriction.

@ssfrr
Copy link
Contributor

ssfrr commented Jul 9, 2014

@StefanKarpinski you mention that that you'd be able to dispatch on an interface. Also in the implement syntax the idea is that a particular type implements the interface.

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:

module M

abstract A
abstract B

type A2 <: A end
type A3 <: A end
type B2 <: B end

function f(a::A2, b::B2)
    # do stuff
end

function f(a::A3, b::B2)
    # do stuff
end

export f, A, B, A2, A3, B2
end # module M

now I want to write a generic function that takes an A and B

using M

function userfunc(a::A, b::B, i::Int)
    res = f(a, b)
    res + i
end

In this example the f function forms an ad-hoc interface that takes an A and a B, and I want to be able to assume I can call the f function on them. In this case it isn't clear which one of them should be considered to implement the interface.

Other modules that want to provide concrete subtypes of A and B should be expected to provide implementations of f. To avoid the combinatorial explosion of required methods I'd expect the library to define f against the abstract types:

module N

using M

type SpecialA <: A end
type SpecialB <: B end

function M.f(a::SpecialA, b::SpecialB)
    # do stuff
end

function M.f(a::A, b::SpecialB)
    # do stuff
end

function M.f(a::SpecialA, b::B)
    # do stuff
end

export SpecialA, SpecialB

end # module N

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 import confusion though. It took me a couple tries at this example to remember that when I put using M and then tried to add methods to f it didn't do what I expected, and I had to add the methods to M.f (or I could have used import). I don't think that interfaces are the solution to that problem though. Is there a separate issue to brainstorm ways to make adding methods more intuitive?

@tknopp
Copy link
Contributor Author

tknopp commented Jul 9, 2014

@abe-egnor I also think that a more open approach seems more feasible. My prototype #7025 lacks essentially two things:
a) a better syntax for defining interfaces
b) parametric type definitions

As I am not so much a parametric type guru I am kind of sure that b) is solvable by someone with deeper experience.
Regarding a) one could go with a macro. Personally I think we could spend some language support for directly defining the interface as part of the abstract type definition. The has approach might be too short sighted. A code block might make this nicer. Actually this is highly related to #4935 where an "internal" interface is defined while this her is about the public interface. These don't have to be bundled as I think this issue is much more important than #4935. But still syntax wise one might want to take both use cases into account.

@datnamer
Copy link

New Swift for tensorflow compiler AD usecase for protocols:
https://gist.github.com/rxwei/30ba75ce092ab3b0dce4bde1fc2c9f1d
@timholy and @Keno might be interested in this. Has brand new content

@AzamatB
Copy link
Contributor

AzamatB commented Dec 12, 2018

I think this presentation deserves attention when exploring the design space for this problem.

@StefanKarpinski
Copy link
Member

StefanKarpinski commented Dec 12, 2018

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.

@JeffreySarnoff
Copy link
Contributor

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.

        that's the 🎫

@qpwo
Copy link

qpwo commented Feb 11, 2019

Aside from avoiding breaking changes, would the elimination of abstract types and the introduction of golang-style implicit interfaces be feasible in julia?

@StefanKarpinski
Copy link
Member

No, it would not.

@tknopp
Copy link
Contributor Author

tknopp commented Feb 12, 2019

well, isn't that what protocols / traits are all about? There was some discussion whether protocols need to be implicit or explicit.

@tpapp
Copy link
Contributor

tpapp commented Feb 12, 2019

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):

  1. "interfaces" are cheap and lightweight, just a bunch of functions with a prescribed behavior for a set of types (yes, types are the right level of granularity — for x::T, T should be sufficient to decide whether x implements the interface) . So if one is defining a package with extensible behavior, it really makes sense to document the interface.

  2. Interfaces don't need to be described by subtype relations. Types without a common (nontrivial) supertype may implement the same interface. A type can implement multiple interfaces.

  3. Forwarding/composition implicitly requires interfaces. "How to make a wrapper inherit all methods of the parent" is a question that crops up often, but it is not the right question. The practical solution is to have a core interface and just implement that for the wrapper.

  4. Traits are cheap and should be used liberally. Base.IndexStyle is an excellent canonical example.

The following would benefit from clarification as I am not sure what the best practice is:

  1. Should the interface have a query function, like eg Tables.istable for deciding if an object implements the interface? I think it is good practice, if a caller can work with various alternative interfaces and needs to walk down the list of fallbacks.

  2. What's the best place for an interface documentation in a docstring? I would say the query function above.

@datnamer
Copy link

datnamer commented Feb 12, 2019

  1. yes, types are the right level of granularity

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.

  1. Interfaces don't need to be described by subtype relations.

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).

@StefanKarpinski
Copy link
Member

StefanKarpinski commented Feb 12, 2019

What's the point of nominal subtyping if you are essentially just using them as abstract interfaces?

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.

@iamed2
Copy link
Contributor

iamed2 commented Feb 12, 2019

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.

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.

@Roger-luo
Copy link
Contributor

Roger-luo commented Feb 13, 2019

Just a thought, while using MacroTools's forward:

It's sometimes annoying to forward a lot methods

@forward Foo.x a b c d ...

what if we could use Foo.x's type and a list of method then infer which one to forward? This will be a kind of inheritance and can be implemented with existing features (macros + generated function), it looks like some kind of interface as well, but we don't need anything else in the language.

I know we could never came up with a list what is going to inherit (this is also why static class model is less flexible), sometimes you only need a few of them, but it's just convenient for core functions (e.g someone want to define a wrapper (subtype of AbstractArray) around Array, most of the functions are just forwarded)

@tpapp
Copy link
Contributor

tpapp commented Feb 13, 2019

@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 A is an array, axes(A) gives me a range of coordinates that are valid for getindex). Formal specs of interfaces cannot address these in general, so I am of the opinion that they would just add boilerplate with little value. I am also concerned that it would just raise a (small) barrier to entry for little benefit.

However, what I would love to see is

  1. documentation for more and more interfaces (in a docstring),

  2. test suites to catch obvious errors for mature interfaces for newly defined types (eg a lot of T <: AbstractArray implement eltype(::T) and not eltype(::Type{T}).

@datnamer
Copy link

datnamer commented Feb 20, 2019

@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.

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.

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.

@NHDaly
Copy link
Member

NHDaly commented Mar 1, 2019

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/
https://en.cppreference.com/w/cpp/language/constraints

@lassepe
Copy link
Contributor

lassepe commented Oct 15, 2019

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.

@bramtayl
Copy link
Contributor

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...).

import Base: Generator
@require getindex(AbstractArray, Vararg{Int})
function getindex(container::Generator, index...)
    iterator = container.iter
    if @works getindex(iterator, index...)
        container.f(getindex(iterator, index...))
    else
        @interfaceerror getindex(iterator, index...)
    end
end

@Uroc327
Copy link

Uroc327 commented Apr 13, 2021

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 protocol syntax above, I believe. Both require the programmer to explicitly specify that a type A adhere to interface I. This allows to mix types and interfaces from different libraries and also allows to implement interface instances for types, that don't adhere to the interface out of the box.

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, of course, bears the potential problem that some methods seemingly implement an interface for some unrelated type. On the other hand, we already have this exact problem with duck-typing. We just can't dispatch on the problem :P

@sighoya
Copy link

sighoya commented Apr 14, 2021

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.

@Uroc327
Copy link

Uroc327 commented Apr 14, 2021 via email

@clarkevans
Copy link
Member

Is this related to #32732 ? I need something like static_hasmethod to provide fallback implementations for functions that can be specialized in HypertextLiteral. The alternative is to raise a MethodError for the user defined type, but this seems unfriendly. Having someone get a stack trace out of the box seems like a harsh user experience.

@tknopp
Copy link
Contributor Author

tknopp commented Mar 31, 2022

No, this issue is about better error messages not about preventing the error. What I usually do is to use the @mustimplement macro, see for instance https://github.com/MagneticParticleImaging/MPIFiles.jl/blob/master/src/MPIFiles.jl#L102

If you want to prevent an error you can write a fallback implementation acting in the abstract type that does not error. @mustimplement basically does this but then throws.

@tpapp
Copy link
Contributor

tpapp commented Mar 31, 2022

For friendly error messages, see register_error_hint.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests