From 98064521d25ace3a0bbc36e686bd9ab136224f89 Mon Sep 17 00:00:00 2001 From: Tony Fong Date: Wed, 7 Jan 2015 20:50:00 +0700 Subject: [PATCH 01/12] first stab at parametrically-typed trait (e.g. monad) --- examples/monads.jl | 45 ++++++++++++++++++++++++++++ src/helpers.jl | 37 +++++++++++++++++------ src/traitdef.jl | 75 +++++++++++++++++++++++++++++++++++----------- 3 files changed, 131 insertions(+), 26 deletions(-) create mode 100644 examples/monads.jl diff --git a/examples/monads.jl b/examples/monads.jl new file mode 100644 index 0000000..e600acd --- /dev/null +++ b/examples/monads.jl @@ -0,0 +1,45 @@ +using Traits + +immutable FuncFullSig{TI, TO} + f::Function +end + +call{TI,TO}( fs::FuncFullSig{TI,TO}, x::TI ) = (fs.f(x))::TO + +@traitdef Monad{X{Y}} begin + mreturn(::X, Y) -> X{Y} # we cannot infer X so we have to supply it + bind( X{Y}, FuncFullSig{Y, X{Y}} ) -> X{Y} +end + +@traitimpl Monad{Nullable{Y}} begin + mreturn{Y}( ::Type{Nullable}, x::Y ) = Nullable{Y}(x) + bind{Y,Z}( x::Nullable{Y}, f::FuncFullSig{Y, Nullable{Z}} ) = begin + if isnull(x) + return Nullable{Z}() + else + try + return f.f( x.value )::Nullable{Z} + catch + return Nullable{Z}() + end + end + end +end + +@assert isequal( mreturn( Nullable, 1.0 ), Nullable{Float64}( 1.0 ) ) +testfunc( x::Float64 ) = x == 0.0 ? Nullable{Float64}() : Nullable{Float64}( 1.0/x ) +@assert isequal( bind( Nullable{Float64}( 2.0 ), FuncFullSig{Float64,Nullable{Float64}}( testfunc ) ), Nullable{Float64}( 0.5 ) ) +@assert isequal( bind( Nullable{Float64}( 0.0 ), FuncFullSig{Float64,Nullable{Float64}}( testfunc ) ), Nullable{Float64}() ) + +# Note: FuncFullSig{TO} is itself a Monad +#= +@traitfn mreturn{;Monad{FuncFullSig{T}}}( x::T ) = FuncFullSig{T,T}(_->x) +@traitfn mreturn{;Monad{Array{T}}}( a::T ) = T[ a ] +#@traitfn mreturn{;Monad{Nullable{T}}}( a::T ) = Nullable{T}(a) + +@traitfn function bind{;Monad{Array{T}}}( x::Array{T}, f::FuncFullSig{T, Array{T} } ) + T[ map( f.f, x )... ] +end +@traitfn function bind{;Monad{FuncFullSig{T}}}( x::FuncFullSig{T}, f::FuncFullSig{T, FuncFullSig{T} }) +end +=# diff --git a/src/helpers.jl b/src/helpers.jl index 7a1b08c..69b81fe 100644 --- a/src/helpers.jl +++ b/src/helpers.jl @@ -2,7 +2,7 @@ export deparameterize_type @doc """Removes type parameters from types, e.g. Array{Int}->Array. - + It is often useful to make an associated type with this to match against methods which do not specialize on the type parameters. """ -> @@ -109,6 +109,25 @@ function hasparameters(t::DataType) end end +function tpar1( t::DataType ) + for p in t.parameters + if typeof( p ) == DataType + return p + end + end + error( "Parametric Trait: tpar1: No type parameter found" ) +end + +function tparlast( t::DataType ) + for i in length(t.parameters):-1:1 + p = t.parameters[i] + if typeof( p ) == DataType + return p + end + end + error( "Parametric Trait: tparlast: No type parameter found" ) +end + # # check whether a function is parameterized # function isparameterized(m::Method) # if isa(m.tvars, Tuple) @@ -149,13 +168,13 @@ end # inds = ones(Int,len) # out = Array(Tuple, n) # for j=1:n -# try +# try # out[j] = tuple([typs[ii][i] for (ii,i) in enumerate(inds)]...) # catch e # @show j, inds # end # # updated inds -# inds[1] += 1 +# inds[1] += 1 # for i=1:len-1 # if inds[i]>lens[i] # inds[i] = 1 @@ -233,7 +252,7 @@ end # # if there are one or more methods, check them # for mm in meths # if is_fnparameter_match_inputs(mm[2]) -# ----------> check this +# ----------> check this # @show TS, mm[1], TS<:mm[1], mm[1]<:TS # if TS<:mm[1] # dbg_println("A parameterized method matches exactly: $TS") @@ -245,9 +264,9 @@ end # dbg_println("A parameterized cannot match: $TS") # return Res.F # end - + # end - + # # if length(meths)==1 && isparameterized(meths[1][3]) # # if is_parameter_match_inputs(meths[1][2]) # # if Base.typeseq(meths[1][1], TS) @@ -323,7 +342,7 @@ end # end # if r==Res.M # push!(checksTS, sTS) # needs further checking below -# end +# end # end # # recurse into subtypes where above was not # out = 0 @@ -333,9 +352,9 @@ end # dbg_println("False: one lower subtype does not check: $sTS") # return Res.F # end -# out += r # accumulate undicided +# out += r # accumulate undicided # end - + # if out==0 # dbg_println("True: all subtypes test true.") # return Res.T diff --git a/src/traitdef.jl b/src/traitdef.jl index 9cc100a..c6e7161 100644 --- a/src/traitdef.jl +++ b/src/traitdef.jl @@ -17,10 +17,15 @@ function parsecurly(def::Expr) # parses :(Cmp{x,y}) # into: :Cmp, [:x,:y], :(Cmp{x,y}), () + + # parses :(Monad{X{Y}}) + # into: :Monad, [:( :curly, :X, :Y )], :(Monad{X}), () name = def.args[1] - paras = Symbol[] - append!(paras,def.args[2:end]) - trait = def + paras = Any[] + append!(paras,def.args[2:end] ) + trait = Expr( :curly, name, + map( x->typeof(x)==Symbol ? x : Base.Meta.isexpr( x, :curly )? x.args[1] : error( "Traits: unknown " * string(x) ), + def.args[2:end])... ) return name, paras, trait, :(()) end function parsecomp(def::Expr) @@ -50,10 +55,10 @@ function parsetraithead(def::Expr) # trait = :(Cmp{X,Y} # supertraits = :(Eq{X,Y}, Tr1{X}) # paras = [:X,:Y] - # + # # Returns: # :(immutable Cmp{X,Y} <: Trait{(Eq{X,Y}, Tr1{X})} end) - + if def.head==:tuple # contains several parents name, paras, trait, supertraits = parsetuple(def) elseif def.head==:comparison # contains a <: @@ -70,13 +75,25 @@ function parsetraithead(def::Expr) end # make :(immutable Cmp{X,Y} <: Trait{(Eq{X,Y}, Tr1{X})} end) out = :(immutable $trait <: Traits.Trait{$supertraits} end) - return out, name + + # capture Y in Monad{X{Y}} + headassoc = Symbol[] + for p in paras + if Base.Meta.isexpr( p, :curly ) + @assert( typeof( p.args[2] ) == Symbol ) + if !in( p.args[2], headassoc ) + push!( headassoc, p.args[2] ) + end + end + end + + return out, name, paras, headassoc end # 2) parse the function definitions ### -function parsebody(body::Expr) +function parsebody(body::Expr, paras::Array{Any,1}, headassoc::Array{Symbol,1} ) # Transforms: # body = quote # R = g(X) @@ -93,10 +110,26 @@ function parsebody(body::Expr) # :(Bool[X==Y]) isassoc(ex::Expr) = ex.head==:(=) # associated types isconstraints(ex::Expr) = ex.head==:macrocall # constraints - + outfns = Expr(:dict) constr = :(Bool[]) assoc = quote end + # but first, add the assoc types from the head + for s in headassoc + local s_inited::Bool = false + for p in paras + if Base.Meta.isexpr( p, :curly ) && p.args[2] == s + hosttype = p.args[1] + if !s_inited + push!( assoc.args, :($s = Traits.tparlast( $hosttype ) ) ) + s_inited=true + else + push!( assoc.args, :( @assert( $s == Traits.tparlast( $hosttype ) ) ) ) + end + end + end + end + for ln in Lines(body) if isconstraints(ln) parseconstraints!(constr, ln) @@ -147,7 +180,15 @@ function parsefnstypes!(outfns, ln) "Something went wrong parsing the trait definition body with line:\n$ln")) end argtype = :() - append!(argtype.args, def.args[2:end]) + for i = 2:length( def.args ) + a = def.args[i] + if Base.Meta.isexpr( a, :(::) ) # shorthand + t = a.args[1] + push!( argtype.args, Expr( :curly, :Type, t ) ) + else + push!( argtype.args, a ) + end + end return fn, argtype, tvars end function parseret!(rettype, ln) @@ -161,7 +202,7 @@ function parsefnstypes!(outfns, ln) append!(rettype.args, tmp) end - + rettype = :() if ln.head==:tuple # several ret-types: @@ -169,7 +210,7 @@ function parsefnstypes!(outfns, ln) append!(rettype.args, ln.args[2:end]) ln = ln.args[1] end - + if ln.head==:(->) # f1(X,Y) -> x parseret!(rettype, ln) fn, argtype, tvars = parsefn(ln.args[1]) @@ -199,7 +240,7 @@ function parsefnstypes!(outfns, ln) subt2tvar!(rettype.args) translate!(rettype.args, trans) tvar2tvar!(rettype.args) - + push!(outfns.args, :($fn => ($argtype, $rettype))) end @@ -207,7 +248,7 @@ end ### @doc """The `@traitdef` macro is used to construct a trait. Example: - + ``` @traitdef MyArith{X,Y} begin # associated types @@ -228,7 +269,7 @@ end end istrait(MyArith{Int, Int8}) # -> true ``` - + - Assignments are for associated types, here `Z,D`. These are types which can be calculated from the input types `X,Y` @@ -251,11 +292,11 @@ end """ -> macro traitdef(head, body) ## make Trait type - traithead, name = parsetraithead(head) + traithead, name, paras, headassoc = parsetraithead(head) # make the body - meths, constr, assoc = parsebody(body) + meths, constr, assoc = parsebody(body, paras, headassoc) # make sure a generic function of all associated types exisits - + traitbody = quote methods::Dict{Union(Function,DataType), Tuple} constraints::Vector{Bool} From 47beb443f12c812c086401b3aff7f00832930772 Mon Sep 17 00:00:00 2001 From: Tony Fong Date: Thu, 8 Jan 2015 16:24:35 +0700 Subject: [PATCH 02/12] make traitimpl aware of associated type --- examples/monads.jl | 1 + src/helpers.jl | 26 ++++++++++++++++ src/traitdef.jl | 18 ++++++++--- src/traitimpl.jl | 74 ++++++++++++++++++++++++++++++++++++++-------- 4 files changed, 103 insertions(+), 16 deletions(-) diff --git a/examples/monads.jl b/examples/monads.jl index e600acd..2ecefe3 100644 --- a/examples/monads.jl +++ b/examples/monads.jl @@ -24,6 +24,7 @@ end end end end + @sample_params Dict(:Y => [Int,Float64]) # this tests out the params a bit end @assert isequal( mreturn( Nullable, 1.0 ), Nullable{Float64}( 1.0 ) ) diff --git a/src/helpers.jl b/src/helpers.jl index 69b81fe..1a2d02e 100644 --- a/src/helpers.jl +++ b/src/helpers.jl @@ -128,6 +128,32 @@ function tparlast( t::DataType ) error( "Parametric Trait: tparlast: No type parameter found" ) end +function tparpop( t::DataType ) + root = deparameterize_type(t) + for i in length(t.parameters):-1:1 + p = t.parameters[i] + if typeof( p ) == DataType + if i == 1 + return root + else + return root{ t.parameters[1:i-1]... } + end + end + end + error( "Parametric Trait: tparpop: No type parameter found" ) +end + +function argreplace!( ex::Expr, nmap ) + for j = 1:length(ex.args) + a = ex.args[j] + if typeof(a) == Symbol && haskey( nmap, a ) + ex.args[j] = nmap[a] + elseif typeof(a) == Expr + argreplace!( a, nmap ) + end + end +end + # # check whether a function is parameterized # function isparameterized(m::Method) # if isa(m.tvars, Tuple) diff --git a/src/traitdef.jl b/src/traitdef.jl index c6e7161..5f911a8 100644 --- a/src/traitdef.jl +++ b/src/traitdef.jl @@ -14,7 +14,7 @@ # 1) parse the header ### -function parsecurly(def::Expr) +function parsecurly(def::Expr ) # parses :(Cmp{x,y}) # into: :Cmp, [:x,:y], :(Cmp{x,y}), () @@ -114,14 +114,18 @@ function parsebody(body::Expr, paras::Array{Any,1}, headassoc::Array{Symbol,1} ) outfns = Expr(:dict) constr = :(Bool[]) assoc = quote end + params_rename = Dict{Symbol,Symbol}() # but first, add the assoc types from the head for s in headassoc local s_inited::Bool = false for p in paras if Base.Meta.isexpr( p, :curly ) && p.args[2] == s + rootsym = symbol( string(p.args[1],"0") ) hosttype = p.args[1] if !s_inited - push!( assoc.args, :($s = Traits.tparlast( $hosttype ) ) ) + params_rename[ hosttype ] = rootsym + push!( assoc.args, :($rootsym = Traits.tparpop( $hosttype ) ) ) + push!( assoc.args, :($s = Traits.tparlast( $hosttype ) ) ) s_inited=true else push!( assoc.args, :( @assert( $s == Traits.tparlast( $hosttype ) ) ) ) @@ -136,12 +140,15 @@ function parsebody(body::Expr, paras::Array{Any,1}, headassoc::Array{Symbol,1} ) elseif isassoc(ln) push!(assoc.args, ln) else # the rest of the body are function signatures - parsefnstypes!(outfns, ln) + parsefnstypes!(outfns, ln, params_rename) end end # store associated types: tmp = :(TypeVar[]) for ln in Lines(assoc) + if Base.Meta.isexpr( ln, :macrocall ) + continue + end tvar = ln.args[1] stvar = string(tvar) push!(tmp.args, :(TypeVar(symbol($stvar) ,$tvar))) @@ -164,7 +171,10 @@ function parseconstraints!(constr, block) end end -function parsefnstypes!(outfns, ln) +function parsefnstypes!(outfns::Expr, lnargs::Expr, params_rename::Dict{Symbol,Symbol} ) + ln = deepcopy( lnargs ) + argreplace!( ln, params_rename ) + function parsefn(def) # Parse to get function signature. # parses f(X,Y), f{X <:T}(X,Y) and X+Y diff --git a/src/traitimpl.jl b/src/traitimpl.jl index 34229d7..461db4c 100644 --- a/src/traitimpl.jl +++ b/src/traitimpl.jl @@ -33,7 +33,7 @@ function get_fname_only(ex) isa(ex,Symbol) ? ex : ex.args[1] end -function get_fsig(ex::Expr) +function get_fsig(ex::Expr) ex.args[1].args[2:end] end @@ -45,7 +45,7 @@ function check_macro_body(bodyargs, implfs, trait) # check that the signature of implfs agrees with trait().methods for (f,sig) in trait().methods # for now just check length. - if length(sig[1])!=length(get_fsig(implfs[f])) + if length(sig[1])!=length(get_fsig(implfs[f])) error("""Method definition: $f $sig does not match implementation: @@ -55,16 +55,21 @@ function check_macro_body(bodyargs, implfs, trait) nothing end -function parse_body(body::Expr) +function parse_impl_body(body::Expr) implfs = Dict() + sample_params = Dict() + for ln in Lines(body) - if !isdefined(get_fname_only(ln)) + if Base.Meta.isexpr( ln, :macrocall ) && ln.args[1] == symbol( "@sample_params" ) + sample_params = eval_curmod( ln.args[2] ) + continue + elseif !isdefined(get_fname_only(ln)) # define a standard generic function: eval_curmod(:($(get_fname_only(ln))() = error("Not defined"))) end implfs[eval_curmod(get_fname_only(ln))] = ln end - return implfs + return implfs, sample_params end function prefix_module!(ex::Expr, modname::Symbol) @@ -77,9 +82,9 @@ function prefix_module!(ex::Expr, modname::Symbol) elseif ex.head!= :function error("Not a function definition:\n$ex") end - + fnname = get_fname(ex) - fnname_only = get_fname_only(ex) + fnname_only = get_fname_only(ex) if isa(fnname, Symbol) ex.args[1].args[1] = :($modname.$fnname) elseif fnname.head==:curly @@ -100,7 +105,7 @@ end Example continuing from the documentation of `@traitdef`, implementing the `MyArith` trait: - ``` + ``` type A; a end; type AB; b end @traitimpl MyArith{A,AB} begin +(x::A,y::AB) = A(x.a+y.b) @@ -126,8 +131,8 @@ macro traitimpl(head, body) throw(TraitException("""Not all supertraits of $trait are implemented. Implement them first.""")) end - ## Parse macro body - implfs = parse_body(body) + ## Parse macro body + implfs,sample_params = parse_impl_body(body) #check_macro_body(body.args, implfs, trait) # doesn't work with associated types ## Make methods out = quote end @@ -136,8 +141,53 @@ macro traitimpl(head, body) prefix_module!(fndef, modname) push!(out.args,fndef) end - + + headassoc = Symbol[] + for p in paras + if Base.Meta.isexpr( p, :curly ) + @assert( typeof( p.args[2] ) == Symbol ) + if !in( p.args[2], headassoc ) + push!( headassoc, p.args[2] ) + end + end + end + ## Assert that the implementation went smoothly - push!(out.args, :(@assert istrait($trait_expr, verbose=true))) + if isempty( headassoc ) + push!(out.args, :(@assert istrait($trait_expr, verbose=true))) + elseif !isempty( sample_params ) + traithead = deepcopy( head ) + sample_exprs = Any[] + for s in headassoc + push!( sample_exprs, map( x->parse(string(x)), sample_params[ s ]) ) + end + # get all the permutations, using ideas from cartesian + sz = Int[ length(x) for x in sample_exprs ] + N = length(headassoc) + c = ones(Int, N) + sz1 = sz[1] + isdone = false + while !isdone + dt = Dict{Symbol,Any}() + for (i,s) in enumerate( headassoc ) + dt[s] = sample_exprs[i][ c[i] ] + end + traithead = deepcopy( head ) + argreplace!( traithead, dt ) + push!( out.args, :( @assert istrait( $traithead, verbose=true ) ) ) + + if (c[1]+=1) > sz1 + idim = 1 + while c[idim] > sz[idim] && idim < N + c[idim] = 1 + idim += 1 + c[idim] += 1 + end + isdone = c[end] > sz[end] + end + end + else + println( "@traitimpl: " * string( head ) * " should include @sample_params to test it out." ) + end return esc(out) end From e1211c3e2a4ba850252a31472c91ba65eae12315 Mon Sep 17 00:00:00 2001 From: Tony Fong Date: Sat, 10 Jan 2015 20:00:42 +0700 Subject: [PATCH 03/12] Add parametric trait tests. mod some test due to.. changes in the underlying code changes. Add documentations. --- README.md | 30 ++++++++---- examples/monads.jl | 31 +++++++++++- src/Traits.jl | 54 ++++++++++++++------- src/helpers.jl | 106 +++++++++++++++++++++++++++++++++++++++--- src/traitdef.jl | 59 +++++++++++++++++++---- src/traitimpl.jl | 62 ++++++++++++++++-------- test/paramtraits.jl | 24 ++++++++++ test/runtests.jl | 4 +- test/traitdef.jl | 30 ++++++------ test/traitdispatch.jl | 20 ++++---- 10 files changed, 330 insertions(+), 90 deletions(-) create mode 100644 test/paramtraits.jl diff --git a/README.md b/README.md index 61de2ed..35a0466 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,12 @@ end Z = promote_type(X,Y) # calculates Z from X and Y fun5(X,Y) -> Z end + +# using parametric trait. Note the nested curly +@traitdef SemiFunctor{X{Y}} begin + fmap( Function, X{Y} } -> Any +end + ``` Note that return-type checking is quite experimental. It can be turned off by defining `Main.Traits_check_return_types=false` before @@ -155,6 +161,17 @@ try catch e println(e) # ErrorException("assertion failed: istrait(Tr4{Int,Float64})") end + +# for parametric trait, +@traitimpl SemiFunctor{Nullable{T}} begin + fmap{T}( f::Function, x::Nullable{T}) = Nullable(f(x.value)) +end + +# for Array, it is a bit difficult because the eltype is the first argument. +# Also note that this sample implementation won’t cover higher dimensions +@traitimpl SemiFunctor{Array{T...}} begin + fmap{T}( f::Function, x::Array{T,1}) = map(f, x) +end ``` Trait functions & dispatch: @@ -342,16 +359,9 @@ do not have a strict hierarchy like types. - Are there better ways for trait-dispatch? -- Sometimes it would be good to get at type parameters, for instance - for Arrays and the like: - ```julia - @traitdef Indexable{X{Y}} begin - getindex(X, Any) -> Y - setindex!(X, Y, Any) -> X - end - ``` - This problem is similar to triangular dispatch and may be solved - by: https://github.com/JuliaLang/julia/issues/6984#issuecomment-49751358 +- Issues related to parametric trait: + * Triangular dispatch: + https://github.com/JuliaLang/julia/issues/6984#issuecomment-49751358 # Issues diff --git a/examples/monads.jl b/examples/monads.jl index 2ecefe3..d9621ae 100644 --- a/examples/monads.jl +++ b/examples/monads.jl @@ -6,11 +6,38 @@ end call{TI,TO}( fs::FuncFullSig{TI,TO}, x::TI ) = (fs.f(x))::TO +@traitdef SemiFunctor{X{Y}} begin + fmap( Function, X{Y} ) -> Any +end + +@traitdef Functor{X{Y}} begin + fmap( FuncFullSig{Y,Z}, X{Y} ) -> X{Z} +end + @traitdef Monad{X{Y}} begin mreturn(::X, Y) -> X{Y} # we cannot infer X so we have to supply it bind( X{Y}, FuncFullSig{Y, X{Y}} ) -> X{Y} end +@traitimpl SemiFunctor{ Array{Y...} } begin + fmap{Y}( f::Function, x::Array{Y,1} ) = map(f, x) +end + +@traitimpl Functor{ Array{Y...} } begin + fmap{Y,Z}( f::FuncFullSig{Y,Z}, x::Array{Y} ) = Z[ f.f(i) for i in x ] +end + +@traitimpl Monad{ Array{Y...} } begin + mreturn{Y}( ::Type{Array}, x::Y ) = Y[x] + bind{Y,Z}( x::Array{Y}, f::FuncFullSig{Y, Array{Z}} ) = begin + ret = Z[] + for c in x + append!( ret, f.f( x ) ) + end + ret + end +end + @traitimpl Monad{Nullable{Y}} begin mreturn{Y}( ::Type{Nullable}, x::Y ) = Nullable{Y}(x) bind{Y,Z}( x::Nullable{Y}, f::FuncFullSig{Y, Nullable{Z}} ) = begin @@ -24,7 +51,6 @@ end end end end - @sample_params Dict(:Y => [Int,Float64]) # this tests out the params a bit end @assert isequal( mreturn( Nullable, 1.0 ), Nullable{Float64}( 1.0 ) ) @@ -32,6 +58,9 @@ testfunc( x::Float64 ) = x == 0.0 ? Nullable{Float64}() : Nullable{Float64}( 1.0 @assert isequal( bind( Nullable{Float64}( 2.0 ), FuncFullSig{Float64,Nullable{Float64}}( testfunc ) ), Nullable{Float64}( 0.5 ) ) @assert isequal( bind( Nullable{Float64}( 0.0 ), FuncFullSig{Float64,Nullable{Float64}}( testfunc ) ), Nullable{Float64}() ) +import Base: bind +@traitfn bind{S;Monad{S}}( x::S, f::Function ) = bind(x,FuncFullSig{Traits.tparlast(S),S}( f ) ) + # Note: FuncFullSig{TO} is itself a Monad #= @traitfn mreturn{;Monad{FuncFullSig{T}}}( x::T ) = FuncFullSig{T,T}(_->x) diff --git a/src/Traits.jl b/src/Traits.jl index a9a0222..143c348 100644 --- a/src/Traits.jl +++ b/src/Traits.jl @@ -1,7 +1,7 @@ module Traits -@doc """This package provides an implementation of traits, aka interfaces or type-classes. +@doc """This package provides an implementation of traits, aka interfaces or type-classes. It is based on the premises that traits are: - + - contracts on one type or between several types. The contract can contain required methods but also other assertions and just belonging to a group (i.e. the trait). @@ -9,8 +9,9 @@ module Traits """ -> current_module() export istrait, istraittype, issubtrait, - traitgetsuper, traitgetpara, traitmethods, - @traitdef, @traitimpl, @traitfn, TraitException, All + traitgetsuper, traitgetpara, traitmethods, + @traitdef, @traitimpl, @traitfn, TraitException, All, + tparusesuffix if !(VERSION>v"0.4-") error("Traits.jl needs Julia version 0.4.-") @@ -32,9 +33,9 @@ end SUPER of Trait is needed to specify super-traits (a tuple).""" -> abstract Trait{SUPER} -# A concrete trait type has the form +# A concrete trait type has the form ## Tr{X,Y,Z} <: Trait{(ST1{X,Y},ST2{Z})} -# +# # immutable Tr{X,Y,Z} <: Trait{(ST1{X,Y},ST2{Z})} # methods # Tr() = new(methods_made_in_macro) @@ -52,16 +53,16 @@ immutable _TraitStorage end @doc """Type All is to denote that any type goes in type signatures in @traitdef. This is a bit awkward: - + - method_exists(f, s) returns true if there is a method of f with signature sig such that s<:sig. Thus All<->Union() - Base.return_types works the other way around, there All<->Any - + See also https://github.com/JuliaLang/julia/issues/8974"""-> abstract All # General trait exception -type TraitException <: Exception +type TraitException <: Exception msg::String end @@ -92,16 +93,33 @@ function istrait{T<:Trait}(Tr::Type{T}; verbose=false) # check supertraits !istrait(traitgetsuper(Tr); verbose=verbose) && return false # check methods definitions - try + try Tr() catch if verbose - println("""Not all generic functions of trait $Tr are defined. + println("""Not all generic functions of trait $Tr are defined. Define them before using $Tr""") end return false end out = true + function testanytypevars( at ) + if typeof(at) == TypeVar + return true + elseif typeof(at) == DataType + return( any( y->typeof(y) == TypeVar, at.parameters ) ) + elseif typeof(at) <: Tuple + for sat in at + if testanytypevars(sat) + return true + end + end + return false + else + println( "unknown type in signature " * string( typeof( at ) ) * " val: " * string(at) ) + end + end + anytypevars = false # check call signature of methods: for (meth,sig) in Tr().methods # instead of: @@ -110,12 +128,12 @@ function istrait{T<:Trait}(Tr::Type{T}; verbose=false) # https://github.com/JuliaLang/julia/issues/8959 sigg = map(x->x===All ? Union() : x, sig[1]) + anytypevars = testanytypevars( sig[1] ) + if isa(meth, Function) - if !method_exists(meth, sigg) # I think this does the right thing. - if verbose - println("Method $meth with call signature $(sig[1]) not defined for $T") - end - out = false + out = !anytypevars ? method_exists(meth,sigg) : method_exists_tvars( meth,sigg,verbose ) + if !out && verbose + println("Method $meth with call signature $(sig[1]) not defined for $T") end elseif isa(meth, DataType) # a constructor, presumably. # But discard the catch all to convert, i.e. this means @@ -132,7 +150,9 @@ function istrait{T<:Trait}(Tr::Type{T}; verbose=false) end end # check return-type - if flag_check_return_types && out # only check if all methods were defined + # unfortunately if the sig has TypeVar in them it doesn't seem possible to check + # the return type + if !anytypevars && flag_check_return_types && out # only check if all methods were defined for (meth,sig) in Tr().methods # replace All in sig[1] with Any sigg = map(x->x===All ? Any : x, sig[1]) diff --git a/src/helpers.jl b/src/helpers.jl index 1a2d02e..d8384c0 100644 --- a/src/helpers.jl +++ b/src/helpers.jl @@ -109,13 +109,14 @@ function hasparameters(t::DataType) end end -function tpar1( t::DataType ) - for p in t.parameters - if typeof( p ) == DataType - return p - end +# tparfirst, tparlast, tparpop, tparshift: low-level lookup of type parameters +# tparprefix, tparget, tparsuffix: based on tparusesuffix, get the composition +function tparfirst( t::DataType ) + if isempty( t.parameters ) + error( "Parametric Trait: tpar1: " * string(t) * " doesn't support parameters" ) end - error( "Parametric Trait: tpar1: No type parameter found" ) + + return t.parameters[1] end function tparlast( t::DataType ) @@ -143,6 +144,43 @@ function tparpop( t::DataType ) error( "Parametric Trait: tparpop: No type parameter found" ) end +# return a tuple of the rest of the type parameters after the first +function tparshift( t::DataType ) + return t.parameters[2:end] +end + +trait_use1st_reg = Set{Any}() + +function tparprefix( trait::Any, pos::Any, t::Any) + global trait_use1st_reg + args = ( trait, pos, deparameterize_type(t) ) + if !in( args, trait_use1st_reg ) + tparpop( t ) + else + args[end] + end +end + +function tparget( trait::Any, pos::Any, t::Any) + global trait_use1st_reg + args = ( trait, pos, deparameterize_type(t) ) + if !in( args, trait_use1st_reg ) + tparlast( t ) + else + tparfirst( t ) + end +end + +function tparsuffix( trait::Any, pos::Any, t::Any) + global trait_use1st_reg + args = ( trait, pos, deparameterize_type(t) ) + if !in( args, trait_use1st_reg ) + () + else + tparshift( t ) + end +end + function argreplace!( ex::Expr, nmap ) for j = 1:length(ex.args) a = ex.args[j] @@ -154,6 +192,62 @@ function argreplace!( ex::Expr, nmap ) end end +# a hacky way to test when the signature has type variables in them +# There would be false positive in pathological cases! +function method_exists_tvars( f::Function, argts::Tuple, verbose::Bool ) + for m in methods(f) + if verbose + println( "matching ", m ) + end + if length(m.sig) > length(argts) + continue + end + n = min( length(m.sig), length(argts ) ) + lasttype = Any + if length(m.sig) < length(argts) + if isempty( m.sig ) || m.sig[end].name.name != :Vararg + continue + end + lasttype = m.sig[end].parameters[1] + end + match=true + for i in 1:n + if verbose + print( " check ", argts[i], " ? <: ", m.sig[i], " = " ) + end + if !( argts[i] <: m.sig[i] ) + if verbose + println( "false") + end + match=false + break + else + if verbose + println( "true" ) + end + end + end + if match # so far + for i in n+1:length( argts ) + if !(argts[i] <: lasttype ) + match=false + break + end + end + end + if match + if verbose + println( " ... a match" ) + end + return true + end + end + if verbose + println( " ... not a match" ) + end + return false +end + # # check whether a function is parameterized # function isparameterized(m::Method) # if isa(m.tvars, Tuple) diff --git a/src/traitdef.jl b/src/traitdef.jl index 5f911a8..8e9de2d 100644 --- a/src/traitdef.jl +++ b/src/traitdef.jl @@ -93,7 +93,7 @@ end # 2) parse the function definitions ### -function parsebody(body::Expr, paras::Array{Any,1}, headassoc::Array{Symbol,1} ) +function parsebody(name::Symbol, body::Expr, paras::Array{Any,1}, headassoc::Array{Symbol,1} ) # Transforms: # body = quote # R = g(X) @@ -115,20 +115,32 @@ function parsebody(body::Expr, paras::Array{Any,1}, headassoc::Array{Symbol,1} ) constr = :(Bool[]) assoc = quote end params_rename = Dict{Symbol,Symbol}() + local_typesyms = Set{Symbol}() + push!( local_typesyms, name ) + for p in paras + if typeof( p ) == Symbol + push!( local_typesyms, p ) + end + end # but first, add the assoc types from the head for s in headassoc local s_inited::Bool = false - for p in paras + for (i,p) in enumerate( paras ) if Base.Meta.isexpr( p, :curly ) && p.args[2] == s rootsym = symbol( string(p.args[1],"0") ) hosttype = p.args[1] + tailsym = symbol( string(p.args[1],"0_" ) ) if !s_inited params_rename[ hosttype ] = rootsym - push!( assoc.args, :($rootsym = Traits.tparpop( $hosttype ) ) ) - push!( assoc.args, :($s = Traits.tparlast( $hosttype ) ) ) + push!( assoc.args, :($rootsym = Traits.tparprefix( $name, Val{$i}, $hosttype ) ) ) + push!( assoc.args, :($s = Traits.tparget( $name, Val{$i}, $hosttype ) ) ) + push!( assoc.args, :($tailsym = Traits.tparsuffix( $name, Val{$i}, $hosttype ) ) ) + push!( local_typesyms, rootsym ) + push!( local_typesyms, s ) + push!( local_typesyms, tailsym ) s_inited=true else - push!( assoc.args, :( @assert( $s == Traits.tparlast( $hosttype ) ) ) ) + push!( assoc.args, :( @assert( $s == Traits.tparget( $name, Val{$i}, $hosttype ) ) ) ) end end end @@ -140,7 +152,7 @@ function parsebody(body::Expr, paras::Array{Any,1}, headassoc::Array{Symbol,1} ) elseif isassoc(ln) push!(assoc.args, ln) else # the rest of the body are function signatures - parsefnstypes!(outfns, ln, params_rename) + parsefnstypes!(outfns, ln, local_typesyms, params_rename ) end end # store associated types: @@ -149,6 +161,9 @@ function parsebody(body::Expr, paras::Array{Any,1}, headassoc::Array{Symbol,1} ) if Base.Meta.isexpr( ln, :macrocall ) continue end + if endswith( string(ln.args[1]), "0_" ) + continue + end tvar = ln.args[1] stvar = string(tvar) push!(tmp.args, :(TypeVar(symbol($stvar) ,$tvar))) @@ -171,9 +186,23 @@ function parseconstraints!(constr, block) end end -function parsefnstypes!(outfns::Expr, lnargs::Expr, params_rename::Dict{Symbol,Symbol} ) +function parsefnstypes!(outfns::Expr, lnargs::Expr, local_typesyms::Set{Symbol}, params_rename::Dict{Symbol,Symbol} ) ln = deepcopy( lnargs ) argreplace!( ln, params_rename ) + rootsyms = values( params_rename ) + + function addargscurlytails!( def ) + if typeof( def ) == Expr + for a in def.args + addargscurlytails!( a ) + end + if Base.Meta.isexpr( def, :curly ) + if in( def.args[1], rootsyms ) + push!( def.args, Expr( :(...), symbol( string(def.args[1])*"_" ) ) ) + end + end + end + end function parsefn(def) # Parse to get function signature. @@ -191,7 +220,8 @@ function parsefnstypes!(outfns::Expr, lnargs::Expr, params_rename::Dict{Symbol,S end argtype = :() for i = 2:length( def.args ) - a = def.args[i] + a = deepcopy( def.args[i] ) + addargscurlytails!( a ) if Base.Meta.isexpr( a, :(::) ) # shorthand t = a.args[1] push!( argtype.args, Expr( :curly, :Type, t ) ) @@ -299,12 +329,23 @@ end istrait(MyInv{Int}) # -> false istrait(MyInv{Float64}) # -> true ``` + + A trait can be declare to accept a type parameter: + ``` + @traitdef SemiFunctor{X{Y}} begin + fmap( Function, X{Y} ) -> Any # we ignore the output sig for now + end + fmap{T}( f::Function, a::Nullable{T}) = Nullable(f(a)) + istrait(SemiFunctor{Nullable{Int}}) # -> true + ``` + + By default, the type parameter in question must be the last one. To override this (e.g. Array), see @traitimpl """ -> macro traitdef(head, body) ## make Trait type traithead, name, paras, headassoc = parsetraithead(head) # make the body - meths, constr, assoc = parsebody(body, paras, headassoc) + meths, constr, assoc = parsebody(name, body, paras, headassoc) # make sure a generic function of all associated types exisits traitbody = quote diff --git a/src/traitimpl.jl b/src/traitimpl.jl index 461db4c..76d00ea 100644 --- a/src/traitimpl.jl +++ b/src/traitimpl.jl @@ -60,10 +60,7 @@ function parse_impl_body(body::Expr) sample_params = Dict() for ln in Lines(body) - if Base.Meta.isexpr( ln, :macrocall ) && ln.args[1] == symbol( "@sample_params" ) - sample_params = eval_curmod( ln.args[2] ) - continue - elseif !isdefined(get_fname_only(ln)) + if !isdefined(get_fname_only(ln)) # define a standard generic function: eval_curmod(:($(get_fname_only(ln))() = error("Not defined"))) end @@ -116,10 +113,27 @@ end istrait(MyArith{A, AB}) # -> true ``` - Notes + if a trait accepts a type parameter, by default it is the last one + ``` + @traitdef SemiFunctor{X{Y}} begin + fmap( Function, X{Y}) -> Any + end + @traitimpl SemiFunctor{Nullable{Y}} begin + fmap{Y}( f::Function, x::Nullable{Y} ) = Nullable(f(x.value)) + end + istrait( SemiFunctor{Nullable{Int} }) # -> true + ``` + However, for Array type, we could still use SemiFunctor but we have to use + @traitimpl explicitly, like so + ``` + # NOTE THE ellipsis "..."" IN THE TYPE PARAMETER + @traitimpl SemiFunctor{Array{Y...}} begin + fmap{Y}( f::Function, x::Array{Y,1} ) = map(f, x) + end + istrait( SemiFunctor{Array{Int,1} }) # -> true + istrait( SemiFunctor{Array{Int,2} }) # -> false + ``` - - the type annotations are mandatory. No parameterized methods - are allowed (for now). """ -> macro traitimpl(head, body) ## Parse macro header @@ -133,28 +147,37 @@ macro traitimpl(head, body) end ## Parse macro body implfs,sample_params = parse_impl_body(body) + out = quote end #check_macro_body(body.args, implfs, trait) # doesn't work with associated types + headassoc = Symbol[] + for (i,p) in enumerate(paras) + if Base.Meta.isexpr( p, :curly ) + if Base.Meta.isexpr(p.args[2], :(...) ) + rootsym = p.args[1] + testsym = p.args[2].args[1] + push!( out.args, :( push!( Traits.trait_use1st_reg, ($name, Val{$i}, $rootsym ) ) ) ) + elseif typeof( p.args[2] ) == Symbol + testsym = p.args[2] + else + error( "traitimpl: in head, "*string(p)* " should be either a Symbol or T...") + end + if !in( testsym, headassoc ) + push!( headassoc, testsym ) + end + end + end + ## Make methods - out = quote end for (fn, fndef) in implfs modname = module_name(Base.function_module(fn)) prefix_module!(fndef, modname) push!(out.args,fndef) end - headassoc = Symbol[] - for p in paras - if Base.Meta.isexpr( p, :curly ) - @assert( typeof( p.args[2] ) == Symbol ) - if !in( p.args[2], headassoc ) - push!( headassoc, p.args[2] ) - end - end - end - - ## Assert that the implementation went smoothly + ## Assert that the implementation went smoothly for non-parametric strait if isempty( headassoc ) push!(out.args, :(@assert istrait($trait_expr, verbose=true))) + #= elseif !isempty( sample_params ) traithead = deepcopy( head ) sample_exprs = Any[] @@ -188,6 +211,7 @@ macro traitimpl(head, body) end else println( "@traitimpl: " * string( head ) * " should include @sample_params to test it out." ) + =# end return esc(out) end diff --git a/test/paramtraits.jl b/test/paramtraits.jl new file mode 100644 index 0000000..1f2dac7 --- /dev/null +++ b/test/paramtraits.jl @@ -0,0 +1,24 @@ +using Traits +using Base.Test + +import Traits: tparusesuffix +@traitdef SemiFunctor{X{Y}} begin + fmap( Function, X{Y} ) -> Any +end + +fmap{Y}( f::Function, x::Nullable{Y} ) = Nullable( f(x.value) ) + +println( "test SemiFunctor{Nullable}" ) +@test istrait( SemiFunctor{Nullable{Int}}) + +# This change the order +@traitimpl SemiFunctor{ Array{Y...} } begin + fmap{Y}( f::Function, x::Array{Y,1} ) = map(f, x) +end + +@test Traits.tparsuffix( SemiFunctor, Val{1}, Array{Int,1} ) != () + +println( "test istrait{ SemiFunctor{Array{Int,1}}}" ) +@test istrait( SemiFunctor{Array{Int,1}} ) +println( "test istrait{ SemiFunctor{Array{Int,2}}}" ) +@test istrait( SemiFunctor{Array{Int,2}} ) == false diff --git a/test/runtests.jl b/test/runtests.jl index 2d6c6ca..89c78b5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -10,7 +10,7 @@ type A2 end # BUG flags: set to false once fixed to activate tests method_exists_bug = true # see https://github.com/JuliaLang/julia/issues/8959 -method_exists_bug2 = true # see https://github.com/JuliaLang/julia/issues/9043 +method_exists_bug2 = false # see https://github.com/JuliaLang/julia/issues/9043 # manual implementations include("manual-traitdef.jl") @@ -22,4 +22,4 @@ include("manual-traitdispatch.jl") include("traitdef.jl") include("traitfns.jl") include("traitdispatch.jl") - +include("paramtraits.jl") diff --git a/test/traitdef.jl b/test/traitdef.jl index fc2257f..3132cca 100644 --- a/test/traitdef.jl +++ b/test/traitdef.jl @@ -2,29 +2,29 @@ td = :(@traitdef Cr20{X} begin length(X) end) -a,b = Traits.parsebody(td.args[end]) +a,b = Traits.parsebody(:Cr20, td.args[end], Any[], Symbol[] ) @test a==Expr(:dict, :(length=>((X,),(Any...,)))) @test b==:(Bool[]) td0 = :(@traitdef Cr20{X} begin length(X) - + @constraints begin string(X.name)[1]=='I' end end) -a,b = Traits.parsebody(td0.args[end]) +a,b = Traits.parsebody(:Cr20, td0.args[end], Any[], Symbol[] ) @test a==Expr(:dict, :(length=>((X,),(Any...,)))) @test b==:(Bool[(string(X.name))[1] == 'I']) td1 = :(@traitdef Cr20{X} begin length(X) -> Int - + @constraints begin string(X.name)[1]=='I' end end) -a,b = Traits.parsebody(td1.args[end]) +a,b = Traits.parsebody(:Cr20, td1.args[end], Any[], Symbol[] ) @test a==Expr(:dict, :(length=>((X,),(Int,)))) @test b==:(Bool[(string(X.name))[1] == 'I']) @@ -32,12 +32,12 @@ td2 = :(@traitdef Cr20{X,Y} begin X + Y -> Int,Float64 -(X,Y) -> Int (/)(X,Y) -> Int - + @constraints begin string(X.name)[1]=='I' end end) -a,b,c = Traits.parsebody(td2.args[end]) +a,b,c = Traits.parsebody(:Cr20, td2.args[end], Any[], Symbol[]) @test a==Expr(:dict, :((+) => ((X,Y),(Int,Float64))), :((-) => ((X,Y),(Int,))), :((/) => ((X,Y),(Int,)))) @@ -47,14 +47,14 @@ a,b,c = Traits.parsebody(td2.args[end]) td3 = :(@traitdef Cr20{X,Y} begin fn(X) -> Type{X} end) -a,b,c = Traits.parsebody(td3.args[end]) +a,b,c = Traits.parsebody(:Cr20, td3.args[end], Any[:X,:Y], Symbol[] ) @test a==Expr(:dict, :((fn) => ((X,),(Type{X},)))) td4 = :(@traitdef Cr20{X} begin fn{Y<:II}(X,Y) -> Type{X} fn76{K<:FloatingPoint, I<:Integer}(X, Vector{I}, Vector{K}) -> I end) -a,b,c = Traits.parsebody(td4.args[end]) +a,b,c = Traits.parsebody(:Cr20, td4.args[end], Any[:X], Symbol[] ) v = :(TypeVar(symbol("Y"),II)) t = :(TypeVar(symbol("I"),Integer)) k = :(TypeVar(symbol("K"),FloatingPoint)) @@ -174,14 +174,13 @@ end fn75{Y <: Integer}(x::UInt8, y::Y) = y+x if method_exists_bug2 @test !istrait(Pr0{UInt8}) + fn75(x::UInt8, y::Int8) = y+x + @test !istrait(Pr0{UInt8}) # this works, not because only for y::Int8 not for all Integers else @test istrait(Pr0{UInt8}) end @test !istrait(Pr0{Int8}) -fn75(x::UInt8, y::Int8) = y+x -@test !istrait(Pr0{UInt8}) # this works, not because only for y::Int8 not for all Integers - @traitdef Pr1{X} begin fn76{I<:Integer}(X, Vector{I}) -> I end @@ -191,13 +190,12 @@ if method_exists_bug2 else @test istrait(Pr1{UInt8}) end -@test !istrait(Pr1{UInt8}) # test constraints @traitdef Cr20{X} begin length(X) -> Any - + @constraints begin string(X.name)[1]=='I' end @@ -261,7 +259,7 @@ end # type-functions based on return_type: State = Base.return_types(start, (X,))[1] # this is circular but that is ok, as trait needs to be implemented. Item = Base.return_types(next, (X,State))[1][1] - + # interface functions start(X) -> State next(X, State) -> Item, State @@ -293,7 +291,7 @@ type A4758 end @traitdef TT46{Ar} begin T = Base.return_types(eltype, (Ar,))[1] Arnp = deparameterize_type(Ar) # Array stripped of type parameters - + Arnp(T, Int64) -> Ar Arnp(T, Int64...) -> Ar @constraints begin diff --git a/test/traitdispatch.jl b/test/traitdispatch.jl index a3a2967..5653427 100644 --- a/test/traitdispatch.jl +++ b/test/traitdispatch.jl @@ -35,28 +35,28 @@ ff1(x,y) = x==y # @code_llvm ft1(4,5) # @code_llvm ff1(4,5) @test 0.02 Date: Sat, 10 Jan 2015 20:07:23 +0700 Subject: [PATCH 04/12] remove my own dead code --- src/traitimpl.jl | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/src/traitimpl.jl b/src/traitimpl.jl index 76d00ea..feaf824 100644 --- a/src/traitimpl.jl +++ b/src/traitimpl.jl @@ -177,41 +177,6 @@ macro traitimpl(head, body) ## Assert that the implementation went smoothly for non-parametric strait if isempty( headassoc ) push!(out.args, :(@assert istrait($trait_expr, verbose=true))) - #= - elseif !isempty( sample_params ) - traithead = deepcopy( head ) - sample_exprs = Any[] - for s in headassoc - push!( sample_exprs, map( x->parse(string(x)), sample_params[ s ]) ) - end - # get all the permutations, using ideas from cartesian - sz = Int[ length(x) for x in sample_exprs ] - N = length(headassoc) - c = ones(Int, N) - sz1 = sz[1] - isdone = false - while !isdone - dt = Dict{Symbol,Any}() - for (i,s) in enumerate( headassoc ) - dt[s] = sample_exprs[i][ c[i] ] - end - traithead = deepcopy( head ) - argreplace!( traithead, dt ) - push!( out.args, :( @assert istrait( $traithead, verbose=true ) ) ) - - if (c[1]+=1) > sz1 - idim = 1 - while c[idim] > sz[idim] && idim < N - c[idim] = 1 - idim += 1 - c[idim] += 1 - end - isdone = c[end] > sz[end] - end - end - else - println( "@traitimpl: " * string( head ) * " should include @sample_params to test it out." ) - =# end return esc(out) end From 79c78f87603c3497444f0d234cd1afa59f48eee4 Mon Sep 17 00:00:00 2001 From: Tony Fong Date: Sat, 10 Jan 2015 20:40:26 +0700 Subject: [PATCH 05/12] remove non-existent function in export --- src/Traits.jl | 3 +-- test/paramtraits.jl | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Traits.jl b/src/Traits.jl index 143c348..81cb118 100644 --- a/src/Traits.jl +++ b/src/Traits.jl @@ -10,8 +10,7 @@ module Traits export istrait, istraittype, issubtrait, traitgetsuper, traitgetpara, traitmethods, - @traitdef, @traitimpl, @traitfn, TraitException, All, - tparusesuffix + @traitdef, @traitimpl, @traitfn, TraitException, All if !(VERSION>v"0.4-") error("Traits.jl needs Julia version 0.4.-") diff --git a/test/paramtraits.jl b/test/paramtraits.jl index 1f2dac7..d4458d9 100644 --- a/test/paramtraits.jl +++ b/test/paramtraits.jl @@ -1,7 +1,6 @@ using Traits using Base.Test -import Traits: tparusesuffix @traitdef SemiFunctor{X{Y}} begin fmap( Function, X{Y} ) -> Any end From 8febf07dae7e28f89588cc73740e71e184a344cf Mon Sep 17 00:00:00 2001 From: Tony Fong Date: Tue, 13 Jan 2015 02:09:24 +0700 Subject: [PATCH 06/12] a "better" trait match scoring scheme --- src/helpers.jl | 4 ++++ src/traitdef.jl | 60 ++++++++++++++++++++++++++++++++++++++++++------- src/traitfns.jl | 50 ++++++++++++++++++++--------------------- 3 files changed, 81 insertions(+), 33 deletions(-) diff --git a/src/helpers.jl b/src/helpers.jl index d8384c0..da291a8 100644 --- a/src/helpers.jl +++ b/src/helpers.jl @@ -149,6 +149,8 @@ function tparshift( t::DataType ) return t.parameters[2:end] end +# to store if ( Trait name, Trait's nth parameter, nth's parameter being type ) +# should use the 1st type parameter instead of the last one (default) trait_use1st_reg = Set{Any}() function tparprefix( trait::Any, pos::Any, t::Any) @@ -248,6 +250,8 @@ function method_exists_tvars( f::Function, argts::Tuple, verbose::Bool ) return false end +trait_match_scores = Dict{Symbol,Float64}() + # # check whether a function is parameterized # function isparameterized(m::Method) # if isa(m.tvars, Tuple) diff --git a/src/traitdef.jl b/src/traitdef.jl index 8e9de2d..2800364 100644 --- a/src/traitdef.jl +++ b/src/traitdef.jl @@ -19,7 +19,15 @@ function parsecurly(def::Expr ) # into: :Cmp, [:x,:y], :(Cmp{x,y}), () # parses :(Monad{X{Y}}) - # into: :Monad, [:( :curly, :X, :Y )], :(Monad{X}), () + # into: :Monad, [:(X{Y})], :(Monad{X}), () + + # multiple parametric trait: + # parses :(ComboTr{X{Y}, Z{T}}) + # into: :ComboTr, [:(X{Y}), :(Z{T}) ]], :(ComboTr{X,Z}), () + + # Note that if we have + # :(ComboTr{X{Y}, Z{Y}}) # note the same Y + # The trait constructor will have an assertion that the parameter in X and Z must match name = def.args[1] paras = Any[] append!(paras,def.args[2:end] ) @@ -31,20 +39,49 @@ end function parsecomp(def::Expr) # parses :(Cmp{x,y} <: Eq{x,y}) # into: :Cmp, [:x,:y], :(Cmp{x,y}), :((Eq{x,y},)) + + # parses :(Tr2{X{Z1},Y{Z2}} <: Tr2base{X,Y}) + # into: :Tr2, [:(X{Z1}),:(Y{Z2})], :(Tr2{X,Y}), :((Tr2base{X,Y},)) + # the supertraits' parameters are redundant and, if given, are stripped out. So the following + # would produce the same output + # :(Tr2{X{Z1},Y{Z2}} <: Tr2base{X{Z1},Y{Z2}}) if def.args[2]!=:<: error("not a <:") end name, paras, trait = parsecurly(def.args[1]) supertraits = :() - push!(supertraits.args, def.args[3]) + + if !Base.Meta.isexpr( def.args[3], :curly ) + error( "Traits: RHS of " * string( def ) * " must be a curly (Trait) expression" ) + end + c = def.args[3] + supertrait = Expr( :curly, c.args[1], + map( x->typeof(x)==Symbol ? x : Base.Meta.isexpr( x, :curly )? x.args[1] : error( "Traits: unknown " * string(x) ), + c.args[2:end] )... ) + + push!(supertraits.args, supertrait ) return name, paras, trait, supertraits end function parsetuple(def::Expr) # parses :(Cmp{x,y} <: Eq{x,y}, Sz{x}, Uz{y}) # into :Cmp, [:x,:y], :(Cmp{x,y}), :((Eq{x,y},Sz{x},Uz{y})) + + # parses :(Tr2{X{Z1},Y{Z2}} <: Tr2base{X,Y}), Tr1base{X} + # into: :Tr2, [:(X{Z1}),:(Y{Z2})], :(Tr2{X,Y}), :((Tr2base{X,Y},Tr1base{X})) + # the supertraits' parameters are redundant and, if given, are stripped out. So the following + # would produce the same output + # :(Tr2{X{Z1},Y{Z2}} <: Tr2base{X{Z1},Y{Z2}}, Tr1base{X{Z1}}) name, paras, trait, supertraits = parsecomp(def.args[1]) - append!(supertraits.args, def.args[2:end]) + for i in 2:length(def.args) + c = def.args[i] + if !Base.Meta.isexpr( c, :curly ) + error( "Traits: supertrait #" * string(i) * " is not a curly (Trait) expression" ) + end + push!( supertraits.args, Expr( :curly, c.args[1], + map( x->typeof(x)==Symbol ? x : Base.Meta.isexpr( x, :curly )? x.args[1] : error( "Traits: unknown " * string(x) ), + c.args[2:end] )... ) ) + end return name, paras, trait, supertraits end @@ -69,14 +106,18 @@ function parsetraithead(def::Expr) error("Interface specification error") end # check supertraits<:Traits + maxscore = 0.0 for i =1:length(supertraits.args) + global trait_match_scores st = supertraits.args[i].args[1] + maxscore = max( trait_match_scores[st], maxscore ) eval_curmod(:(@assert istraittype($st))) end + basescore = maxscore + 1.0 + 0.1 * length( supertraits.args ) # make :(immutable Cmp{X,Y} <: Trait{(Eq{X,Y}, Tr1{X})} end) out = :(immutable $trait <: Traits.Trait{$supertraits} end) - # capture Y in Monad{X{Y}} + # capture type parameters e.g. the Y in Monad{X{Y}} headassoc = Symbol[] for p in paras if Base.Meta.isexpr( p, :curly ) @@ -86,8 +127,7 @@ function parsetraithead(def::Expr) end end end - - return out, name, paras, headassoc + return out, name, paras, headassoc, basescore end # 2) parse the function definitions @@ -140,7 +180,9 @@ function parsebody(name::Symbol, body::Expr, paras::Array{Any,1}, headassoc::Arr push!( local_typesyms, tailsym ) s_inited=true else - push!( assoc.args, :( @assert( $s == Traits.tparget( $name, Val{$i}, $hosttype ) ) ) ) + teststmt = :( @assert( $s == Traits.tparget( $name, Val{$i}, $hosttype ) ) ) + push!( teststmt.args, :( string( "In ", p, ", ", s, " does not match an earlier definition" ) ) ) + push!( assoc.args, teststmt ) end end end @@ -343,10 +385,11 @@ end """ -> macro traitdef(head, body) ## make Trait type - traithead, name, paras, headassoc = parsetraithead(head) + traithead, name, paras, headassoc, basescore = parsetraithead(head) # make the body meths, constr, assoc = parsebody(name, body, paras, headassoc) # make sure a generic function of all associated types exisits + global trait_match_scores traitbody = quote methods::Dict{Union(Function,DataType), Tuple} @@ -357,6 +400,7 @@ macro traitdef(head, body) new( $meths, $constr, assoctyps) end end + trait_match_scores[ name ] = basescore + 0.1 * (length( constr.args )-1) # add body to the type definition traithead.args[3] = traitbody return esc(traithead) diff --git a/src/traitfns.jl b/src/traitfns.jl index 3d64b8b..c7cadaa 100644 --- a/src/traitfns.jl +++ b/src/traitfns.jl @@ -23,11 +23,11 @@ type ParsedFn # (probably should adapt MetaTools.jl...) name::FName # f1 fun # f1{X<:Int,Y} typs # [:(X<:Int),:Y] - sig # [:(x::X), :(y::Y)] + sig # [:(x::X), :(y::Y)] traits # (D1{X}, D2{X,Y}) body # quote ... end end -function ==(p::ParsedFn, q::ParsedFn) +function ==(p::ParsedFn, q::ParsedFn) out = true for n in names(p) out = out && getfield(p,n)==getfield(q,n) @@ -42,7 +42,7 @@ end function parsetraitfn_head(head::Expr) # Transforms # f1{X<:Int,Y; D1{X}, D2{X,Y}}(x::X,y::Y) - # + # # into a ParsedFn nametyp = head.args[1] @@ -57,16 +57,16 @@ end gettypesymbol(x::Expr) = x.args[1] # :(X1<:Int) gettypesymbol(x::Symbol) = x function translate_head(fn::ParsedFn) - # Takes output from parsetraitfn_head and + # Takes output from parsetraitfn_head and # renames sig and TypeVar: # f1{X,Y; D1{X}, D2{X,Y}}(x::X,y::Y) # -> # f1{X1,X2; D1{X1}, D2{X1,X2}}(x::X1,y::X2) # # Returns translated ParsedFn - + function make_trans(sig, typs) - # makes two dictionaries with keys the old typevars, and + # makes two dictionaries with keys the old typevars, and # values the lowercase and uppercase variables # make variable-symbol translation map: @@ -85,7 +85,7 @@ function translate_head(fn::ParsedFn) return trans_var, trans_Tvar end trans_var, trans_Tvar = make_trans(fn.sig, fn.typs) - + # do the translations: fnt = deepcopy(fn) for i in 2:length(fnt.fun.args) @@ -121,7 +121,7 @@ function translate_head(fn::ParsedFn) for t in fnt.traits t.args[2:end] = map(x->trans_Tvar[x], t.args[2:end]) end - + return fnt end @@ -181,7 +181,7 @@ function get_concrete_type_symb(typs) # [:(X<:Int), :Y] -> [:Int, :Any] out = Any[] for t in typs - if isa(t, Symbol) + if isa(t, Symbol) push!(out, :Any) elseif t.head==:. push!(out, t) @@ -238,20 +238,20 @@ end @doc """The heart, the trait-dispatch function. - + Trait-function (TF) dispatch works like: - first dispatch on the normal types - + Then dispatch on traits using the following rules, terminating when only one or zero possibilities are left - find all matching traits - discriminate using subtraits, i.e. a subtrait will win over its supertrait - score all traits according to: - 1 point for all single parameter traits, + 1 point for all single parameter traits, 2 points for all two parameter traits, - etc. + etc. Now pick the highest scoring method. - if still ambiguous throw an error """-> @@ -288,10 +288,10 @@ function traitdispatch(traittypes, fname) # - pick method with most points # # This is not the end of the story but better... - score = zeros(Int, length(poss)) + score = zeros(Float64, length(poss)) for (i,p1) in enumerate(poss) for t in p1 - score[i] += length(t.parameters) + score[i] += trait_match_scores[ t.name.name ] end end poss = poss[find(maximum(score).==score)] @@ -331,12 +331,11 @@ macro traitfn(fndef) throw(TraitException( "There are repeated traits in the trait signature of $(fndef.args[1])")) end - ## make primary function: f #### tf(x, y) # (Just overwrite definitions of f if they exists already, # generates warnings though...) - + # definition head: fn{X,Y}(x::X,y::Y) f = makefnhead(fn.name, fn.typs, fn.sig) # definition body: _trait_fn(_trait_type_f1(x,y) ), x, y) @@ -344,10 +343,10 @@ macro traitfn(fndef) args2 = Any[makefncall(fn.name, args1), fn.sig...] body = makefncall(fn.name, args2) f = :($f = $body) - + ## make function containing the logic: trait_f #### tf(::Type{(Traits...,)}, x, y) - # 1) make the traits-type + # 1) make the traits-type trait_typ = Expr(:tuple) append!(trait_typ.args, fn.traits) trait_typ = :(::Type{$trait_typ}) @@ -361,26 +360,27 @@ macro traitfn(fndef) #### tf(Traits._TraitStorage, sig...) # This function will return all defined Trait-tuples for a certain # signature. - + ## 1) Get the existing traits out of the trait_type_f: # These can be retrieved with the call: # trait_type_f(Traits._TraitStorage, ::Type{X}, ::Type{Y}...) for suitable X, Y... args1 = Any[:(Traits._TraitStorage), get_concrete_type_symb(fn.typs)...] trait_type_f_store_call = makefncall(fn.name, args1) - + args2 = Any[:(Traits._TraitStorage), fn.typs...] if has_only_one_method(fn.name, args2) - traittypes = eval_curmod(trait_type_f_store_call)[2] + tmp = eval_curmod(trait_type_f_store_call) + traittypes = eval_curmod(trait_type_f_store_call)[2] else traittypes = Any[] end - + ## 2) update old_traittypes with the new ones newtrait = Expr(:tuple, fnt.traits...) if !(newtrait in traittypes) push!(traittypes, newtrait) end - + ## 3) make new trait-type storage function # tf(::Type{Traits._TraitStorage}, ::Type{X}, ::Type{Y}...) sig2typs(sig) = [s.args[2] for s in fnt.sig] @@ -412,7 +412,7 @@ macro traitfn(fndef) return out end push!(trait_type_f.args, body) - + ## now put all together #### out = quote From 426352f1a7fc62d1a46a8a6cd5105629499136db Mon Sep 17 00:00:00 2001 From: Tony Fong Date: Tue, 13 Jan 2015 02:10:00 +0700 Subject: [PATCH 07/12] cache trait singleton --- src/Traits.jl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Traits.jl b/src/Traits.jl index 81cb118..4590f7e 100644 --- a/src/Traits.jl +++ b/src/Traits.jl @@ -92,8 +92,9 @@ function istrait{T<:Trait}(Tr::Type{T}; verbose=false) # check supertraits !istrait(traitgetsuper(Tr); verbose=verbose) && return false # check methods definitions + local tr::T try - Tr() + tr=Tr() catch if verbose println("""Not all generic functions of trait $Tr are defined. @@ -120,7 +121,7 @@ function istrait{T<:Trait}(Tr::Type{T}; verbose=false) end anytypevars = false # check call signature of methods: - for (meth,sig) in Tr().methods + for (meth,sig) in tr.methods # instead of: ## checks = length(methods(meth, sig[1]))>0 # Now using method_exists. But see bug @@ -152,7 +153,7 @@ function istrait{T<:Trait}(Tr::Type{T}; verbose=false) # unfortunately if the sig has TypeVar in them it doesn't seem possible to check # the return type if !anytypevars && flag_check_return_types && out # only check if all methods were defined - for (meth,sig) in Tr().methods + for (meth,sig) in tr.methods # replace All in sig[1] with Any sigg = map(x->x===All ? Any : x, sig[1]) tmp = Base.return_types(meth, sigg) @@ -177,7 +178,7 @@ function istrait{T<:Trait}(Tr::Type{T}; verbose=false) end end # check constraints - if !all(Tr().constraints) + if !all(tr.constraints) if verbose println("Not all constraints are satisfied for $T") end From 69188652ed1ac5189f1b179e7dfb97c1e3e61a59 Mon Sep 17 00:00:00 2001 From: Tony Fong Date: Tue, 13 Jan 2015 02:10:14 +0700 Subject: [PATCH 08/12] better tests and examples --- examples/monads.jl | 128 ++++++++++++++++++++++++++++++-------------- test/paramtraits.jl | 16 ++++-- 2 files changed, 101 insertions(+), 43 deletions(-) diff --git a/examples/monads.jl b/examples/monads.jl index d9621ae..2ac75a4 100644 --- a/examples/monads.jl +++ b/examples/monads.jl @@ -6,6 +6,8 @@ end call{TI,TO}( fs::FuncFullSig{TI,TO}, x::TI ) = (fs.f(x))::TO +# I use the prefix "Semi" to remind ourselves that the +# there may not be a guarantee of function output type @traitdef SemiFunctor{X{Y}} begin fmap( Function, X{Y} ) -> Any end @@ -14,62 +16,110 @@ end fmap( FuncFullSig{Y,Z}, X{Y} ) -> X{Z} end +@traitdef SemiMonad{X{Y}} begin + mreturn( ::X,Y) ->X{Y} + bind( X{Y}, Function ) -> Any +end + +# ::X is the shorthand for singleton type argument @traitdef Monad{X{Y}} begin mreturn(::X, Y) -> X{Y} # we cannot infer X so we have to supply it bind( X{Y}, FuncFullSig{Y, X{Y}} ) -> X{Y} end -@traitimpl SemiFunctor{ Array{Y...} } begin - fmap{Y}( f::Function, x::Array{Y,1} ) = map(f, x) -end - -@traitimpl Functor{ Array{Y...} } begin - fmap{Y,Z}( f::FuncFullSig{Y,Z}, x::Array{Y} ) = Z[ f.f(i) for i in x ] -end - -@traitimpl Monad{ Array{Y...} } begin - mreturn{Y}( ::Type{Array}, x::Y ) = Y[x] - bind{Y,Z}( x::Array{Y}, f::FuncFullSig{Y, Array{Z}} ) = begin - ret = Z[] - for c in x - append!( ret, f.f( x ) ) - end - ret - end -end - -@traitimpl Monad{Nullable{Y}} begin +# === implementation of traits +@traitimpl SemiMonad{ Nullable{Y} } begin mreturn{Y}( ::Type{Nullable}, x::Y ) = Nullable{Y}(x) - bind{Y,Z}( x::Nullable{Y}, f::FuncFullSig{Y, Nullable{Z}} ) = begin + bind{Y}( x::Nullable{Y}, f::Function ) = begin if isnull(x) - return Nullable{Z}() + return Nullable() else try - return f.f( x.value )::Nullable{Z} + return f( x.value ) catch - return Nullable{Z}() + return Nullable() end end end end -@assert isequal( mreturn( Nullable, 1.0 ), Nullable{Float64}( 1.0 ) ) -testfunc( x::Float64 ) = x == 0.0 ? Nullable{Float64}() : Nullable{Float64}( 1.0/x ) -@assert isequal( bind( Nullable{Float64}( 2.0 ), FuncFullSig{Float64,Nullable{Float64}}( testfunc ) ), Nullable{Float64}( 0.5 ) ) -@assert isequal( bind( Nullable{Float64}( 0.0 ), FuncFullSig{Float64,Nullable{Float64}}( testfunc ) ), Nullable{Float64}() ) +@traitimpl SemiMonad{ Array{Y...} } begin + mreturn{Y}( ::Type{Array}, x::Y ) = Y[x] + bind{Y}( x::Array{Y,1}, f::Function ) = [ f(_) for _ in x ] +end -import Base: bind -@traitfn bind{S;Monad{S}}( x::S, f::Function ) = bind(x,FuncFullSig{Traits.tparlast(S),S}( f ) ) +# === some combo traits ====== +@traitdef MonadRelated1{ X{Y}, Z } <: SemiMonad{X} begin + @constraints begin + Y == Z + end +end -# Note: FuncFullSig{TO} is itself a Monad -#= -@traitfn mreturn{;Monad{FuncFullSig{T}}}( x::T ) = FuncFullSig{T,T}(_->x) -@traitfn mreturn{;Monad{Array{T}}}( a::T ) = T[ a ] -#@traitfn mreturn{;Monad{Nullable{T}}}( a::T ) = Nullable{T}(a) +@traitimpl SemiFunctor{ Array{Y...} } begin + fmap{Y}( f::Function, x::Array{Y,1} ) = map(f, x) +end + +# the deparameterize_type is ugly, but at least we don't have +# to do it many times +@traitfn mequal{M,Y;MonadRelated1{M,Y}}( x::M, y::Y ) = + isequal( x, mreturn( Traits.deparameterize_type(M), y ) ) + +@traitfn mequal{Y,M;MonadRelated1{M,Y}}( x::Y, y::M ) = + isequal( y, mreturn( Traits.deparameterize_type(M), x ) ) + +mequal( x, y ) = isequal( x, y ) + +@assert mequal( Nullable( 1.0 ), 1.0 ) +@assert mequal( 1.0, Nullable( 1.0 ) ) + +# but it's better than that. Since we have Array being a SemiMonad, +# we get this for free +@assert mequal( 1.0, [ 1.0 ] ) +@assert mequal( [1.0], 1.0 ) + +# now we compare an Array of nullables and a simple Array +@traitdef CollectionM{X{Y}} <: Collection{X} begin + @constraints begin + istrait( SemiMonad{Y} ) # we cannot put it in the header + end +end +@traitdef IterM{X{Y}} <: Iter{X} begin + @constraints begin + istrait( SemiMonad{Y} ) # we cannot put it in the header + end +end -@traitfn function bind{;Monad{Array{T}}}( x::Array{T}, f::FuncFullSig{T, Array{T} } ) - T[ map( f.f, x )... ] +@traitfn mequal(X,Y;CollectionM{X}, Collection{Y})( xc::X, yc::Y ) = begin + println( "using CollectionM comparison") + if length(xc) != length(yc) + return false + end + xs = start(xc) + ys = start(yc) + while( !done(xc, xs) ) + x,xs = next(xc,xs) + y,ys = next(yc,ys) + if !mequal( x, y ) + return false + end + end + return true end -@traitfn function bind{;Monad{FuncFullSig{T}}}( x::FuncFullSig{T}, f::FuncFullSig{T, FuncFullSig{T} }) +@traitfn mequal(X,Y;IterM{X}, Iter{Y})( xc::X, yc::Y ) = begin + println( "using IterM comparison") + xs = start(xc) + ys = start(yc) + while( !done(xc, xs) && !done( yc, ys) ) + x,xs = next(xc,xs) + y,ys = next(yc,ys) + if !mequal( x, y ) + return false + end + end + if !done(xc,xs) || !done(yc,ys) + return false + end + return true end -=# + +@assert mequal( [ Nullable(1.0), Nullable(2.0) ], [1.0, 2.0 ] ) diff --git a/test/paramtraits.jl b/test/paramtraits.jl index d4458d9..38f7c1d 100644 --- a/test/paramtraits.jl +++ b/test/paramtraits.jl @@ -5,19 +5,27 @@ using Base.Test fmap( Function, X{Y} ) -> Any end +# this definition should satisfy the SemiFunctor requirement for Nullable fmap{Y}( f::Function, x::Nullable{Y} ) = Nullable( f(x.value) ) println( "test SemiFunctor{Nullable}" ) @test istrait( SemiFunctor{Nullable{Int}}) -# This change the order +# This change the default type parameter position. +# Note that it doesn't change the fact that Array has the same SemiFunctor +# trait, just in this case (Array) we should use the first type parameter +# in the trait context. +# There is no other easy way to do it. @traitimpl SemiFunctor{ Array{Y...} } begin fmap{Y}( f::Function, x::Array{Y,1} ) = map(f, x) end -@test Traits.tparsuffix( SemiFunctor, Val{1}, Array{Int,1} ) != () +# This test that the suffix would be (1,) +print( "Type parameter suffix for SemiFunctor{Array{Int,1}} == ") +println( Traits.tparsuffix( SemiFunctor, Val{1}, Array{Int,1} ) ) +@test Traits.tparsuffix( SemiFunctor, Val{1}, Array{Int,1} ) == (1,) -println( "test istrait{ SemiFunctor{Array{Int,1}}}" ) +println( "test istrait{ SemiFunctor{Array{Int,1}}} == true" ) @test istrait( SemiFunctor{Array{Int,1}} ) -println( "test istrait{ SemiFunctor{Array{Int,2}}}" ) +println( "test istrait{ SemiFunctor{Array{Int,2}}} == false" ) @test istrait( SemiFunctor{Array{Int,2}} ) == false From 576dd456efe31cc0ac1a6fac0691ab988fc20db0 Mon Sep 17 00:00:00 2001 From: Tony Fong Date: Tue, 13 Jan 2015 02:30:44 +0700 Subject: [PATCH 09/12] "fixed" previous ambiguities --- src/traitdef.jl | 2 +- test/manual-traitdef.jl | 32 ++++++++++++++++++++++---------- test/traitdispatch.jl | 4 ++-- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/traitdef.jl b/src/traitdef.jl index 2800364..6599b4d 100644 --- a/src/traitdef.jl +++ b/src/traitdef.jl @@ -113,7 +113,7 @@ function parsetraithead(def::Expr) maxscore = max( trait_match_scores[st], maxscore ) eval_curmod(:(@assert istraittype($st))) end - basescore = maxscore + 1.0 + 0.1 * length( supertraits.args ) + basescore = maxscore + 1.0 + 0.1 * length( supertraits.args ) + 0.01 * (length( paras )-1) # make :(immutable Cmp{X,Y} <: Trait{(Eq{X,Y}, Tr1{X})} end) out = :(immutable $trait <: Traits.Trait{$supertraits} end) diff --git a/test/manual-traitdef.jl b/test/manual-traitdef.jl index a212e0d..1169d5a 100644 --- a/test/manual-traitdef.jl +++ b/test/manual-traitdef.jl @@ -3,8 +3,8 @@ # All type belong to the empty trait, as it makes no restriction on # the types: -@test istrait( () ) - +@test istrait( () ) + immutable Tr1{X1} <: Traits.Trait{()} methods @@ -22,6 +22,10 @@ immutable Tr3{X1,X2} <: Traits.Trait{(Tr1{X1}, Tr2{X1,X2})} Tr3() = new(Dict(), []) end +Traits.trait_match_scores[:Tr1] = 1.0 +Traits.trait_match_scores[:Tr2] = 1.0 +Traits.trait_match_scores[:Tr3] = 2.2 + @test istraittype(Tr1) @test istraittype(Tr1{A1}) @test istraittype( (Tr1{A1},Tr2{A1,A2}) ) @@ -32,7 +36,7 @@ end @test traitgetsuper(Tr3{A1,A2})==(Tr1{A1},Tr2{A1,A2}) # any type is part of a unconstrained trait: -@test istrait(Tr1{Int}) +@test istrait(Tr1{Int}) @test istrait(Tr2{DataType,Int}) @test istrait(Tr3{String,DataType}) @test_throws TraitException istrait(Tr3{:a,7}) # maybe this should error? @@ -40,23 +44,25 @@ end immutable D1{X1} <: Traits.Trait{()} methods constraints - function D1() + function D1() new(Dict( - sin => ((X1,), (Float64,)), - cos => ((X1,), (Float64,)), + sin => ((X1,), (Float64,)), + cos => ((X1,), (Float64,)), ), [] ) end end +Traits.trait_match_scores[:D1] = 1.0 + @test istrait(D1{Int}) @test !istrait(D1{String}) immutable D2{X1,X2} <: Traits.Trait{(D1{X1}, D1{X2})} methods constraints - function D2() + function D2() new(Dict( (+) => ((X1, X2), (Any,)), (-) => ((X1, X2), (Any,)) @@ -66,13 +72,15 @@ immutable D2{X1,X2} <: Traits.Trait{(D1{X1}, D1{X2})} end end +Traits.trait_match_scores[:D2] = 2.2 + @test istrait(D2{Int, Int}) @test !istrait(D2{Int, String}) immutable D3{X1} <: Traits.Trait{()} methods constraints - function D3() + function D3() new(Dict( getkey => ((X1,Any,Any), (Any,)), get! => ((X1, Any, Any), (Any,)) @@ -81,11 +89,12 @@ immutable D3{X1} <: Traits.Trait{()} ) end end +Traits.trait_match_scores[:D3] = 1.0 immutable D4{X1,X2} <: Traits.Trait{()} # like D2 but without supertraits methods constraints - function D4() + function D4() new(Dict( (+) => ((X1, X2), (Any,)), (-) => ((X1, X2), (Any,)) @@ -95,6 +104,7 @@ immutable D4{X1,X2} <: Traits.Trait{()} # like D2 but without supertraits end end +Traits.trait_match_scores[:D4] = 1.02 @test istrait(D3{Dict{Int,Int}}) @test !istrait(D3{Int}) @@ -110,7 +120,7 @@ immutable CTr1{X1,X2} <: Traits.Trait{()} constraints::Array{Bool,1} # constraints are an array of functions # which need to evaluate to true. Their # signature is f(X,Y) = ... - + function CTr1() new(Dict( (+) => ((X1, X2), (Any,)), @@ -121,6 +131,7 @@ immutable CTr1{X1,X2} <: Traits.Trait{()} ) end end +Traits.trait_match_scores[:CTr1] = 1.1 @test !istrait(CTr1{Int32, Int}) @test istrait(CTr1{Int, Int}) @@ -146,6 +157,7 @@ immutable CTrAs{X1,X2} <: Traits.Trait{()} ) end end +Traits.trait_match_scores[:CTrAs] = 1.0 @test istrait(CTrAs{Int32, Int}) # @test istrait(CTrAs{Integer, Integer}) # doesn't work because return type of /(Integer, Integer)==Any diff --git a/test/traitdispatch.jl b/test/traitdispatch.jl index 5653427..db6585b 100644 --- a/test/traitdispatch.jl +++ b/test/traitdispatch.jl @@ -174,8 +174,8 @@ end end @traitfn tf7465{X<:Integer,Y; TrTr22{X,Y}}(x::X,y::Y) = x*y*1000 - # errors again because ambigours again -@test_throws Traits.TraitException tf7465(5,6) + # no ambiguities because TrTr22 is hierarchically more specific +@test tf7465(5,6) == 5*6*1000 ## single argument ambiguities #### From c617cdd9313434fe2c33eb7c072e8197891253be Mon Sep 17 00:00:00 2001 From: Tony Fong Date: Tue, 13 Jan 2015 18:41:34 +0700 Subject: [PATCH 10/12] fix distracting error in examples --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 35a0466..77d0982 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ end # for parametric trait, @traitimpl SemiFunctor{Nullable{T}} begin - fmap{T}( f::Function, x::Nullable{T}) = Nullable(f(x.value)) + fmap{T}( f::Function, x::Nullable{T}) = isnull(x) ? Nullable() : Nullable(f(x.value)) end # for Array, it is a bit difficult because the eltype is the first argument. From d9210506e420909b892268dbca56328b2fea6212 Mon Sep 17 00:00:00 2001 From: Tony Fong Date: Tue, 13 Jan 2015 21:27:13 +0700 Subject: [PATCH 11/12] bugfix multi-parameter-parametric trait --- src/traitdef.jl | 17 +++++++++-------- test/paramtraits.jl | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/traitdef.jl b/src/traitdef.jl index 6599b4d..e5ca8f4 100644 --- a/src/traitdef.jl +++ b/src/traitdef.jl @@ -170,20 +170,21 @@ function parsebody(name::Symbol, body::Expr, paras::Array{Any,1}, headassoc::Arr rootsym = symbol( string(p.args[1],"0") ) hosttype = p.args[1] tailsym = symbol( string(p.args[1],"0_" ) ) + params_rename[ hosttype ] = rootsym + push!( assoc.args, :($rootsym = Traits.tparprefix( $name, Val{$i}, $hosttype ) ) ) if !s_inited - params_rename[ hosttype ] = rootsym - push!( assoc.args, :($rootsym = Traits.tparprefix( $name, Val{$i}, $hosttype ) ) ) push!( assoc.args, :($s = Traits.tparget( $name, Val{$i}, $hosttype ) ) ) - push!( assoc.args, :($tailsym = Traits.tparsuffix( $name, Val{$i}, $hosttype ) ) ) - push!( local_typesyms, rootsym ) - push!( local_typesyms, s ) - push!( local_typesyms, tailsym ) s_inited=true else - teststmt = :( @assert( $s == Traits.tparget( $name, Val{$i}, $hosttype ) ) ) - push!( teststmt.args, :( string( "In ", p, ", ", s, " does not match an earlier definition" ) ) ) + teststmt = :( @assert( isequal( $s, Traits.tparget( $name, Val{$i}, $hosttype ) ) ) ) + #push!( teststmt.args, @sprintf( "In %s, %s does not match an earlier definition", p, s ) ) + #@show( teststmt ) push!( assoc.args, teststmt ) end + push!( assoc.args, :($tailsym = Traits.tparsuffix( $name, Val{$i}, $hosttype ) ) ) + push!( local_typesyms, rootsym ) + push!( local_typesyms, s ) + push!( local_typesyms, tailsym ) end end end diff --git a/test/paramtraits.jl b/test/paramtraits.jl index 38f7c1d..613b1af 100644 --- a/test/paramtraits.jl +++ b/test/paramtraits.jl @@ -29,3 +29,24 @@ println( "test istrait{ SemiFunctor{Array{Int,1}}} == true" ) @test istrait( SemiFunctor{Array{Int,1}} ) println( "test istrait{ SemiFunctor{Array{Int,2}}} == false" ) @test istrait( SemiFunctor{Array{Int,2}} ) == false + +println( "test multi-field traits each field is parametric") +@traitdef MyParamTr1{X{Y}, Z{Y}} begin + @constraints begin + Y <: Number + end + ftest1( X{Y}, Z{Y} ) -> X{Z{Y}} +end + +println( " ... test its implementation" ) +@traitimpl MyParamTr1{ Array{T...}, Nullable{T} } begin + ftest1{T}( a::Array{T,1}, x::Nullable{T} ) = isnull(x) ? + Nullable{T}[] : Nullable{T}[ Nullable(_*x.value) for _ in a ] +end + +println( " ... check Array x Nullable" ) +@assert istrait( MyParamTr1{ Array{Int,1}, Nullable{Int} }, verbose=true ) +@assert ! istrait( MyParamTr1{ Array{Int,2}, Nullable{Int} } ) +@assert ! istrait( MyParamTr1{ Array{Int,1}, Nullable{Float64} } ) +println( " ... check dispatch") +@assert isequal( ftest1( [1,2,3], Nullable(2) ),[Nullable(2),Nullable(4),Nullable(6)] ) From fa5c0b403ec85a8e03555e99e792f132c100adad Mon Sep 17 00:00:00 2001 From: Tony Fong Date: Wed, 14 Jan 2015 23:36:44 +0700 Subject: [PATCH 12/12] revert dispatch scoring mechanism to status quo --- examples/monads.jl | 14 ++++++++++---- src/helpers.jl | 2 -- src/traitdef.jl | 10 ++-------- src/traitfns.jl | 4 ++-- test/manual-traitdef.jl | 13 ------------- test/traitdispatch.jl | 4 ++-- 6 files changed, 16 insertions(+), 31 deletions(-) diff --git a/examples/monads.jl b/examples/monads.jl index 2ac75a4..b080f68 100644 --- a/examples/monads.jl +++ b/examples/monads.jl @@ -18,19 +18,19 @@ end @traitdef SemiMonad{X{Y}} begin mreturn( ::X,Y) ->X{Y} - bind( X{Y}, Function ) -> Any + bind( Function, X{Y} ) -> Any end # ::X is the shorthand for singleton type argument @traitdef Monad{X{Y}} begin mreturn(::X, Y) -> X{Y} # we cannot infer X so we have to supply it - bind( X{Y}, FuncFullSig{Y, X{Y}} ) -> X{Y} + bind( FuncFullSig{Y, X{Y}}, X{Y} ) -> X{Y} end # === implementation of traits @traitimpl SemiMonad{ Nullable{Y} } begin mreturn{Y}( ::Type{Nullable}, x::Y ) = Nullable{Y}(x) - bind{Y}( x::Nullable{Y}, f::Function ) = begin + bind{Y}( f::Function, x::Nullable{Y} ) = begin if isnull(x) return Nullable() else @@ -45,7 +45,7 @@ end @traitimpl SemiMonad{ Array{Y...} } begin mreturn{Y}( ::Type{Array}, x::Y ) = Y[x] - bind{Y}( x::Array{Y,1}, f::Function ) = [ f(_) for _ in x ] + bind{Y}( f::Function, x::Array{Y,1} ) = [ f(_) for _ in x ] end # === some combo traits ====== @@ -89,6 +89,12 @@ end end end +@traitdef SemiFunctorMonad{X{Y}} <: SemiFunctor{X} begin + @constraints begin + istrait( SemiMonad{Y} ) + end +end + @traitfn mequal(X,Y;CollectionM{X}, Collection{Y})( xc::X, yc::Y ) = begin println( "using CollectionM comparison") if length(xc) != length(yc) diff --git a/src/helpers.jl b/src/helpers.jl index da291a8..32f4440 100644 --- a/src/helpers.jl +++ b/src/helpers.jl @@ -250,8 +250,6 @@ function method_exists_tvars( f::Function, argts::Tuple, verbose::Bool ) return false end -trait_match_scores = Dict{Symbol,Float64}() - # # check whether a function is parameterized # function isparameterized(m::Method) # if isa(m.tvars, Tuple) diff --git a/src/traitdef.jl b/src/traitdef.jl index e5ca8f4..cf5c754 100644 --- a/src/traitdef.jl +++ b/src/traitdef.jl @@ -106,14 +106,10 @@ function parsetraithead(def::Expr) error("Interface specification error") end # check supertraits<:Traits - maxscore = 0.0 for i =1:length(supertraits.args) - global trait_match_scores st = supertraits.args[i].args[1] - maxscore = max( trait_match_scores[st], maxscore ) eval_curmod(:(@assert istraittype($st))) end - basescore = maxscore + 1.0 + 0.1 * length( supertraits.args ) + 0.01 * (length( paras )-1) # make :(immutable Cmp{X,Y} <: Trait{(Eq{X,Y}, Tr1{X})} end) out = :(immutable $trait <: Traits.Trait{$supertraits} end) @@ -127,7 +123,7 @@ function parsetraithead(def::Expr) end end end - return out, name, paras, headassoc, basescore + return out, name, paras, headassoc end # 2) parse the function definitions @@ -386,11 +382,10 @@ end """ -> macro traitdef(head, body) ## make Trait type - traithead, name, paras, headassoc, basescore = parsetraithead(head) + traithead, name, paras, headassoc= parsetraithead(head) # make the body meths, constr, assoc = parsebody(name, body, paras, headassoc) # make sure a generic function of all associated types exisits - global trait_match_scores traitbody = quote methods::Dict{Union(Function,DataType), Tuple} @@ -401,7 +396,6 @@ macro traitdef(head, body) new( $meths, $constr, assoctyps) end end - trait_match_scores[ name ] = basescore + 0.1 * (length( constr.args )-1) # add body to the type definition traithead.args[3] = traitbody return esc(traithead) diff --git a/src/traitfns.jl b/src/traitfns.jl index c7cadaa..e310127 100644 --- a/src/traitfns.jl +++ b/src/traitfns.jl @@ -288,10 +288,10 @@ function traitdispatch(traittypes, fname) # - pick method with most points # # This is not the end of the story but better... - score = zeros(Float64, length(poss)) + score = zeros(Int, length(poss)) for (i,p1) in enumerate(poss) for t in p1 - score[i] += trait_match_scores[ t.name.name ] + score[i] += length( t.parameters ) end end poss = poss[find(maximum(score).==score)] diff --git a/test/manual-traitdef.jl b/test/manual-traitdef.jl index 1169d5a..89a5b87 100644 --- a/test/manual-traitdef.jl +++ b/test/manual-traitdef.jl @@ -22,10 +22,6 @@ immutable Tr3{X1,X2} <: Traits.Trait{(Tr1{X1}, Tr2{X1,X2})} Tr3() = new(Dict(), []) end -Traits.trait_match_scores[:Tr1] = 1.0 -Traits.trait_match_scores[:Tr2] = 1.0 -Traits.trait_match_scores[:Tr3] = 2.2 - @test istraittype(Tr1) @test istraittype(Tr1{A1}) @test istraittype( (Tr1{A1},Tr2{A1,A2}) ) @@ -54,8 +50,6 @@ immutable D1{X1} <: Traits.Trait{()} end end -Traits.trait_match_scores[:D1] = 1.0 - @test istrait(D1{Int}) @test !istrait(D1{String}) @@ -72,8 +66,6 @@ immutable D2{X1,X2} <: Traits.Trait{(D1{X1}, D1{X2})} end end -Traits.trait_match_scores[:D2] = 2.2 - @test istrait(D2{Int, Int}) @test !istrait(D2{Int, String}) @@ -89,7 +81,6 @@ immutable D3{X1} <: Traits.Trait{()} ) end end -Traits.trait_match_scores[:D3] = 1.0 immutable D4{X1,X2} <: Traits.Trait{()} # like D2 but without supertraits methods @@ -104,8 +95,6 @@ immutable D4{X1,X2} <: Traits.Trait{()} # like D2 but without supertraits end end -Traits.trait_match_scores[:D4] = 1.02 - @test istrait(D3{Dict{Int,Int}}) @test !istrait(D3{Int}) @@ -131,7 +120,6 @@ immutable CTr1{X1,X2} <: Traits.Trait{()} ) end end -Traits.trait_match_scores[:CTr1] = 1.1 @test !istrait(CTr1{Int32, Int}) @test istrait(CTr1{Int, Int}) @@ -157,7 +145,6 @@ immutable CTrAs{X1,X2} <: Traits.Trait{()} ) end end -Traits.trait_match_scores[:CTrAs] = 1.0 @test istrait(CTrAs{Int32, Int}) # @test istrait(CTrAs{Integer, Integer}) # doesn't work because return type of /(Integer, Integer)==Any diff --git a/test/traitdispatch.jl b/test/traitdispatch.jl index 1f32734..e9cf01c 100644 --- a/test/traitdispatch.jl +++ b/test/traitdispatch.jl @@ -174,8 +174,8 @@ end end @traitfn tf7465{X<:Integer,Y; TrTr22{X,Y}}(x::X,y::Y) = x*y*1000 - # no ambiguities because TrTr22 is hierarchically more specific -@test tf7465(5,6) == 5*6*1000 +# ambiguities because TrTr22 and TrTr1{X},TrTr1{Y} are equally applicable +@test_throws Traits.TraitException tf7465(5,6) ## single argument ambiguities ####