-
Notifications
You must be signed in to change notification settings - Fork 6
[WIP] first stab at parametrically-typed trait (e.g. monad) #4
Conversation
Cool that you're having a stab at this! I haven't read up on monads yet but couldn't this be done with associated types within the current framework of Traits.jl? Like so: @traitdef Monad{X,Y} begin
XY = X{Y} # this is an associated type
mreturn(::X, Y) -> XY # we cannot infer X so we have to supply it
bind( XY, FuncFullSig{Y, XY} ) -> XY
end |
Yes, you are probably right in this case, but I can imagine when we need to coordinate a few parametric types to interact in a specific ways, like so @traitdef FastIndexable{C{X}, I{X}} begin
# contracts requiring the same parametric data type supplied to both C and I
end In any case, I don't really have all the answers but this is kind of fun. |
I agree that it might be nice to have some sugar to get at type parameters but probably not just now until things settle down a bit. Unless, of course, it turns out it's not sugar but an essential nutrient. I look forward to the completed monad example... |
Actually, after thinking some more about the frame work, I can make the following distinction
The rest of Traits.jl mechanism probably warrants this viewpoint. I probably want to do this @traitfn tf{ X; MyParaTr{X} }( x::X ) = ... So X could be I probably don't want to spell it out here for Trait.jl that X is actually parametric, like this: @traitfn tf{ X,Y; MyParaTr{X,Y} }( x::X{Y} ) = ... That'd be very unwieldy. |
I think what you spell out with your second bullet point is exactly the use-case for associated types. I can try to dig up my old internet searches on those, here one link: http://nattermorphisms.blogspot.ch/2008/10/2-minute-intro-to-associated-types-type.html Traits.jl has "Associated (Data) Type" in his classification. But rust has some better descriptions. The way to implement it then is: @traitdef Monad{XY} begin # so XY could be Nullable{Int}, Nullable by itself does not work!
X = Traits.deparameterize_type(XY)
Y = XY.parameters[1]
mreturn(::X, Y) -> XY # we cannot infer X so we have to supply it
bind( XY, FuncFullSig{Y, XY} ) -> XY
end
@assert istrait(Monad{Array{Int,1}}) Note that |
Yes on the ex = :( @traitdef Monad{X{Y}} begin
mreturn(::X, Y) -> X{Y}
bind( X{Y}, FuncFullSig{Y, X{Y}} ) -> X{Y}
end )
macroexpand(ex)
# note the popping of type parameters, and curly can be chained e.g. MyType{X}{Y} == Mytype{X,Y}
:(immutable Monad{X} <: Traits.Trait{()} # /Users/tonyfong/.julia/v0.4/Traits/src/traitdef.jl, line 321:
methods::Dict{Union(Function,DataType),Tuple} # line 322:
constraints::Vector{Bool} # line 323:
assoctyps::Vector{TypeVar} # line 324:
function Monad() # /Users/tonyfong/.julia/v0.4/Traits/src/traitdef.jl, line 325:
begin
X0 = Traits.tparpop(X)
Y = Traits.tparlast(X)
assoctyps = TypeVar[TypeVar(symbol("X0"),X0),TypeVar(symbol("Y"),Y)]
end # line 326:
new($(Expr(:dict, :(mreturn => ((Type{X0},Y),(X0{Y},))), :(bind => ((X0{Y},FuncFullSig{Y,X0{Y}}),(X0{Y},))))),Bool[],assoctyps)
end
end) Also, since we cannot test |
I don't quite understand On one hand I am a bit hesitant to add sugar for something which can perfectly fine be written using associated types. However, I suspect that this might be one of the most common uses of associated types so nicer syntax would be good. Let me know when you think this becomes mergable, also could you add tests? |
I will definitely add test and docs, as I want to also fresh out |
Sounds good, thanks! |
changes in the underlying code changes. Add documentations.
I have added some very simple tests for a start. I also cleanup some existing tests as the func |
One more thing, as I'm working my way through Does it make sense to define a way to get the "base type" and the parameter inside a traitfn? For example, @traitfn f{X,Y; Tr1{X}, Tr{Y}, @tbase(X) == @tbase(Y), @tparam(X) <: @tparam(Y) }( x::X, y::Y ) = ... or is it easier to put the burden on a combo trait like so: @traitdef ComboTr{X{Z1},Y{Z2}} begin
...
@constraints begin
istrait( Tr{X{Z1}} )
istrait( Tr{Y{Z2}} )
X == Y
Z1 <: Z2
end
end
@traitfn f{X,Y; CombTr{X,Y}}( x::X, y::Y ) ... It also feels to me that scoring on the match against |
I think your example is equivalent to: @traitdef ComboTr{X{Z1},Y{Z2}} <: Tr{X{Z1}},Tr{Y{Z2}} begin
...
@constraints begin
X == Y
Z1 <: Z2
end
end I've pondered syntax for making traits on the fly inside |
In essence listing several traits in a |
Yes, I agree. Thanks for pointing out the better syntax. |
Thanks for working on this :-) |
With parametric traits, ambiguous matching can be quite severe if we stick to the parameter count scheme. I'm making the following proposal:
I think the script The scale factors are clearly arbitrary, but so far I'm happy with the result. |
# for parametric trait, | ||
@traitimpl SemiFunctor{Nullable{T}} begin | ||
fmap{T}( f::Function, x::Nullable{T}) = Nullable(f(x.value)) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This implementation of fmap
will make a non-null value out of a null value:
julia> Nullable(sin(Nullable{Int}().value))
Nullable(0.8414709848078965)
Is that intended?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No it's not. I'll fix.
@traitdef SemiFunctor{X{Y}} begin | ||
fmap( Function, X{Y} } -> Any | ||
end | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could the return type not be more specific here? Otherwise there seems to be no need have a parametric type if none of the parameters actually feature.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I thought about that, but for staging to work, I think we would need access to the input/output type of functions during the staging phase. Here's a recent comment I made on this topic: JuliaLang/julia#7474 (comment)
Yep, I also thought that this issue is getting a bit out of hand. Does your Monad stuff work without the new dispatch rules? If so, could you clear the rest out and just leave that in. Which I can then merge. For the dispatch related stuff I opened #5, let's move the discussion and your dispatch code to there. |
Well, it won't work well, but I'm happy to revert to the existing dispatch rule and then go from there. I'll let you know when I'm done. |
Cool, thanks! |
The example/monads.jl will fail because of dispatch ambiguities. But the basics of the parametric trait is tested out in |
Thanks a lot Tony for your work! Sorry but it will probably be early next week before I can properly review it. |
My pleasure! Nothing like learning something by diving into it. |
Tony, I started looking at the code, finally... (sorry this is taking so long). I updated Traits and rebased your branch, its here: https://github.com/mauro3/Traits.jl/tree/tf/monads + one extra commit from me. Maybe take it from there, saves you doing the rebase. I encountered a few questions: What do the @traitimpl SemiMonad{ Array{Y...} } begin
mreturn{Y}( ::Type{Array}, x::Y ) = Y[x]
bind{Y}( f::Function, x::Array{Y,1} ) = [ f(_) for _ in x ]
end What is the difference between What is the purpose of the
Other notes:
|
Probably the @traitimpl SemiMonad{ TypeWith5Paras{_,_,Y,_,_} } begin
mreturn{Y}( ::Type{Array}, x::Y ) = Y[x]
bind{Y}( f::Function, x::Array{Y,1} ) = [ f(_) for _ in x ]
end I recall that traits on types without all their parameters specified are very fiddly, so maybe forcing the user to be specific is good? |
The way I think about this is that in a context of a trait, a parametric type can be decomposed as
The I don't have a strong reason not to use explicit I'll revert with further documentation. |
I see, that's clever! Thanks for the explanation. One thing about the syntax: in Julia |
That is a good point. We should strive for consistency. Let's consider some alternatives:
what do you think? |
Also, so far it was not necessary to use |
So I'm leaning toward "good". |
If we take |
I thought some more about this. What happens if you actually want to specify some concrete type-parameters? Say I want parameter @traitdef Tr{X{Y}} ... end
type A{a,b,c} end
@traitimpl Tr{A{_Int_, Int, 3}}
...
end So besides a "I don't care at all" Anyway, as a somewhat related side-note: until parameterized types cannot be specified more precisely, Traits.jl will struggle. Because the contracts between types are encoded in methods, which have richer possibilities to talk about types than types themselves. This might change if JuliaLang/julia#6984 gets off the ground. |
Not sure I follow. The way I understood the framework, |
I can implement a non-parametric trait in a few ways for a parametric type: @traitimpl Tr{Array}...
@traitimpl Tr{Array{Int}}...
@traitimpl Tr{Array{Int,1}}... Whereas (as implemented currently) for a parametric trait only the parameter before the special parameter can be given. Not saying that we need to implement that as well, just musing. |
@tonyhffong, I did some major updates with #10. I'm not sure how that impacts on this PR. I'll try and look into it, although just now I probably used up my Traits.jl time for a wee while. I saw over in Lint.jl that you will not have so much time to spend on Julia anymore, this is a shame, I was hoping to have you on board here! |
Don't worry about it. I'll take a look, though no promises on timing. |
As discussed today at JuliaCon: @traitdef Tr1{X{Y}} begin
f(..) = ...
end
# the X{Y} is essentially sugar for
@traitdef Tr1{X} begin
Y = traitpara(X)
end
# and would need to be defined like so
traitpara{K,N}(::Tr1, ::Array{K,N}) = K
traitpara{K,N}(::Tr1, ::Dict{K,V}) = V
# Example:
@traitdef IsIndexable{X{I}} begin
getindex(X, I)
end
traitpara{K,N}(::Tr1, ::Dict{K,N}) = K
traitpara(::Tr1, ::Array) = Int
# implement macro:
@traitimpl Tr1{Vector} begin
@traitpara{T}(::Array{T,1}) = T
f{T}(A) = 1
end |
Hi @mauro3 , as I hack through the code, I think there's an even better way, using using Traits
@traitdef Functor{X{Y}} begin
fmap(Function,Y) -> X
end
@traitimpl Functor{ Array{T,N} -> T } begin
fmap{T}( f::Function, x::Array{T,1} ) = map( f, x )
end With the arrow construct, we solve the problem of multiple-parameters in a trait, the order of the parameters, AND the flexibility to define "diagonal parametric trait" such as @traitdef Tr{X{Y,Z}} begin
f(X,Y) -> Z
end
# note the repeated parameters in (T,T) which satisfy Y,Z above
@traitimpl Tr{ Array{T,N} -> (T,T) } begin
f{T}( x::Array{T,1}, y::T ) = sum(x)+y
end |
This looks good, although I haven't quite grasped all its implications. One nit-pick, should it be:
|
julia these days don't like the
But you raised an interesting point, in the opposite direction: would it be easier to read if we make the parameter in parenthesis or square bracket? |
You're right about the warning. I was already in the
Concerning your question, I think the |
Very experimental. Do not merge.
Just trying to get this out to get some feedback.
The key new thing is that trait definition allows another layer of curly, i.e.
So that any datatype supplied must be parametric. How do we get
Y
in actual usage? we have two choicesX.parameters
X.parameters
I'm not sure which one is more kosher, and how to switch to the other one if we want to (a syntax issue).
Also,
istrait
currently cannot successfully test if a parametric datatype can satisfy a trait. There is still work to do.I have made a simple "Nullable monad" example using this framework.