JuliaLang / julia

The Julia Programming Language
https://julialang.org/
MIT License
45.43k stars 5.45k forks source link

Interfaces for Abstract Types #6975

Open tknopp opened 10 years ago

tknopp commented 10 years ago

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:

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 commented 10 years ago

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 commented 10 years ago

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 commented 10 years ago

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 commented 10 years ago

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 commented 10 years ago

@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 commented 10 years ago

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

tknopp commented 10 years ago

@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 commented 10 years ago

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 commented 10 years ago

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 commented 10 years ago

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 commented 10 years ago

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 commented 10 years ago

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 commented 10 years ago

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

StefanKarpinski commented 10 years ago

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 commented 10 years ago

@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 commented 10 years ago

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

tknopp commented 10 years ago

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 commented 10 years ago

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 commented 10 years ago

@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 commented 10 years ago

@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 commented 10 years ago

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 commented 10 years ago

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 commented 10 years ago

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 commented 10 years ago

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

StefanKarpinski commented 10 years ago

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

ssfrr commented 10 years ago

@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 commented 10 years ago

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

abr-egn commented 10 years ago

https://gist.github.com/abe-egnor/503661eb4cc0d66b4489 has my first stab at the sort of implementation I was thinking of. In short, an interface is a function from types to a dict that defines the name and parameter types of the required functions for that interface. The @implement macro just calls the function for the given types and then splices the types into the function definitions given, checking that all functions have been defined.

Good points:

Bad points:

abr-egn commented 10 years ago

I think I have a solution to the parameterization problem - in short, the interface definition should be a macro over type expressions, not a function over type values. The @implement macro can then extend type parameters to function definitions, allowing something like:

@interface stack(Container, Data) begin
  stack_push!(Container, Data)
end

@implement stack{T}(Vector{T}, T) begin
  stack_push!(vec, x) = push!(vec, x)
end

In that case, the type parameters are extended to the methods defined in the interface, so it expands to stack_push!{T}(vec::Vector{T}, x::T) = push!(vec, x), which I believe is exactly the right thing.

I'll re-work my initial implementation to do this as I get the time; probably on the order of a week.

abr-egn commented 10 years ago

https://github.com/JuliaLang/julia/pull/7695

mauro3 commented 10 years ago

I browsed the internet a bit to see what other programming languages do about interfaces, inheritance and such and came up with a few ideas. (In case anyone is interested here the very rough notes I took https://gist.github.com/mauro3/e3e18833daf49cdf8f60)

The short of it is that maybe interfaces could be implemented by:

This would turn abstract types into interfaces and the concrete subtypes would then be required to implement that interface.

The long story:

What I found is that a few of the "modern" languages do away with subtype polymorphism, i.e. there is no direct grouping of types, and instead they group their types based on them belonging to interfaces / traits / type classes. In some languages, the interfaces / traits / type classes can have order between them and inherit from each other. They also seem (mostly) happy about that choice. Examples are: Go, Rust, Haskell. Go is the least strict of the three and lets its interfaces be specified implicitly, i.e. if a type implements the specific set of functions of an interface then it belongs to that interface. For Rust the interface (traits) must be implemented explicitly in a impl block. Neither Go nor Rust have multimethods. Haskell has multimethods and they are actually directly linked to the interface (type class).

In some sense this is similar to what Julia does too, the abstract types are like an (implicit) interface, i.e. they are about behavior and not about fields. This is what @StefanKarpinski also observed in one of his posts above and stated that additionally having interfaces "feels a little too non-orthogonal." So, Julia has a type hierarchy (i.e. subtype polymorphism) whereas Go/Rust/Haskell don't.

How about turning Julia's abstract types into more of an interface / trait / type class, whilst keeping all types in the hierarchy None<: ... <:Any? This would entail: 1) allow multiple inheritance for (abstract) types (issue #5) 2) allow associating functions with abstract types (i.e. define an interface) 3) Allow to specify that interface, for both abstract (i.e. a default implementation) and concrete types.

I think this could lead to a more fine grained type graph than we have now and could be implemented step by step. For instance, a array-type would be pieced together:

abstract Container  <: Iterable, Indexable, ...
end

abstract AbstractArray <: Container, Arithmetic, ...
    ...
end

abstract  Associative{K,V} <: Iterable, Indexable, Eq
    haskey :: (Associative, _) --> Bool
end

abstract Iterable{T,S}
    start :: Iterable --> S
    done  :: (Iterable,S) --> Bool
    next  :: (Iterable,S) --> (T,S)
end

abstract Indexable{A,I}
    getindex  :: (A,I) --> eltype(A)
    setindex! :: (A,I) --> A
    get! :: (A, I, eltype(A)) --> eltype(A)
    get :: (A, I, eltype(A)) --> eltype(A)
end

abstract Eq{A,B}
    == :: (A,B) --> Boolean
end
...

So, basically abstract types can then have generic functions as fields (i.e. become an interface) whereas concrete types have just normal fields. This may for example solve the problem of too many things being derived of AbstractArray, as people could just pick the useful pieces for their container as opposed to derive from AbstractArray.

If this is at all a good idea, there is a lot to be worked out (in particular how to specify types and type parameters), but maybe worth a thought?

@ssfrr commented above that interfaces and multiple dispatch are incompatible. That shouldn't be the case as, for instance, in Haskell multimethods are only possible by using type classes.

rfourquet commented 10 years ago

I also found while reading @StefanKarpinski write-up that using directly abstract instead of interface could make sense. However in this case, it is important that abstract inherits one crucial property of interface: the possibility for a type to implement an interface after being defined. Then I can use a type typA from lib A with an algorithm algoB from lib B by declaring in my code that typA implements the interface required by algoB (I guess that this implies that concrete types have a kind of open multiple inheritance).

quinnj commented 10 years ago

@mauro3, I actually really like your suggestion. To me, it feels very "julian" and natural. I also think it's a unique and powerful integration of interfaces, multiple inheritance, and abstract type "fields" (though, not really, since the fields would only be methods/functions, not values). I also think this melds well with @StefanKarpinski's idea of distinguishing "inner" vs. "outer" interface methods, since you could implement his proposal for the sort! example by declaring abstract Algorithm and Algorithm.sort!.

abitlong commented 10 years ago

sorry everybody

------------------ 原始邮件 ------------------ 发件人: "Jacob Quinn"notifications@github.com; 发送时间: 2014年9月12日(星期五) 上午6:23 收件人: "JuliaLang/julia"julia@noreply.github.com; 抄送: "Implement"369002291@qq.com; 主题: Re: [julia] Interfaces for Abstract Types (#6975)

@mauro3, I actually really like your suggestion. To me, it feels very "julian" and natural. I also think it's a unique and powerful integration of interfaces, multiple inheritance, and abstract type "fields" (though, not really, since the fields would only be methods/functions, not values). I also think this melds well with @StefanKarpinski's idea of distinguishing "inner" vs. "outer" interface methods, since you could implement his proposal for the sort! example by declaring abstract Algorithm and Algorithm.sort!.

— Reply to this email directly or view it on GitHub.

pao commented 10 years ago

@implement Very sorry; not sure how we pinged you. If you didn't already know, you can remove yourself from those notifications using the "Unsubscribe" button on the right-hand side of the screen.

abitlong commented 10 years ago

No,I just want to say I can't help you too much to say sarry

------------------ 原始邮件 ------------------ 发件人: "pao"notifications@github.com; 发送时间: 2014年9月13日(星期六) 晚上9:50 收件人: "JuliaLang/julia"julia@noreply.github.com; 抄送: "Implement"369002291@qq.com; 主题: Re: [julia] Interfaces for Abstract Types (#6975)

@implement Very sorry; not sure how we pinged you. If you didn't already know, you can remove yourself from those notifications using the "Unsubscribe" button on the right-hand side of the screen.

— Reply to this email directly or view it on GitHub.

pao commented 10 years ago

We don't expect you to! It was an accident, since we're talking about a Julia macro with the same name as your username. Thanks!

mauro3 commented 10 years ago

I just saw that there are some potentially interesting features (maybe relevant to this issue) worked on in Rust: http://blog.rust-lang.org/2014/09/15/Rust-1.0.html, in particular: https://github.com/rust-lang/rfcs/pull/195

mauro3 commented 9 years ago

After seeing THTT ("Tim Holy Trait Trick"), I gave interfaces/traits some more thought over the last few weeks. I came up with some ideas and an implementation: Traits.jl. First, (I think) traits should be seen as a contract involving one or several types. This means that just attaching the functions of an interface to one abstract type, as I and others suggested above, does not work (at least not in the general case of a trait involving several types). And second, methods should be able to use traits for dispatch, as @StefanKarpinski suggested above.

Nuff said, here an example using my package Traits.jl:

@traitdef Eq{X,Y} begin
    # note that anything is part of Eq as ==(::Any,::Any) is defined
    ==(X,Y) -> Bool
end

@traitdef Cmp{X,Y} <: Eq{X,Y} begin
    isless(X,Y) -> Bool
end

This declares that Eq and Cmp are contracts between types X and Y. Cmp has Eq as a supertrait, i.e. both the Eq and Cmp need to be fulfilled. In the @traitdef body, the function signatures specify what methods need to be defined. The return types do nothing at the moment. Types do not need to explicitly implement a trait, just implementing the functions will do. I can check whether, say, Cmp{Int,Float64} is indeed a trait:

julia> istrait(Cmp{Int,Float64})
true

julia> istrait(Cmp{Int,String})
false

Explicit trait implementation is not in the package yet but should be fairly straightforward to add.

A function using trait-dispatch can be defined like so

@traitfn ft1{X,Y; Cmp{X,Y}}(x::X,y::Y) = x>y ? 5 : 6

This declares a function ft1 which takes two argument with the constraint that their types need to fulfil Cmp{X,Y}. I can add another method dispatching on another trait:

@traitdef MyT{X,Y} begin
    foobar(X,Y) -> Bool
end
# and implement it for a type:
type A
    a
end
foobar(a::A, b::A) = a.a==b.a

@traitfn ft1{X,Y; MyT{X,Y}}(x::X,y::Y) = foobar(x,y) ? -99 : -999

These trait-functions can now be called just like normal functions:

julia> ft1(4,5)
6

julia> ft1(A(5), A(6))
-999

Adding other type to a trait later is easy (which wouldn't be the case using Unions for ft1):

julia> ft1("asdf", 5)
ERROR: TraitException("No matching trait found for function ft1")
 in _trait_type_ft1 at

julia> foobar(a::String, b::Int) = length(a)==b  # adds {String, Int} to MyTr
foobar (generic function with 2 methods)

julia> ft1("asdf", 5)
-999

Implementation of trait functions and their dispatch is based on Tim's trick and on staged functions, see below. Trait definition is relatively trivial, see here for a manual implementation of it all.

In brief, trait dispatch turns

@traitfn f{X,Y; Trait1{X,Y}}(x::X,y::Y) = x+y

into something like this (a bit simplified)

f(x,y) = _f(x,y, checkfn(x,y))
_f{X,Y}(x::X,y::Y,::Type{Trait1{X,Y}}) = x+y
# default
checkfn{T,S}(x::T,y::S) = error("Function f not implemented for type ($T,$S)")
# add types-tuples to Trait1 by modifying the checkfn function:
checkfn(::Int, ::Int) = Trait1{Int,Int}
f(1,2) # 3

In the package, the generation of checkfn is automated by stagedfuncitons. But see the README of Traits.jl for more details.

Performance For simple trait-functions the produced machine code is identical to their duck-typed counterparts, i.e. as good as it gets. For longer functions there are differences, up to ~20% in length. I'm not sure why as I thought this should all be inlined away.

(edited 27 Oct to reflect minor changes in Traits.jl)

eschnett commented 9 years ago

Is the Traits.jl package ready for exploring? The readme says "implement interfaces with @traitimpl (not done yet...)" -- is this an important shortcoming?

mauro3 commented 9 years ago

It's ready for exploring (including bugs :-). The missing @traitimpl just means that instead of

@traitimpl Cmp{T1, T2} begin
   isless(t1::T1, t2::T2) = t1.t < t2.f
end

you just define the function(s) manually

Base.isless(t1::T1, t2::T2) = t1.t < t2.f

for two of your types T1 and T2.

mauro3 commented 9 years ago

I added the @traitimpl macro, so above example now works. I also updated the README with details on usage. And I added an example implementing part of @lindahua Graphs.jl interface: https://github.com/mauro3/Traits.jl/blob/master/examples/ex_graphs.jl

ssfrr commented 9 years ago

This is really cool. I particularly like that it recognizes that interfaces in general are a property of tuples of types, not individual types.

JeffBezanson commented 9 years ago

I also find this very cool. There's a lot to like about this approach. Nice work.

johnmyleswhite commented 9 years ago

:+1:

mauro3 commented 9 years ago

Thanks for the good feedback! I updated/refactored the code a bit and it should reasonably bug-free and good for playing around. At this point it would probably be good, if people can give this a spin to see whether it fits their use-cases.

tonyhffong commented 9 years ago

This is one of those packages that makes one look at his/her own code in new light. Very cool.

timholy commented 9 years ago

Sorry I haven't had time to look this over seriously yet, but I know that once I do I'll want to refactor some stuff...

skariel commented 9 years ago

I'll refactor my packages too :)

I was wondering, it seems to me that if traits are available (and allow for multiple dispatch, like the suggestion above) then there is no need for an abstract type hierarchy mechanism, or abstract types at all. Could this be?

After traits get implemented every function in base and later in the whole ecosystem would eventually expose a public api based solely on traits, and abstract types would disappear. Of course the process could be catalyzed by deprecating abstract types

skariel commented 9 years ago

Thinking of this a bit more, replacing abstract types by traits would require parametrizing types like this:

Array{X; Cmp{X}} # an array of comparables
myvar::Type{X; Cmp{X}} # just a variable which is comparable

I agree with mauro3 point above, which having traits (by his definition, which i think is very good) is equivalent to abstract types which

I would also add that to allow for traits to be assigned to types after their definition one would also need to allow for "lazy inheritance" ie to tell the compiler that a type inherits from some abstract type after it was defined.

so all in all seems to me that developping some trait/interface concept outside of abstract types would induce some duplication, introducing different ways to achieve the same thing. I now think the best way to introduce these concepts is by slowly adding features to abstract types

EDIT: of course at some point inheriting concrete types from abstract ones would have to be deprecated and finally disallowed. Type traits would be determined implicitly or explicitly but never by inheritance