tk3369 / binarytraits.jl Goto Github PK
View Code? Open in Web Editor NEWCan do or not? It's easy. See https://tk3369.github.io/BinaryTraits.jl/dev/
License: MIT License
Can do or not? It's easy. See https://tk3369.github.io/BinaryTraits.jl/dev/
License: MIT License
The README is growing pretty fast so it would be better to move to docs. Also, we can publish doc strings and execute doclets for developer documentation.
High level requirements
Some interfaces requires the implementer to define a function that returns true
so that it knows if a type has implemented the interface at runtime.
Examples:
Tables.istable
IteratorInterfaceExtensions.isiterable
Technically, this is not necessary anymore if these packages adopt BinaryTraits because they can just call the trait function e.g. trait(Table,x)
and it would return Is{Table}()
for a properly implemented type.
I do not expect, however, that everyone will adopt BinaryTraits. Yet, an implementer may still want to use BinaryTraits' function to verify correct implementation of the interface. It can be achieved if we have a "BinaryTraitsLibrary" that defines a bunch of well-known traits and the respective interface contracts. Now, one of the contracts for Tables.jl will be "hey, you must extend my istable
function and return true
".
For that reason, we can design syntax such that a contract must return a true
value.
This issue is used to trigger TagBot; feel free to unsubscribe.
If you haven't already, you should update your TagBot.yml
to include issue comment triggers.
Please see this post on Discourse for instructions and more details.
If you'd like for me to do this for you, comment TagBot fix
on this issue.
I'll open a PR within a few hours, please be patient!
The types/functions generated by the macro should be documented.
Keyword arguments are not supported
This is actually a limitation with Julia itself because keyword arguments are not considered for dispatch. I also wonder how useful it would be⦠Can you share a use case for this if you happen to have one?
Here's a sample usage of the @trait
macro:
@trait FlySwim as Ability prefix Can,Cannot with Fly,Swim
The issue is that you cannot specify the prefix-clause without the as-clause, and you cannot specify the with-clause without the prefix-clause. It would be nice if the as-clause, prefix-clause, and with-clause are all optional.
It seems to be a hassle to define the "entrypoint" function for every function that supports traits.
For example:
function plot(device::T, x::Vector{Float64}, y::Vector{Float64}; color = :black, linewidth = 1) where T
plot(trait(Plottable,T), device, x, y; color, linewidth)
end
It would be nice to have syntax like:
@holy Plottable plot(device, x::Vector{Float64}, y::Vector{Float64}; color = :black, linewidth = 1)
Note: I can't think of a good verb yet (hence @holy
π ).
It would be cool, if I could do:
@trait Sparse prefix Is,Not
@assign Adjoint{T,SparseVector{T}} where T with Sparse
Currently I have to use this work-around, because the @assign
macro expects a Symbol
as a first argument.
julia> @trait Sparse prefix Is,Not
julia> const AVS = Adjoint{<:Any, SparseVector}
Adjoint{var"#s54",SparseVector} where var"#s54"
julia> @assign AVS with Sparse
julia> @implement IsSparse by nnz()
julia> nnz(a::Adjoint{<:Any, SparseVector}) = nnz(a.data)
nnz (generic function with 2 methods)
julia> check(Adjoint{Float64, SparseVector})
β
Adjoint{Float64,SparseVector} has implemented:
1. SparseTrait: IsSparse β’ nnz(::<Type>)::Any
The specified argument type needs to be simple, for example no type parameter allowed
Right, I realized the missing feature as well. Currently, it needs to be a simple βsymbolβ for the parser to work. I guess itβs less convenient but you can define a constant and then use it in the interface. See the CartesianIndexing trait example.
MWE
using BinaryTraits
using BinaryTraits: Prefix
julia> @trait Foo
julia> @implement Prefix.Is{Foo} by f(_, x)
julia> struct Bar end
julia> f(::Bar, x) = 1
julia> @assign Bar with Prefix.Is{Foo}
julia> @check Bar
ERROR: Tuple field type cannot be Union{}
The issue seems to be covered by this issue on the Julia repo. If that problem is resolved such that we can use Tuple{Union{}}
again, I suspect this will go back to working
This is a pretty minor issue but prefixes are constants so they print out as Positive
or Negative
. There's clearly an advantage to having a constant instead needlessly creating a bunch of extra types, like unique binary prefix types. The only idea I really have is to create a specialized show
method inside the @trait
macro. Something like...
function Base.show(io::IO, ::MIME"text/plain", ::Negative{T}) where {T<:NewlyCreatedTrait}
print(io, "Not{$T}")
end
I don't see any other way of doing this than defining show
inside the macro because that's the only place you'll be able to track down the actually prefix syntax that is used.
It's also not a terribly big deal, unless for some reason people are printing a prefix in the REPL, so unless there's a more ideal solution available this might not even be worth addressing.
See https://discourse.julialang.org/t/ann-binarytraits-jl-a-new-traits-package/37475/5?u=tk3369
In a nutshell, the ideas are:
Up till now, the trait/assignment/interface/checker functionalities work fine if everything is defined in the same module. It becomes tricky when the trait is defined in one module but needs to be implemented in another. There are a couple of issues:
Namespace - what if the same trait is defined from different third-party modules and they mean different things?
Currently, the traits/prefix/interface maps are stored in BinaryTraits module and it is
In order to support a module implementing an interface defined from another module, I would like to propose a new syntax. The following hypothetical example illustrate how we could declare our data type for conforming to the Tables.jl's RowTable interface from this fork.
@assign AwesomeTable with Tables.RowTable
Having implemented PR #38 , it is now possible to implement this syntax change. The macro @assign
can just parse the module name (caution: it could have multiple tokens) and then look up the trait from the client module's traits map. In this case, the RowTable
trait would be stored in the Tables
module's map. When we check the interface contracts, we would have to look up the contracts as defined in Tables
module as well.
@KlausC - let me know what you think...
We still haven't incorporated the return type of contracts into the check
function. Ideally, this should be an optional thing because the compiler may not reliably infer the return type of a function. Perhaps the default is false
(i.e. do not check return type) because it's not 100% reliable but the user can turn that on when using the check
function.
Since we use Holy Traits pattern, it does not fit well with the current interface specification check. For example, with holy traits we should expect an implementation of liftoff(::CanFly, ::Duck)
rather than liftoff(::Duck)
as specified in the @implement
macro.
Is this a problem.... or not?
Hi,
I encountered the error when running the code below:
import Distributions: rand, logpdf
"""
Generalized distributions that support sampling (and evaluating the probability of)
structured data.
"""
abstract type GDistr end
@trait GDistr prefix Is, IsNot
@implement Is{GDistr} by rand(rng::AbstractRNG, _)
@implement Is{GDistr} by logpdf(_, x)
@assign Distribution with Is{GDistr}
@check Distribution
Is this the expected behavior? Or am I doing something wrong? (My Julia version is 1.7)
The stack trace:
ERROR: LoadError: type UnionAll has no field parameters
Stacktrace:
[1] getproperty
@ ./Base.jl:37 [inlined]
[2] check_method(m::Method, sig::Type{Tuple{typeof(Distributions.logpdf), Distributions.Distribution, Union{}}}, kwnames::Tuple{})
@ BinaryTraits ~/.julia/packages/BinaryTraits/58m13/src/interface.jl:256
[3] has_method(f::Any, t::Any, kwnames::Tuple{})
@ BinaryTraits ~/.julia/packages/BinaryTraits/58m13/src/interface.jl:244
[4] check(m::Module, T::UnionAll)
@ BinaryTraits ~/.julia/packages/BinaryTraits/58m13/src/interface.jl:56
[5] top-level scope
@ ~/.julia/dev/SEDL/src/distributions_utils.jl:17
It would be helpful if we could have traits subtype some type we define, rather than Any
. This allows us to filter traits by supertype when generating documentation.
For example, say I want to make an Animal
interface, and then I have Fish
and Bird
traits as well each with their own interface that is independent of Animal
. I add a docstring to each of these Animal, Fish, Bird
traits to document the interface they must satisfy.
Now I want to make a Habitat
interface, and within that I have Water
and Tree
traits which each include their own interfaces. Again, I document the interface in the trait itself.
I would like to generate a Documenter.jl
page which includes all of the Animal
interfaces, and another page which includes all of the Habitat
interfaces with @autodocs
. Currently, we can do this by manually including the docstrings for each trait or restricting trait implementations to particular files, but it would be helpful if I could filter the trait types according to the Julia type hierarchy to generate my interface documentation.
In the end, I'd like to generate an Animals
interface page via
@autodocs
Module = [Foo]
Filter = t -> typeof(t) === DataType && t <: Foo.AbstractAnimal
where AbstractAnimal <: Any
is an abstract type I create to make sure that {Animal, Fish, Bird} <: Abstract Animal
are all independent types in the type tree
Relevant link to the Documenter.jl
docs
From Janis Erdmanis via Slack:
The API is rather clear and seems I could find it useful for enforcing interface. However it also feels a bit verbose. Looking at the under the hood section I wonder wouldn't it be possible to have an API:
struct Lack{T} end
struct Has{T} end
abstract type FlyTrait <: Ability end
FlyTrait(x) = Lack{FlyTrait}
And then the methods would have a form:
tickle(::Has{FlyTrait}, ::Has{SwimTrait}, x) = "Flying high and diving deep"
Consider this example:
@trait Fly
@implement CanFly by liftoff()
abstract type Bird end
struct Duck <: Bird end
struct Chicken <: Bird end
@assign Bird with Fly
Since I assigned the abstract type Bird
with the Fly
trait and the Fly
trait requires an interface contract liftoff
, I expect both Duck
and Chicken
to satisfy that contract. However:
julia> check(Duck)
β
Duck has no interface contract requirements.
The interface check should traverse super-type tree and check all required contracts.
The current implementation associates the trait type <T>Trait
with a set of contracts. It seems that it makes more sense to associate the can-trait i.e. Can<T>
to the contracts instead.
If I define a module and then try to verify interfaces from outside of the module, it gets confused:
julia> module Foo
using BinaryTraits
using BinaryTraits.Prefix: Is
@trait Cute
@implement Is{Cute} by awesome(_)
struct Wat end
@assign Wat with Is{Cute}
end
julia> @check Foo.Wat
β
Main.Foo.Wat has no interface contract requirements.
The workaround is to run the check inside the module, or expose a method to do that:
julia> module Bar
using BinaryTraits
using BinaryTraits.Prefix: Is
@trait Cute
@implement Is{Cute} by awesome(_)
struct Wat end
@assign Wat with Is{Cute}
testme() = @check Wat
end
julia> Bar.testme()
β Warning: Missing implementation
β contract = BinaryTrait{Main.Bar.Cute}: BinaryTraits.Prefix.Positive{Main.Bar.Cute} β’ awesome(πΉ)
β @ BinaryTraits ~/.julia/dev/BinaryTraits/src/interface.jl:62
β Main.Bar.Wat is missing these implementations:
1. BinaryTrait{Main.Bar.Cute}: BinaryTraits.Prefix.Positive{Main.Bar.Cute} β’ awesome(πΉ) (Missing implementation)
I would like to apply a default trait to an abstract type, and then special case some subtypes with the negated trait. Take for example below:
abstract type Bar end
struct Foo <: Bar end
@trait A_Foo prefix Is,Not
@implement Is{A_Foo} by getfoo(_)
@assign Bar with Not{A_Foo}
@assign Foo with Is{A_Foo}
@traitfn getfoo(::Not{A_Foo}) = "Not a Foo!"
# This passes, but seems like it shouldn't
@check Foo
Foo
passes the check, but getfoo(Foo())
fails because I haven't implemented getfoo(::Foo)
yet in accordance with the Is{A_Foo}
interface. This behavior seems nuanced since it requires associating only one of Is{A_Foo}
or Not{A_Foo}
to each type at a time, so it may contradict BinaryTraits
's implementation.
Alternatively, if there's a better way/a design pattern to achieve this behavior (default trait applied to abstract type, special cased implementation of the negated trait to subtypes) I am open implementing that instead
First, it's kinda weird that we're storing client data in BinaryTraits module. Second, I just found another problem due to this design:
The global state is being re-initialized and the data gets wiped away. The case come up with I have a module X that uses BinaryTraits. In module X, I defined a trait. When using X
is finished, BinaryTraits.prefix_map
is empty again, where it should have a map for the trait that I defined in module X.
I think it would be cleaner to put this map in the client module's namespace - perhaps define a function that takes the trait symbol and map that to the prefixes.
For this reasons, I'm retracting PR #36
The argument requires to specify an argument type for each argument - could default to Any
Yes, but I am unsure if Any is the right default. When I tried to use it with the AbstractArray interface, which uses duck typing in the interface, I figured that I need to use Base.Bottom instead. See the LinearIndexing trait example. If we want to impose a default then I think Bottom is more appropriate because otherwise the implementer must define the function that accepts explicitly Any.
At several spots in the source code I found this pattern:
x = get!(dict, key, Constructor())
That will create an instance of Constructor
every time the line is executed (while needed only once for each key). It should be replaced by
x = get!(dict, key) do; Constructor() end
The type T
which is assigned a trait determines the argument type of the first argument of the method specified in the @implement macro. That is a restriction with respect to the methods which can be requested to be implemented.
Wouldn't it be a better option to allow to specify the position of this argument?
For example the methods in Base
, which need in iterator do not all expect the iterator as the first argument (map
, reduce
, get!
, .........)
It should be possible to define interfaces, which require more than one interface type.
Example with proposal for syntax:
@trait ProblemDescription prefix Is,IsNot
@trait Solve
@implement Tuple{IsProblemDescription,CanSolve} by solve(_2, _1, general_parameters)
A declarative, efficient, and flexible JavaScript library for building user interfaces.
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. πππ
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google β€οΈ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.