Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Builds on and supersedes #39 #41

Merged
merged 8 commits into from
Apr 23, 2017
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -12,6 +12,6 @@ matrix:
notifications:
email: false
# uncomment the following lines to override the default test script
# script:
# - if [[ -a .git/shallow ]]; then git fetch --unshallow; fi
# - julia --check-bounds=yes -e 'Pkg.clone(pwd()); Pkg.build("SimpleTraits"); Pkg.test("SimpleTraits"; coverage=true)'
script:
- if [[ -a .git/shallow ]]; then git fetch --unshallow; fi
- julia --check-bounds=yes -e 'Pkg.clone(pwd()); Pkg.build("SimpleTraits"); Pkg.test("SimpleTraits"; coverage=false)'
75 changes: 62 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -62,12 +62,21 @@ it can be used like so:
```julia
@traitimpl IsNice{X} <- isnice(X)
```

i.e. any type `X` for which `isnice(X)==true` belongs to `IsNice`.
Note that this generates a generated-function under the hood and thus
[restrictions](http://docs.julialang.org/en/release-0.5/manual/metaprogramming/#generated-functions)
on `isnice` apply. Last note that in above example the `@traitimpl IsNice{Int}` "wins"
over the `@traitimpl IsNice{X} <- isnice(X)`, thus
this can be used to define exceptions to a rule.
Notes:

- on Julia-0.5 this generates a generated-function under the
hood and thus [restrictions](http://docs.julialang.org/en/release-0.5/manual/metaprogramming/#generated-functions)
on `isnice` apply.
- on Julia-0.6 no generated function is used and overhead-less dispatch
is only possible if `isnice` is *pure*: "[A pure method]
promises that the result will always be the same constant regardless
of when the method is called [for the same input arguments]."
([ref](https://github.com/mauro3/SimpleTraits.jl/pull/39#issuecomment-293629338)).
- Last note that in above example the `@traitimpl
IsNice{Int}` "wins" over the `@traitimpl IsNice{X} <- isnice(X)`, thus
this can be used to define exceptions to a rule.

It can be checked whether a type belongs to a trait with `istrait`:
```julia
@@ -227,6 +236,24 @@ Source line: 185
shows that the normal method and the trait-method compile down to the
same machine instructions.

However, if the trait-grouping function is not constant or a generated
function then dispatch may be dynamic. This can be checked with
`@check_fast_traitdispatch`, which checks whether the number of lines
of LLVM code is the same for a trait function than a normal one:
```julia
# julia 0.6
checkfn(x) = rand()>0.5 ? true : false # a bit crazy!
@traitdef TestTr{X}
@traitimpl TestTr{X} <- checkfn(X)
# this tests a trait-function with TestTr{Int}:
@check_fast_traitdispatch TestTr
# this tests a trait-function with TestTr{String} and will
# also prints number of LLCM-IR lines of trait vs normal function:
@check_fast_traitdispatch TestTr String true
```
Note that this example only works in Julia 0.6, in Julia 0.5 it
produces just wrong results.

## Advanced features

The macros of the previous section are the official API of the package
@@ -244,9 +271,10 @@ returns `Not{TraitInQuestion{...}}` otherwise (this is the fall-back
for `<:Any`). So instead of using `@traitimpl` this can be coded
directly. Note that anything but a constant function will probably
not be inlined away by the JIT and will lead to slower dynamic
dispatch.
dispatch (see `@check_fast_traitdispatch` for a helper to check).

Example leading to dynamic dispatch:
Example leading to dynamic dispatch in Julia 0.5 (but works well in
Julia 0.6):
```julia
@traitdef IsBits{X}
SimpleTraits.trait{X1}(::Type{IsBits{X1}}) = isbits(X1) ? IsBits{X1} : Not{IsBits{X1}}
@@ -259,17 +287,26 @@ istrait(IsBits{A}) # true
```

Dynamic dispatch can be avoided using a generated
function (or maybe sometimes `Base.@pure` functions?):
function or *pure* functions in Julia-0.6 (sometimes they need to be
annotated with `Base.@pure`):
```julia
@traitdef IsBits{X}
@generated function SimpleTraits.trait{X1}(::Type{IsBits{X1}})
isbits(X1) ? :(IsBits{X1}) : :(Not{IsBits{X1}})
end
```
Note that these programmed-traits can be combined with `@traitimpl`,
i.e. program the general case and add exceptions with `@traitimpl`.

Trait-inheritance can also be hand-coded with above trick. For
What is allowed in generated functions is heavily restricted, see
[Julia manual](https://docs.julialang.org/en/latest/manual/metaprogramming.html#Generated-functions-1).
In particular (in Julia 0.6), no methods which are defined after the
generated function are allowed to be called inside the generated
function, otherwise
[this](https://github.com/JuliaLang/julia/issues/21356) issue is
encountered.

Note that these programmed-traits can be combined with `@traitimpl IsBits{XYZ}`,
i.e. program the general case and add exceptions with `@traitimpl IsBits{XYZ}`.

Trait-inheritance can also be hand-coded with above trick, in Julia 0.5. For
instance, the trait given by (in pseudo syntax) `BeautyAndBeast{X,Y} <: IsNice{X},
!IsNice{Y}, BelongTogether{X,Y}`:
```julia
@@ -281,7 +318,18 @@ instance, the trait given by (in pseudo syntax) `BeautyAndBeast{X,Y} <: IsNice{X
:(Not{BeautyAndBeast{X,Y}})
end
end
# or in 0.6
function SimpleTraits.trait{X,Y}(::Type{BeautyAndBeast{X,Y}})
if istrait(IsNice{X}) && !istrait(IsNice{Y}) && BelongTogether{X,Y}
BeautyAndBeast{X,Y}
else
Not{BeautyAndBeast{X,Y}}
end
end
```
Note that in Julia 0.6, this will lead to slower, dynamic dispatch, as
the latter function is not pure (it depends on the global state of
which types belong to the traits `IsNice` and `BelongTogether`).


Note also that trait functions can be generated functions:
@@ -390,7 +438,8 @@ on letting functions use dispatch based on traits. This dispatch is
currently fairly limited, see section "Gotcha" above, but may be
expanded in the future: either through something like in PR
[m3/multitraits](https://github.com/mauro3/SimpleTraits.jl/pull/2) or
through a more general generated-function approach.
through a more general generated-function approach (definitely not
valid anymore in Julia 0.6).

In the unlikely event that I find myself with too much time on my
hands, I may try to develop a companion package to allow the
100 changes: 77 additions & 23 deletions src/SimpleTraits.jl
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ const curmod = module_name(current_module())
# This is basically just adding a few convenience functions & macros
# around Holy Traits.

export Trait, istrait, @traitdef, @traitimpl, @traitfn, Not
export Trait, istrait, @traitdef, @traitimpl, @traitfn, Not, @check_fast_traitdispatch

# All traits are concrete subtypes of this trait. SUPER is not used
# but present to be compatible with Traits.jl.
@@ -121,8 +121,7 @@ false. This can be done with:
```julia
@traitimpl IsFast{T} <- isfast(T)
```
where `isfast` is that check-function. This generates a `@generated`
function under the hood.
where `isfast` is that check-function.

Note that traits implemented with the former of above methods will
override an implementation with the latter method. Thus it can be
@@ -152,14 +151,14 @@ macro traitimpl(tr)
if !negated
return quote
$fnhead = $trname{$(paras...)}
$isfnhead = true # Add the istrait definition as otherwise
# method-caching can be an issue.
VERSION < v"0.6-" && ($isfnhead = true) # Add the istrait definition as otherwise
# method-caching can be an issue.
end
else
return quote
$fnhead = Not{$trname{$(paras...)}}
$isfnhead = false# Add the istrait definition as otherwise
# method-caching can be an issue.
VERSION < v"0.6-" && ($isfnhead = false) # Add the istrait definition as otherwise
# method-caching can be an issue.
end
end
elseif tr.head==:call
@@ -169,23 +168,22 @@ macro traitimpl(tr)
Tr_{P1__} <- fn_(P2__) => (false, Tr, P1, fn, P2)
end
if negated
return esc(
quote
@generated function SimpleTraits.trait{$(P1...)}(::Type{$Tr{$(P1...)}})
Tr = $Tr
P1 = $P1
return $fn($(P2...)) ? :(Not{$Tr{$(P1...)}}) : :($Tr{$(P1...)})
end
end)
fn = Expr(:call, GlobalRef(SimpleTraits, :!), fn)
end
if VERSION < v"0.6-"
return esc(quote
@generated function SimpleTraits.trait{$(P1...)}(::Type{$Tr{$(P1...)}})
Tr = $Tr
P1 = $P1
return $fn($(P2...)) ? :($Tr{$(P1...)}) : :(Not{$Tr{$(P1...)}})
end
end)
else
return esc(
quote
@generated function SimpleTraits.trait{$(P1...)}(::Type{$Tr{$(P1...)}})
Tr = $Tr
P1 = $P1
return $fn($(P2...)) ? :($Tr{$(P1...)}) : :(Not{$Tr{$(P1...)}})
end
end)
return esc(quote
function SimpleTraits.trait{$(P1...)}(::Type{$Tr{$(P1...)}})
return $fn($(P2...)) ? $Tr{$(P1...)} : Not{$Tr{$(P1...)}}
end
end)
end
else
error("Cannot parse $tr")
@@ -453,6 +451,62 @@ findline(arg) = nothing
# Extras
####

# # This does not work. Errors on compilation with:
# # ERROR: LoadError: UndefVarError: Tr not defined
# function check_traitdispatch(Tr; nlines=5, Args=(Int,))
# @traitfn fn_test(x::::Tr) = 1
# @traitfn fn_test(x::::(!Tr)) = 2
# @assert llvm_lines(fn_test, Args) == nlines
# end


"""
check_fast_traitdispatch(Tr, Args=(Int,), nlines=6, verbose=false)

Macro to check whether a trait-dispatch is fast (i.e. as fast as an
ordinary function call) or whether dispatch is slow (dynamic). Only
works with single parameters traits (so far).

Optional arguments are:
- Type parameter to the trait (default `Int`)
- Verbosity (default `false`)

Example:

@check_fast_traitdispatch IsBits
@check_fast_traitdispatch IsBits String true

TODO: This is rather ugly. Ideally this would be a function but I ran
into problems, see source code. Also the macro is ugly. PRs welcome...
"""
macro check_fast_traitdispatch(Tr, Arg=:Int, verbose=false)
test_fn = gensym()
test_fn_null = gensym()
nl = gensym()
nl_null = gensym()
out = gensym()
esc(quote
$test_fn_null(x) = 1
$nl_null = SimpleTraits.llvm_lines($test_fn_null, ($Arg,))
@traitfn $test_fn(x::::$Tr) = 1
@traitfn $test_fn(x::::(!$Tr)) = 2
$nl = SimpleTraits.llvm_lines($test_fn, ($Arg,))
$out = $nl == $nl_null
if $verbose && !$out
println("Number of llvm code lines $($nl) but should be $($nl_null).")
end
$out
end)
end

"Returns number of llvm-IR lines for a call of function `fn` with argument types `args`"
function llvm_lines(fn, args)
io = IOBuffer()
Base.code_llvm(io, fn, args)
#Base.code_native(io, fn, args)
count(c->c=='\n', String(io))
end

include("base-traits.jl")

end # module
7 changes: 4 additions & 3 deletions src/base-traits.jl
Original file line number Diff line number Diff line change
@@ -11,16 +11,17 @@ export IsLeafType, IsBits, IsImmutable, IsContiguous, IsIndexLinear,
@traitimpl IsAnything{X} <- (x->true)(X)

"Trait which contains no types"
@compat const IsNothing{X} = Not{IsAnything{X}}

@traitdef IsNothing{X}
@traitimpl IsNothing{X} <- (x->false)(X)

"Trait of all isbits-types"
@traitdef IsBits{X}
@traitimpl IsBits{X} <- isbits(X)

"Trait of all immutable types"
@traitdef IsImmutable{X}
@traitimpl IsImmutable{X} <- (X->!X.mutable)(X)
Base.@pure _isimmutable(X) = !X.mutable
@traitimpl IsImmutable{X} <- _isimmutable(X)

"Trait of all callable objects"
@traitdef IsCallable{X}
20 changes: 20 additions & 0 deletions test/base-traits-inference.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Tests that trait dispatch for the BaseTraits does not incur a
# overhead.


# Dict with base-traits to check using value[1] as type and value[2]
# as number of lines allowed in llvm code
cutoff = 5
basetrs = [:IsLeafType=>:Int,
:IsBits=>:Int,
:IsImmutable=>:Int,
:IsContiguous=>:(SubArray{Int64,1,Array{Int64,1},Tuple{Array{Int64,1}},false}),
:IsIndexLinear=>:(Vector{Int}),
:IsAnything=>:Int,
:IsNothing=>:Int,
:IsCallable=>:(typeof(sin)),
:IsIterator=>:(Dict{Int,Int})]

for (bt, tp) in basetrs
@test @eval @check_fast_traitdispatch $bt $tp true
end
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -245,4 +245,5 @@ end
# Other tests
#####
include("base-traits.jl")
include("base-traits-inference.jl")
include("backtraces.jl")