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.
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 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.
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 @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.
@lindahua Thats an interesting example. Have to think about that.
@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.
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.
Thanks @ivarne. So the implementation could look as follows:
has
declaration is parsed.MethodError
needs to look up if the current function is part of the global dictionary.Most of the logic will then be in MethodError
.
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.
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
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: 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.
@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 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.
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 verify_interface
after the function declarations (or in a unit test) or defer it to the MethodError
.
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 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?
@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.
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:
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.
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.
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).
@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!
.
sorry everybody
------------------ 原始邮件 ------------------
发件人: "Jacob Quinn"[email protected];
发送时间: 2014年9月12日(星期五) 上午6:23
收件人: "JuliaLang/julia"[email protected];
抄送: "Implement"[email protected];
主题: 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.
@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.
No,I just want to say I can't help you too much to say sarry
------------------ 原始邮件 ------------------
发件人: "pao"[email protected];
发送时间: 2014年9月13日(星期六) 晚上9:50
收件人: "JuliaLang/julia"[email protected];
抄送: "Implement"[email protected];
主题: 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.
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!
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
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
)
Is the Traits.jl package ready for exploring? The readme says "implement interfaces with @traitimpl (not done yet...)" -- is this an important shortcoming?
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
.
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
This is really cool. I particularly like that it recognizes that interfaces in general are a property of tuples of types, not individual types.
I also find this very cool. There's a lot to like about this approach. Nice work.
:+1:
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.
This is one of those packages that makes one look at his/her own code in new light. Very cool.
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...
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
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
Aren't abstract types just a "boring" example of traits ?
If so, might it be possible to keep the current syntax and simply change it's meaning to trait (giving the ortogonal freedom etc. if the user wants it) ?
_I wonder if this might also be able to address the Point{Float64} <: Pointy{Real}
example (not sure if there's an issue number)?_
Yes, I think you are right. Trait functionality can be achieved by enhancing current julia abstract types. They need
1) multiple inheritance
2) function signatures
3) "lazy inheritance", to explicitly give an already defined type a new trait
Seems like lots of work, but Maybe this can be grown out slowly without much breakage for the community. So at least we got that ;)
I think whatever we choose will be a big change, one that we're not ready to start working on in 0.4. If I had to guess, I'd wager that we're more likely to move in the direction of traits than in the direction of adding traditional multiple inheritance. But my crystal ball is on the fritz, so it's hard to be sure what will happen without just trying stuff.
FWIW, I found Simon Peyton-Jones' discussion of typeclasses in the talk below really informative about how to use something like traits in lieu of subtyping: http://research.microsoft.com/en-us/um/people/simonpj/papers/haskell-retrospective/ECOOP-July09.pdf
Yep, a whole can of worms!
@johnmyleswhite, thanks for the link, very interesting. Here a link to the video of it, which is well worth watching to fill in the gaps. That presentation seems to touch on a lot of questions we got here. And interestingly, the implementation of type-classes is pretty similar to what's in Traits.jl (Tim's trick, traits being datatypes). Haskell's https://www.haskell.org/haskellwiki/Multi-parameter_type_class is a lot like Traits.jl. One of his questions in the talk is: "once we have wholeheartedly adopted generics, do we still really need subtyping." (generics are parametric-polymorphic functions, I think,see) Which is kinda what @skariel and @hayd have been musing about above.
Referring to @skariel and @hayd, I think single parameter traits (as in Traits.jl) are very close to abstract types indeed, except that they can have another hierarchy, i.e. multiple inheritance.
But multi-parameter traits seem to be a bit different, at least they were in my mind. As I saw them, type-parameters of abstract types seem to be mostly about what other types are contained within a type, e.g., Associative{Int,String}
says that the dict contains Int
keys and String
values. Whereas Tr{Associative,Int,String}...
says that there is some "contract" between Associative
, Int
s and Strings
. But then, maybe Associative{Int,String}
should be read that way too, i.e. there are methods like getindex(::Associative, ::Int) -> String
, setindex!(::Associative, ::Int, ::String)
...
@mauro3 The important thing would be to pass objects of type Associative
as argument to a function, so that it can then create Associative{Int,String}
itself:
function f(A::Associative)
a = A{Int,String}() # create new associative
a[1] = "one"
return a
end
You would call this e.g. as f(Dict)
.
@eschnett, sorry, I don't understand what you mean.
@mauro3 I think I was thinking in a too complicated way; ignore me.
I updated Traits.jl with:
@doc
for helpSee https://github.com/mauro3/Traits.jl/blob/master/NEWS.md for details. Feedback welcome!
@Rory-Finnegan put together an interface package https://github.com/Rory-Finnegan/Interfaces.jl
I recently discussed this with @mdcfrancis and we think something similar to Clojure's protocols would be simple and practical. The basic features are (1) protocols are a new kind of type, (2) you define them by listing some method signatures, (3) other types implement them implicitly just by having matching method definitions. You would write e.g.
protocol Iterable
start(::_)
done(::_, state)
next(::_, state)
end
and we have isa(Iterable, Protocol)
and Protocol <: Type
. Naturally, you can dispatch on these. You can check whether a type implements a protocol using T <: Iterable
.
Here are the subtyping rules:
let P, Q be protocol types
let T be a non-protocol type
| input | result |
| --- | --- |
| P <: Any | true |
| Bottom <: P | true |
| (union,unionall,var) <: P | use normal rule; treat P as a base type |
| P <: (union,unionall,var) | use normal rule |
| P <: P | true |
| P <: Q | check methods(Q) <: methods(P) |
| P <: T | false |
| T <: P | P's methods exist with T substituted for _ |
The last one is the big one: to test T <: P, you substitute T for _ in the definition of P and check method_exists
for each signature. Of course, by itself this means fallback definitions that throw "you must implement this" errors become a very bad thing. Hopefully this is more of a cosmetic issue.
Another problem is that this definition is circular if e.g. start(::Iterable)
is defined. Such a definition does not really make sense. We could somehow prevent this, or detect this cycle during subtype checking. I'm not 100% sure simple cycle detection fixes it, but it seems plausible.
For type intersection we have:
| input | result |
| --- | --- |
| P ∩ (union,unionall,tvar) | use normal rule |
| P ∩ Q | P |
| P ∩ T | T |
There are a couple options for P ∩ Q:
P ∩ T is tricky. T is a good conservative approximation, since non-protocol types are "smaller" than protocol types in the sense that they restrict you to one region of the type hierarchy, while protocol types don't (since any type at all can implement any protocol). Doing better than this seems to require general intersection types, which I would rather avoid in the initial implementation since that requires overhauling the subtyping algorithm, and opens worm-can after worm-can.
Specificity: P is only more specific than Q when P<:Q. but since P ∩ Q is always nonempty, definitions with different protocols in the same slot are often ambiguous, which seems like what you'd want (e.g. you'd be saying "if x is Iterable do this, but if x is Printable do that").
However there is no handy way to express the required disambiguating definition, so this should maybe be an error.
After #13412, a protocol can be "encoded" as a UnionAll _
over a Union of tuple types (where the first element of each inner tuple is the type of the function in question). This is a benefit of that design that didn't occur to me before. For example structural subtyping of protocols appears to just fall out automatically.
Of course, these protocols are the "single parameter" style. I like the simplicity of this, plus I'm not sure how to handle groups of types as elegantly as T <: Iterable
.
There was some comments around this idea in the past, x-ref https://github.com/JuliaLang/julia/issues/5#issuecomment-37995516.
Would we support, e.g.
protocol Iterable{T}
start(::_)::T
done(::_, state::T)
next(::_, state::T)
end
Wow, I really like this (especially with @Keno's extension)!
+1 This is exactly what I want!
@Keno That is definitely a nice upgrade path to have for this feature, but there are reasons to defer it. Anything involving return types is of course very problematic. The parameter itself is conceptually fine and would be awesome, but is a bit difficult to implement. It requires maintaining a type environment around the process that checks for existence of all methods.
Seems like you could shoe-horn in traits (like O(1) linear indexing for array-like types) into this scheme. You'd define a dummy method like hassomeproperty(::T) = true
(but _not_ hassomeproperty(::Any) = false
) and then have
protocol MyProperty
hassomeproperty(::_)
end
Could _
appear multiple times in the same method in the protocol definition, like
protocol Comparable
>(::_, ::_)
=(::_, ::_0
end
Could _ appear multiple times in the same method in the protocol definition
Yes. You just drop the candidate type in for every instance of _
.
@JeffBezanson really looking forward to it. Of particular note for me is the 'remoteness' of the protocol. In that I can implement a specific/custom protocol for a type without the author of the type having any knowledge of the existence of the protocol.
What about the fact that methods can be dynamically defined (eg with @eval
) at any time? Then whether a type is a subtype of a given protocol isn't knowable statically in general, which would seem to defeat optimizations that avoid dynamic dispatch in a lot of cases.
Yes, this makes #265 worse :) It's the same issue where dispatch and generated code needs to change when methods are added, just with more dependency edges.
It's good to see this advancing! Of course, I'd be the one to argue that multi-parameter traits are the way forward. But 95% of traits would probably be single parameter anyway. It's just that they would fit so nicely with multiple dispatch! This could probably be revisited later if needs be. Enough said.
A couple of comments:
The suggestion of @Keno (and really state
in Jeff's original) is known as associated types. Note that they are useful without return types as well. Rust has a decent manual entry. I think they are a good idea, although not as necessary as in Rust. I don't think it should be a parameter of the trait though: when defining a function dispatching on Iterable
I wouldn't know what T
is.
In my experience, method_exists
is unusable in its current form for this (#8959). But presumably this will get fixed in #8974 (or with this). I found matching method signatures against trait-sigantures the hardest part when doing Traits.jl, especially to account for parameterized & vararg functions (see).
Presumably inheritance would also be possible?
I would really like to see a mechanism which allows for definition of default implementations. The classic one is that for comparison you only need to define two out of =
, <
, >
, <=
, >=
. Maybe this is where the cycle mentioned by Jeff is actually useful. Continuing above example, defining start(::Indexable) = 1
and done(i::Indexable,state)=length(i)==state
would make those the defaults. Thus many types would only need to define next
.
Good points. I think associated types are somewhat different from the parameter in Iterable{T}
. In my encoding, the parameter would just existentially quantify over everything inside --- "does there exist a T such that type Foo implements this protocol?".
Yes, it seems we could easily allow protocol Foo <: Bar, Baz
, and simply copy the signatures from Bar and Baz into Foo.
Multi-parameter traits are definitely powerful. I think it's very interesting to think about how to integrate them with subtyping. You could have something like TypePair{A,B} <: Trait
, but that doesn't seem quite right.
I think that your proposal (in terms of features) is actually more like Swift than Clojure.
It seems weird (and I think a source of future confusion) to mix nominal (types) and structural (protocol) subtyping (but I guess that is unavoidable).
I'm also a bit skeptical of the expressive power of protocols for Math / Matrix operations. I think thinking through more complicated examples (matrix operations) would be more enlightening than Iteration which has a clearly specified interface. See for instance the core.matrix library.
I agree; at this point we should collect examples of protocols and see if they do what we want.
The way you're imagining this, would protocols be namespaces that their methods belong to? I.e. when you write
protocol Iterable
start(::_)
done(::_, state)
next(::_, state)
end
it would seem natural for this to define the generic functions start
, done
and next
and for their fully qualified names to be Iterable.start
, Iterable.done
and Iterable.next
. A type would implement Iterable
but implementing all of the generic functions in the Iterable
protocol. I proposed something very similar to this some time ago (can't find it now), but with the other side being that when you want to implement a protocol, you do this:
implement T <: Iterable
# in here `start`, `done` and `next` are automatically imported
start(x::T) = something
done(x::T, state) = whatever
next(x::T, state) = etcetera, nextstate
end
This would counteract the "remoteness" that @mdcfrancis mentioned, if I'm understanding it, but then again, I don't really see the benefit of being able to "accidentally" implement a protocol. Can you elaborate on why you feel that's beneficial, @mdcfrancis? I know Go makes a lot of this, but that seems to be because Go can't do duck typing, which Julia can. I suspect that having implement
blocks would eliminate almost all needs to use import
instead of using
, which would be a huge benefit.
I proposed something very similar to this some time ago (can't find it now)
Perhaps https://github.com/JuliaLang/julia/issues/6975#issuecomment-44502467 and earlier https://github.com/quinnj/Datetime.jl/issues/27#issuecomment-31305128? (Edit: Also https://github.com/JuliaLang/julia/issues/6190#issuecomment-37932021.)
Yup, that's it.
@StefanKarpinski quick comments,
If some sort of inheritance on protocols were allowed, MySuperIterabe,
could extend Base.Iterable, in order to reuse the existing methods.
The issue would be if you wanted just a selection of the methods in a
protocol, but that would seem to indicate that the original protocol should
be a composite protocol from the start.
@mdcfrancis – the first point is a good one, although what I'm proposing wouldn't break any existing code, it would just mean that people's code would have to "opt in" to protocols for their types before they could count on dispatch working.
Can you expand on the MyModule.MySuperIterable point? I'm not seeing where the extra verbosity comes from. You could have something like this, for example:
protocol Enumerable <: Iterable
# inherits start, next and done; adds the following:
length(::_) # => Integer
end
Which is essentially what @ivarne said.
In my specific design above, protocols are not namespaces, just statements about other types and functions. However this is probably because I'm focusing on the core type system. I could imagine syntax sugar that expands to a combination of modules and protocols, e.g.
module Iterable
function start end
function done end
function next end
jeff_protocol the_protocol
start(::_)
done(::_, state)
next(::_, state)
end
end
Then in contexts where Iterable is treated as a type, we use Iterable.the_protocol
.
I like this perspective because jeff/mdcfrancis protocols feel very orthogonal to everything else here. The lightweight feel of not needing to say "X implements protocol Y" unless you want to feels "julian" to me.
I don't know why I subscribed this issue and when I did. But it happens that this protocol proposal can solve the question I raised here.
I have nothing to add on a technical basis, but as an example of "protocols" being used in the wild in Julia (sort of) would be JuMP determining the functionality of a solver, e.g.:
https://github.com/JuliaOpt/JuMP.jl/blob/master/src/solvers.jl#L223-L246
# If we already have an MPB model for the solver...
if m.internalModelLoaded
# ... and if the solver supports updating bounds/objective
if applicable(MathProgBase.setvarLB!, m.internalModel, m.colLower) &&
applicable(MathProgBase.setvarUB!, m.internalModel, m.colUpper) &&
applicable(MathProgBase.setconstrLB!, m.internalModel, rowlb) &&
applicable(MathProgBase.setconstrUB!, m.internalModel, rowub) &&
applicable(MathProgBase.setobj!, m.internalModel, f) &&
applicable(MathProgBase.setsense!, m.internalModel, m.objSense)
MathProgBase.setvarLB!(m.internalModel, copy(m.colLower))
MathProgBase.setvarUB!(m.internalModel, copy(m.colUpper))
MathProgBase.setconstrLB!(m.internalModel, rowlb)
MathProgBase.setconstrUB!(m.internalModel, rowub)
MathProgBase.setobj!(m.internalModel, f)
MathProgBase.setsense!(m.internalModel, m.objSense)
else
# The solver doesn't support changing bounds/objective
# We need to build the model from scratch
if !suppress_warnings
Base.warn_once("Solver does not appear to support hot-starts. Model will be built from scratch.")
end
m.internalModelLoaded = false
end
end
Cool, that is useful. Is it sufficient for m.internalModel
to be the thing that implements the protocol, or are both arguments important?
Yes, it's sufficient for m.internalModel
to implement the protocol. The other arguments are mostly just vectors.
Yes, sufficient for m.internalModel
to implement the protocol
A good way to find examples of protocols in the wild is probably searching for applicable
and method_exists
calls.
Elixir also seems to implement protocols, but the number of protocols in the standard library (going off the the definition) seems quite limited.
What would be the relationship between protocols and abstract types? The original issue description proposed something like attaching a protocol to an abstract type. Indeed, it seems to me that most of the (now informal) protocols out there are currently implemented as abstract types. What would abstract types be used for when support for protocols is added? A type hierarchy without any way to declare its API doesn't sound too useful.
Very good question. There are a lot of options there. First, it's important to point out that abstract types and protocols are quite orthogonal, even though they are both ways of grouping objects. Abstract types are purely nominal; they tag objects as belonging to the set. Protocols are purely structural; an object belongs to the set if it happens to have certain properties. So some options are
If we have something like (2), I think it's important to recognize that it's not really a single feature, but a combination of nominal and structural typing.
One thing abstract types seem useful for is their parameters, for example writing convert(AbstractArray{Int}, x)
. If AbstractArray
were a protocol, the element type Int
would not necessarily need to be mentioned in the protocol definition. It's extra information about the type, _aside_ from which methods are required. So AbstractArray{T}
and AbstractArray{S}
would still be different types, despite specifying the same methods, so we've reintroduced nominal typing. So this use of type parameters seems to require nominal typing of some kind.
So would 2. give us multiple abstract inheritance?
So would 2. give us multiple abstract inheritance?
No. It would be a way of integrating or combining the features, but each feature would still have the properties it has now.
I should add that allowing multiple abstract inheritance is yet another nearly-orthogonal design decision. In any case, the problem with using abstract nominal types too heavily is (1) you might lose after-the-fact implementation of protocols (person A defines the type, person B defines the protocol and its implementation for A), (2) you might lose structural subtyping of protocols.
Aren't the type parameters in the current system somehow part of the implicit interface? For instance this definition relies on that: ndims{T,n}(::AbstractArray{T,n}) = n
and many user defined functions do too.
So, in a new protocol + abstract inheritance system we'd have a AbstractArray{T,N}
and ProtoAbstractArray
. Now a type which was nominally not an AbstractArray
would need to be able to specify what the T
and N
parameters are, presumably through hard-coding eltype
and ndims
. Then all the parameterized functions on AbstractArray
s would need to be re-written to use eltype
and ndims
instead of parameters. So, maybe it would make more sense to have the protocol carry the parameters too, so associated types might be very useful after all. (Note that concrete types would still need parameters.)
Also, a grouping of types into a protocol using @malmaud's trick: https://github.com/JuliaLang/julia/issues/6975#issuecomment-161056795 is akin to nominal typing: the grouping is solely due to picking types and the types share no (usable) interface. So maybe abstract types and protocols do overlap quite a bit?
Yes, the parameters of an abstract type are definitely a kind of interface, and to some extent redundant with eltype
and ndims
. The main difference seems to be that you can dispatch on them directly, without an extra method call. I agree that with associated types we'd be much closer to replacing abstract types with protocols/traits. What might the syntax look like? Ideally it would be weaker than method calling, since I'd rather not have a circular dependency between subtyping and method calling.
The remaining question is whether it's useful to implement a protocol _without_ becoming part of the related abstract type. An example might be strings, which are iterable and indexable, but are often treated as "scalar" quantities instead of containers. I don't know how often this arises.
I don't think I quite understand your "method calling" statement. So this suggestion for syntax may not be what you asked for:
protocol PAbstractArray{T,N}
size(_)
getindex(_, i::Int)
...
end
type MyType1
a::Array{Int,1}
...
end
impl MyType for PAbstractArray{Int,1}
size(_) = size(_.a)
getindex(_, i::Int) = getindex(_.a,i)
...
end
# an implicit definition could look like:
associatedT(::Type{PAbstractArray}, :T, ::Type{MyType}) = Int
associatedT(::Type{PAbstractArray}, :N, ::Type{MyType}) = 1
size(mt::MyType) = size(mt.a)
getindex(mt::MyType, i::Int) = getindex(mt.a,i)
# parameterized type
type MyType2{TT, N, T}
a::Array{T, N}
...
end
impl MyType2{TT,N,T} for PAbstractArray{T,N}
size(_) = size(_.a)
getindex(_, i::Int) = getindex(_.a,i)
...
end
That could work, depending on how subtyping of protocol types is defined. For example, given
protocol PAbstractArray{eltype,ndims}
size(_)
getindex(_, i::Int)
...
end
protocol Indexable{eltype}
getindex(_, i::Int)
end
do we have PAbstractArray{Int,1} <: Indexable{Int}
? I think this could work very well if the parameters are matched by name. We could also perhaps automate the definition that makes eltype(x)
return the eltype
parameter of x
's type.
I don't particularly like putting method definitions inside an impl
block, mostly because a single method definition might belong to multiple protocols.
So it looks like with such a mechanism, we would no longer need abstract types. AbstractArray{T,N}
could become a protocol. Then we automatically get multiple inheritance (of protocols). Also, the impossibility to inherit from concrete types (which is a complaint we sometimes hear from newcomers) is obvious, as only protocol inheritance would be supported.
Aside: it would be really nice to be able to express the Callable
trait. It would have to look something like this:
protocol Callable
::TupleCons{_, Bottom}
end
where TupleCons
separately matches the first element of a tuple, and the rest of the elements. The idea is that this matches as long as the method table for _
is non-empty (Bottom being a subtype of every argument tuple type). In fact we might want to make Tuple{a,b}
syntax for TupleCons{a, TupleCons{b, EmptyTuple}}
(see also #11242).
I do not think that is true, all type parameters are existentially quantified _with constraints_ so abstract types and protocols are not directly substitutable.
@jakebolewski can you think of an example? Obviously they will never be the exact same thing; I'd say the question is more whether we can massage one such that we can get by without having both.
Maybe I am missing the point, but how can protocols encode moderately complex abstract types with constraints, such as:
typealias BigMatrix ∃T, T <: Union{BigInt,BigFloat} AbstractArray{T,2}
without having to nominally enumerate every possibility?
The proposed Protocol
proposal is strictly less expressive compared to abstract subtyping which is all I was trying to highlight.
I could imagine the following (naturally, stretching the design to its practical limits):
BigMatrix = ∃T, T<:Union{BigInt, BigFloat} protocol { eltype = T, ndims = 2 }
going along with the observation that we need something like associated types or named type properties to match the expressiveness of existing abstract types. With this, we could potentially have near-compatibility:
AbstractArray = ∃T ∃N protocol { eltype=T, ndims=N }
Structural subtyping for the data fields of objects never seemed very useful to me, but applied to the properties of _types_ instead it seems to make lots of sense.
I also realized that this can provide an escape hatch from ambiguity problems: the intersection of two types is empty if they have conflicting values for some parameter. So if we want an unambiguous Number
type we could have
protocol Number
super = Number
+(_, _)
...
end
This is viewing super
as just another type property.
I do like the proposed protocol syntax, but I have some notes.
But then I may be misunderstanding everything. I only recently started really looking into Julia as something I want to work on, and I don't have an perfect grasp of the type system yet.
(a) I think it would be more interesting with the trait features @mauro3 worked on above. Especially because what good is multiple dispatch if you can't have multiple dispatch protocols! I'll write up my view of what a real world example is later. But the general jist of it comes down to "Is there behavior allowing these two objects to interact". I may be mistaken, and all of that can be encased in protocols, say by:
protocol Foo{bar}
...
end
protocol Bar{foo<:Foo}
...
end
And that also exposes the key problem of not allowing for the Foo protocol to reference the Bar protocol in the same definition.
(b)
do we have PAbstractArray{Int,1} <: Indexable{Int} ? I think this could work very well if the parameters are matched by name.
I'm not sure why we have to match the parameters by _name_ (I'm taking that to be the eltype
names, if I misunderstood please ignore this section). Why not just match the potential function signatures. My primary problem using naming is because it prevents the following:
module SomeBigLibrary
# Assuming required definitions
protocol Baz{el1type}
Base.foo(_, i::el1type) # say `convert`
baz(_)
end
end
module SomeOtherLibrary
# Assuming required definitions
protocol Bar{el2type}
Base.foo(_, i::el2type)
bar(_)
end
end
module My
# Assuming required definitions
protocol Protocol{el_type} # What do I put here to get both subtypes correctly!
Base.foo(_, i::el_type)
SomeBigLibrary.baz(_)
SomeOtherLibrary.bar(_)
end
end
On the other hand it does make sure your protocol only exposes the specific type hierarchy we want it too. If we don't name match Iterable
then we don't get benefits of implementing iterable (and also don't draw an edge in the dependency). But I'm not sure what a user _gain_s from that, besides the ability to do the following...
(c) So, I may be missing something, but isn't the primary purpose where named types are useful is in describing how different parts of a superset behave? Consider the Number
hierarchy and the abstract types Signed
and Unsigned
, both would implement the Integer
protocol, but would behave quite differently sometimes. To distinguish between them are we now forced to define a special negate
on only Signed
types (especially difficult without return types where we may actually want to negate an Unsigned
type)?
I think this is the problem you describe in the super = Number
example. When we declare bitstype Int16 <: Signed
(my other question is even how Number
or Signed
as protocols with their type properties get applied to the concrete type?) does that attach the type properties from the Signed
(super = Signed
) protocol marking it as being different from types marked by the Unsigned
protocol? Because that's a strange solution from my view, and not just because I find the named type parameters strange. If two protocols match exactly except the type they placed in super, how are they different anyway? And if the difference is in behaviors among subsets of a larger type (the protocol) then aren't we really just reinventing the purpose of abstract types?
(d) The problem is that we want abstract types to differentiate between behavior and we want protocols to ensure certain capabilities (often regardless of other behavior), through which behaviors are expressed. But we are trying to complect the capabilities that protocols allow us to ensure and the behaviors abstract types partition.
The solution we are often jumping to is along the lines of "have types declare their intent to implement an abstract class and check for compliance" which is problematic in implementation (circular references, leading to adding function definitions inside of the type block or impl
blocks), and removes the nice quality of protocols being based on the current set of methods and the types they operate on. These problems preclude putting protocols in the abstract hierarchy anyway.
But more importantly protocols don't describe behavior they describe complex capabilities across multiple functions (like iteration) the behavior of that iteration is described by the abstract types (whether it's sorted, or even ordered, for example). On the other hand the combination of protocol + abstract type is useful once we can get our hands on an actual type because it allows us to dispatch off of capabilities (capability utility methods), behaviors (high level methods), or both (implementation details methods).
(e) If we allow protocols to inherit multiple protocols (they are basically structural anyway) and as many abstract types as concrete types (e.g. without multiple abstract inheritance, one) we can allow for the creation of pure protocol types, pure abstract types, and protocol + abstract types.
I believe this fixes the Signed
vs. Unsigned
problem above:
IntegerProtocol
(inheriting whatever protocol structure, NumberAddingProtocol
, IntegerSteppingProtocol
, etc) one from the AbstractSignedInteger
and the other from AbstractUnsignedInteger
).Signed
type is guaranteed both functionality (from the protocol) and behavior (from the abstract hierarchy).AbstractSignedInteger
concrete type without the protocols isn't usable _anyway_.IntegerSteppingProtocol
(which is trivial and basically just an alias for a single function) existed for a given concrete AbstractUnsignedInteger
we could try to solve for Signed
by implementing the other protocols in terms of it. Maybe even with something like convert
.While keeping all of the existing types by turning most of them into protocol + abstract types, and leaving some as pure abstract types.
Edit: (f) Syntax example (including part (a)).
Edit 2: Corrected some mistakes (:<
instead of <:
), fixed up a poor choice (Foo
instead of ::Foo
)
protocol {T<: Number}(Foo <: AbstractFoo; Bar <: AbstractBar) # Abstract inheritance
IterableProtocol(::Foo) # Explicit protocol inheritance.
# Implicit protocol inheritance.
start(::Bar)
next(::Bar, state) # These states should really share an anonymous internal type
done(::Bar, state)
# Custom method for protocol involving both participants, defines Foo / Bar relationship.
set(::Foo, ::Bar, v::T)
# Custom method only on Bar
bar(::Bar)
end
# Protocols both Foo{T} and Bar{T}.
I see problems with this syntax as:
_Abstract type_ defines what an entity is. _Protocol_ defines what an entity does. Within a single package, these two concepts are interchangeable: an entity _is_ what it _does_. And "abstract type" is more direct. However, between two packages, there is a difference: you don't require what your client "is", but you do require what your client "does". Here, "abstract type" gives no information about it.
In my opinion, a protocol is a single dispatched abstract type. It can help the package extension and cooperation. Thus, within a single package, where the entities are closely related, use abstract type to ease the development (by profiting from multiple dispatch); between packages, where the entities are more independent, use protocol to reduce implementation exposure.
@mason-bially
I'm not sure why we have to match the parameters by name
I mean matching by name _as opposed to_ matching by position. These names would act like structurally-subtyped records. If we have
protocol Collection{T}
eltype = T
end
then anything with a property called eltype
is a subtype of Collection
. The order and position of these "parameters" doesn't matter.
If two protocols match exactly except the type they placed in super, how are they different anyway? And if the difference is in behaviors among subsets of a larger type (the protocol) then aren't we really just reinventing the purpose of abstract types?
That's a fair point. The named parameters do, in fact, bring back many of the properties of abstract types. I was starting with the idea that we might need to have both protocols and abstract types, then trying to unify and generalize the features. After all, when you declare type Foo <: Bar
currently, on some level what you've really done is set Foo.super === Bar
. So maybe we should support that directly, along with any other key/value pairs you might want to associate.
"have types declare their intent to implement an abstract class and check for compliance"
Yes, I'm against making that approach the core feature.
If we allow protocols to inherit multiple protocols ... and as many abstract types
Does this mean saying e.g. "T is a subtype of protocol P if it has methods x, y, z, and declares itself to be a subtype of AbstractArray"? I think this sort of "protocol + abstract type" is very similar to what you'd get with my super = T
property proposal. Admittedly, in my version I haven't yet figured out how to chain them intro a hierarchy like we have now (e.g. Integer <: Real <: Number
).
Having a protocol inherit from a (nominal) abstract type seems to be a very strong constraint on it. Would there be subtypes of the abstract type that did _not_ implement the protocol? My gut feeling is it's better to keep protocols and abstract types as orthogonal things.
protocol {T :< Number}(Foo :< AbstractFoo; Bar :< AbstractBar) # Abstract inheritance
IterableProtocol(Foo) # Explicit protocol inheritance.
# Implicit protocol inheritance.
start(Bar)
...
I don't understand this syntax.
{ }
and ( )
mean exactly?f(x::ThisProtocol)=...
mean, given that the protocol relates multiple types?then anything with a property called eltype is a subtype of Collection. The order and position of these "parameters" doesn't matter.
Aha, there was my misunderstanding, that makes more sense. Namely the ability to assign:
el1type = el_type
el2type = el_type
to solve my example problem.
So maybe we should support that directly, along with any other key/value pairs you might want to associate.
And this key/value feature would be on all types, as we would replace abstract with it. That's a nice general solution. Your solution makes a lot more sense to me now.
Admittedly, in my version I haven't yet figured out how to chain them intro a hierarchy like we have now (e.g. Integer <: Real <: Number).
I think you could use super
(e.g. with Integer
's super as Real
) and then either make super
special and act like a named type or add a way to add custom type resolution code (ala python) and make a default rule for the super
parameter.
Having a protocol inherit from a (nominal) abstract type seems to be a very strong constraint on it. Would there be subtypes of the abstract type that did not implement the protocol? My gut feeling is it's better to keep protocols and abstract types as orthogonal things.
Oh yes, the abstract constraint was entirely optional! My entire point was that protocols and abstract types are orthogonal. You would use abstract + protocol to make sure you get a combination of certain behavior _and_ associated capabilities. If you want only the capabilities (for utility functions) or only the behavior then you use them orthogonally.
Does this protocol have a name?
Two protocols with two names (Foo
, and Bar
) which come from the one block, but then I am used to using macros to expand multiple definitions like that. This part of my syntax was an attempt to solve part (a). If you ignore that then the first line could simply be protocol Foo{T <: Number, Bar <: AbstractBar} <: AbstractFoo
(with another, separate, definition for the Bar
protocol). Also, all of Number
, AbstractBar
and AbstractFoo
would be optional, like in normal type definitions,
What does the stuff inside { } and ( ) mean exactly?
The {}
is the stantard parametric type definition section. Allowing for the use of Foo{Float64}
to describe a type implementing the Foo
protocol using Float64
for example. The ()
is basically a variable binding list for the protocol body (so multiple protocols can be described at once). Your confusion is likely my fault because I mistyped :<
instead of <:
in my original. It may also be worth it to swap them to keep the <<name>> <<parametric>> <<bindings>>
structure, with where <<name>>
can sometimes be a list of bindings.
How do you use this protocol? Can you dispatch on it? If so, what does defining
f(x::ThisProtocol)=...
mean, given that the protocol relates multiple types?
Your dispatch example seems correct for it syntax wise in my opinion, indeed consider the following definitions:
protocol FooProtocol # Single protocol definition shortcut
foo(::FooProtocol) # I changed my syntax here, protocol names inside the protocol block should referenced as types
end
abstract FooAbstract
# This next line could use better syntax, like a type alias with an Intersection or something.
protocol Foo <: FooAbstract
FooProtocol(::Foo)
end
type Bar <: FooAbstract
a
end
type Baz
b
end
type Bax <: FooAbstract
c
end
f(f::Any) = ... # def (0)
foo(x::Bar) = ... # def (1a)
foo(x::Baz) = ... # def (1b)
f(x::FooProtocol) = ... # def (2); Least specific type (structural)
f(Bar(...)) # Would call def (2)
f(Baz(...)) # Would call def (2)
f(Bax(...)) # Would call def (0)
f(x::FooAbstract) = ... # def (3); Named type, more specific than structural
f(Bar(...)) # Would call def (3)
f(Baz(...)) # Would call def (2)
f(Bax(...)) # Would call def (3)
f(x::Foo) = ... # def (4); Named structural type, more specific than equivalent named type
f(Bar(...)) # Would call def (4)
f(Baz(...)) # Would call def (2)
f(Bax(...)) # Would call def (3)
Effectively protocols are using the named Top type (Any) unless given a more specific abstract type to check structure on. Indeed it may be worth it to allow something like typealias Foo Intersect{FooProtocol, Foo}
(_Edit: Intersect was the wrong name, perhaps Join instead Intersect was right the first time_) rather than use the protocol syntax to do it.
Ah great, that makes much more sense to me now! Defining multiple protocols together in the same block is interesting; I will have to think some more about that.
I cleaned up all my examples a few minutes ago. Earlier in the thread someone mentioned collecting a corpus of protocols to test ideas on, I think that's a great idea.
The multiple protocol's in the same block is sort of a pet peeve when I try to describe complex relationships between objects with correct-on-both-sides type annotations in define/compile as you load languages (e.g. like python; Java for example doesn't have the problem). On the other hand, most of them are probably easily fixed, usability wise, with multi-methods anyway; but performance considerations may come out of having the features typed within protocols correctly (optimizing protocols by specializing them to vtables say).
You mentioned earlier that protocols could be (spuriously) implemented by methods using ::Any
I think that would be a pretty simple case to simply ignore if it came to it. The concrete type would not classify as a protocol if the implementing method was dispatched on ::Any
. On the other hand I'm not sold that this necessarily a problem.
For starters if the ::Any
method is added after the fact (say because someone came up with a more generic system for handling it) it's still a valid implementation, and if we do use protocols as an optimization feature as well then specialized versions of ::Any
dispatched methods still work for performance gains. So in the end I'd actually be against ignoring them.
But it might be worth it to have a syntax which lets the protocol definer choose between the two options (which ever we make the default, allow the other). For the first, a forwarding syntax for the ::Any
dispatched method, say the global keyword (also see the next section). For the second a way to require a more specific method, I can't think of an existing useful keyword.
Edit: Removed a bunch of pointless stuff.
Your Join
is exactly the intersection of the protocol types. It's actually a "meet". And happily, the Join
type is not necessary, because protocol types are already closed under intersection: to compute the intersection, just return a new protocol type with the two lists of methods concatenated.
I'm not too worried about protocols getting trivialized by ::Any
definitions. To me the rule "look for matching definitions except Any
doesn't count" runs afoul of Occam's razor. Not to mention that threading the "ignore Any" flag through the subtyping algorithm would be pretty annoying. I'm not even sure the resulting algorithm is coherent.
I like the idea of protocols very much (reminds me a bit of CLUsters), I'm just curious, how would this fit in with the new subtyping that was discussed by Jeff at JuliaCon, and with traits? (two things I'd still really like to see in Julia).
This would add a new kind of type with its own subtyping rules (https://github.com/JuliaLang/julia/issues/6975#issuecomment-160857877). At first glance they seem to be compatible with the rest of the system and can just be plugged in.
These protocols are pretty much the "one parameter" version of @mauro3 's traits.
Your
Join
is exactly the intersection of the protocol types.
I somehow convinced myself I was wrong earlier when I said it was the intersection. Although we would still need a way to Intersect types in one line (like Union
).
Edit:
I still also like generalizing protocols and abstract types into one system and allowing custom rules for their resolution (e.g. for super
to describe the current abstract type system). I think if done right this would allow people to add custom type systems and eventually custom optimizations for those type systems. Although I'm not sure protocol would be the right keyword, but at least we could turn abstract
into a macro, that would be cool.
from the wheatfields: better to lift commonality through the protocoled and abstracted than to seek their generalization as destination.
what?
The process of generalizing the intent, capability and potential of protocols and that of abstract types is less effective a way of resolving their qualitatively most satisfying synthesis. It works better first to glean their intrinsic commonalities of purpose, pattern, process. And develop that understanding, allowing refinement of one's perspective to form the synthesis.
Whatever the fruitful realization be for Julia, it is constructed on scaffolding that synthesis offers. Clearer synthesis is constructive strength and inductive power.
What?
I think he's saying we should first figure out what we want from protocols and why they are useful. Then once we have that and abstract types it will be easier to come up with a general synthesis of them.
Mere Protocols
(1) advocating
A protocol may be extended to become a (more elaborated) protocol.
A protocol may be reduced to become a (less elaborated) protocol.
A protocol may be realized as a conforming interface [in software].
A protocol may be queried to determine conformance of an interface.
(2) suggesting
Protocols should support protocol-specific version nums, with default.
It would be good to support some manner of doing this:
When an interface conforms to a protocol, respond true; when an interface
is faithful to a subset of the protocol and would be conformant if augmented,
respond incomplete, and respond false otherwise. A function should list all
necessary augmentation for an interface that is incomplete w.r.t. a protocol.
(3) musing
A protocol could be a distinguished kind of module. Its exports would serve
as the initial comparand when determining whether some interface conforms.
Any protocol specified [exported] types and functions could be declared using
@abstract
, @type
, @immutable
and @function
to support innate abstraction.
[pao: switch to code-quotes, though note that the horse has already left the barn when you're doing this after the fact...]
(you need to quote the @mentions
!)
thanks -- fixing it
On Wed, Dec 16, 2015 at 3:01 AM, Mauro [email protected] wrote:
(you need to quote the @mentions!)
—
Reply to this email directly or view it on GitHub
https://github.com/JuliaLang/julia/issues/6975#issuecomment-165026727.
sorry I should have been more clear: code-quote using ` and not "
Fixed the quoting fix.
thanks -- pardon my prior ignorance
I tried to understand this recent discussion about adding a protocol type. Maybe I am misunderstanding something but why is it necessary to have named protocols instead just using the name of the associated abstract type that the protocol is about to describe?
In my point of view it is quite natural to extend the current abstract type system with some way to describe the behavior that is expected from the type. Much like initially proposed in this thread but maybe with Jeffs syntax
abstract Iterable
start(::_)
done(::_, state)
next(::_, state)
end
When going this route there would be no need to specially indicate that a subtype implements the interface. This would be implicitly done by subtyping.
The primary goal of an explicit interface mechanism is IMHO to get better error messages and to perform better verification test.
So a type declaration like:
type Foo <: Iterable
...
end
Do we define the functions in the ...
section? If not, when do we error about missing functions (and the complexities related to that)? Also, what happens for types that implement multiple protocols, do we enable multiple abstract inheritance? How do we handle super-method resolution? What does this do with multiple dispatch (it seems to just remove it and stick a java-esque object system in there)? How do we define new type specialization's for methods after the first type has been defined? How do we define protocols after we have defined the type?
These questions are all easier to solve by creating a new type (or creating a new type formulation).
There isn't necessarily a related abstract type for each protocol (there probably shouldn't be any really). Multiples of the current interfaces can be implemented by the same type. Which is not describable with the current abstract type system. Hence the problem.
verify_interface
method that can either be called after all function definitions or in a unit testfunction a()
b()
end
function b()
end
Thus, I don't think in-block function definitions would be required here.
I think the difference between a "protocol"-like thing and multiple inheritance is that a type can be added to a protocol after it has been defined. This is useful if you want to make your package (defining protocols) work with existing types. One could allow to modify the supertypes of a type after creation, but at that point it's probably better to call it "protocol" or some such.
Hm so it allows to define alternative/enhanced interfaces to existing types. Still not clear to me where this would be really required. When one wants to add something to an existing interface (when we follow the approach proposed in the OP) one would simply subtype and add additional interface methods to the subtype. This is the nice thing about that approach. It scales quite well.
Example: say I had some package which serializes types. A method tobits
needs to be implemented for a type, then all the functions in that package will work with the type. Let's call this the Serializer
protocol (i.e. tobits
is defined). Now I can add Array
(or any other type) to it by implementing tobits
. With multiple inheritance I couldn't make Array
work with Serialzer
as I cannot add a supertype to Array
after its definition. I think this is an important use-case.
Ok, understand this. https://github.com/JuliaLang/IterativeSolvers.jl/issues/2 is a similar issue, where the solution is basically to use duck-typing. If we could have something that solves this issue elegantly this would be indeed nice. But this is a something that has to be supported at the dispatch level. If I understand the protocol idea above correctly one could either put an abstract type or a protocol as the type annotation in function. Here it would be nice to merge these two concepts with a single tool that is powerful enough.
I agree: it will be very confusing to have both abstract types and protocols. If I recall correctly, it was argued above that abstract types have some semantics which cannot be modeled with protocols, i.e. abstract types have some feature which protocols don't have. Even if that is necessarily the case (I'm not convinced), it will still be confusing as there is such a big overlap between the two concepts. So, abstract types should be removed in favor of protocols.
As far as there is consensus above about protocols, they emphasize specifying interfaces. Abstract types may have been used to do some of that absent protocols. That does not mean it is their most important use. Tell me what protocols are and are not, then I could tell you how abstract types differ and some of what they bring. I have never considered abstract types to be as much about interface as about typology. Throwing away a natural approach to typological flexibility is costly.
@JeffreySarnoff +1
Think of the Number type hierarchy. The different abstract types e.g. Signed, Unsigned, are not defined by their interface. There is no set of methods which defines "Unsigned". It's simply a very useful declaration.
I don't see the problem, really. If both Signed
and Unsigned
types support the same set of methods, we can create two protocols with identical interfaces. Still, declaring a type as Signed
rather than Unsigned
can be used for dispatch (i.e. methods of the same function act differently). The key here is to require an explicit declaration before considering that a type implements a protocol, rather than detecting this implicitly based on the methods it implements.
But having implicitly associated protocols is also important, as in https://github.com/JuliaLang/julia/issues/6975#issuecomment-168499775
Protocols can not only define functions that can be called, but can also document (either implicitly, or in machine-testable ways) properties that need to hold. Such as:
abs(x::Unsigned) == x
signbit(x::Unsigned) == false
-abs(x::Signed) <= 0
This externally visibly behaviour difference between Signed
and Unsigned
is what makes this distinction useful.
If there exists a distinction between types that is so "abstract" that it cannot be immediately verified, at least theoretically, from the outside, then it's likely that one needs to know the implementation of a type to make the right choice. This is where the current abstract
might be useful. This probably goes in the direction of algebraic data types.
There is no reason why protocols should not be used to simply group types, i.e. without requiring any defined methods (and it is possible with the "current" design using the trick: https://github.com/JuliaLang/julia/issues/6975#issuecomment-161056795). (Also note, this does not interfere with implicitly defined protocols.)
Considering the (Un)signed
example: what would I do if I had a type which is Signed
but for some reason has to be also a subtype of another abstract type? This would not be possible.
@eschnett: abstract types, at the moment, have nothing to do with the implementation of their subtypes. Although that has been discussed: #4935.
Algebraic data types are a good example where successive refinement is intrinsically meaningful.
Any taxonomy is much more naturally given, and more directly useful as an abstract type hierarchy than as a melange of protocol specifications.
The note about having a type that is a subtype of more than one abstract type hierarchy is important, too. There is a good deal of utilitarian power that comes with multiple inheritance of abstractions.
@mauro3 Yes, I know. I was thinking of something equivalent to discriminated unions, but implemented as efficiently as tuples instead of via the type system (as unions are currently implemented). This would subsume enums, nullable types, and might be able to handle a few other cases more efficiently than abstract types currently.
For example, like tuples with anonymous elements:
DiscriminatedUnion{Int16, UInt32, Float64}
or with named elements:
discriminated_union MyType
i::Int16
u::UInt32
f::Float64
end
The point I was trying to make is that abstract types are one good way to map such a construct to Julia.
There is no reason why protocols should not be used to simply group types, i.e. without requiring any defined methods (and it is possible with the "current" design using the trick: #6975 (comment)). (Also note, this does not interfere with implicitly defined protocols.)
I feel like you would have to be careful with this to achieve performance, a consideration not many appear to be considering often enough. In the example it would seem one would want to simply define the non-any version so that the compiler could still choose the function at compile time (rather than having to call a function to choose the right one at runtime, or the compiler inspecting functions to determine their results). Personally I believe using multiple abstract "inheritance" as tags would be a better solution.
I feel we should keep required tricks and knowledge of the type system to a minimum (although it could be wrapped in a macro it would feel like a strange hack of a macro; if we are using macros to manipulate the type system then I think @JeffBezanson 's unified solution would better fix this problem).
Considering the (Un)signed example: what would I do if I had a type which is Signed but for some reason has to be also a subtype of another abstract type? This would not be possible.
Multiple abstract inheritance.
I believe all this ground has been covered before, this conversation appears to be going in circles (albeit tighter circles each time). I believe it was mentioned that a corpus or problems using protocols should be acquired. This would allow us to judge solutions more easily.
While we're reiterating things :) I want to remind everybody that abstract types are nominal while protocols are structural, so I favor designs that treat them as orthogonal, _unless_ we can actually come up with an acceptable "encoding" of abstract types in protocols (perhaps with a clever use of associated types). Bonus points, of course, if it also yields multiple abstract inheritance. I feel like this is possible but we're not quite there yet.
@JeffBezanson Are "associated types" distinct from "concrete types associated with [a] protocol"?
Yes I believe so; I mean "associated types" in the technical sense of a protocol specifying some key-value pair where the "value" is a type, in the same way that protocols specify methods. e.g. "type Foo follows the Container protocol if it has an eltype
" or "type Foo follows the Matrix protocol if its ndims
parameter is 2".
abstract types are nominal while protocols are structural and
abstract types are qualitative while protocols are operative and
abstract types (with multiple inheritance) orchestrate while protocols conduct
Even if there were an encoding of one in the other, the "hey there, Hi .. how are you? lets go do!" of Julia needs to present both clearly -- the generally purposeful notion of protocol and multinheritable abstract types (a notion of generalized purpose). If there is an artful unfolding that gives Julia both, separately enfolded, it is more likely done just so than one through one and other.
@mason-bially: so we should add multiple inheritance as well? This would still leave the problem that supertypes cannot be added after the creation of a type (unless that was also allowed).
@JeffBezanson: nothing would stop us to allow purely nominal protocols.
@mauro3 Why should the decision on whether to allow post facto supertype insertion be tied to multiple inheritance? And there are different sorts of supertype creation, some are patently harmless presupposing the ability to interpose a new whatever-they-are: I have wanted to add an abstract type between Real and AbstractFloat, say ProtoFloat, so that I could dispatch on double-double floats and system Floats together without interfering with the system Floats living as subtypes of AbstractFloat. Perhaps less easy to allow, would be the ability to subsect the current subtypes of Integer, and so avoid many "ambiguous with .. define f(Bool) before.." messages; or to introduce a supertype of Signed that is a subtype of Integer and open the numeric hierarchy to transparent handling of, say Ordinal numbers.
Sorry if I initiated another round of the circle. The topic is quite complex and we really have to make sure that the solution is super simple to use. So we need to cover:
Since what was initially proposed in #6975 is quite different to the protocol idea discussed later it may be good to have some sort JEP that describes what the protocols could look like.
An example of how you can define a formal interface and validate it using the current 0.4 (without macros), dispatch currently relies on the traits style dispatch unless there are modifications made to gf.c. This uses generated functions for the validation, all type computation is performed in type space.
I'm starting to use this as a runtime check in a DSL we are defining where I have to ensure that the type supplied is an iterator of dates.
It currently supports multiple inheritance of super types, the _super field name is not used by the runtime and can be any valid symbol. You can supply n other types to the _super Tuple.
https://github.com/mdcfrancis/tc.jl/blob/master/test/runtests.jl
Just pointing out here that I made a followup on a discussion from JuliaCon about possible syntax on traits at https://github.com/JuliaLang/julia/issues/5#issuecomment-230645040
Guy Steele has some good insights into traits in a multiple dispatch language (Fortress), see his JuliaCon 2016 keynote: https://youtu.be/EZD3Scuv02g .
A few highlights: big traits system for algebraic properties, unit testing of trait-properties for types which implement a trait, and that the system they implemented was maybe too complicated and that he'd do something simpler now.
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
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.
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.
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):
"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.
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.
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.
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:
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.
What's the best place for an interface documentation in a docstring? I would say the query function above.
- 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.
- 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).
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.
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.
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)
@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
documentation for more and more interfaces (in a docstring),
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})
.
@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.
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
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...).
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
Most helpful comment
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.