[ANN] BinaryTraits.jl - a new traits package

Hi everyone,

I would like to announce BinaryTraits.jl, a new traits package that focuses on usability and interface specification. You can find documentation from the project repo as usual. In addition, I have provided some examples for the standard Iteration/Indexing and AbstractArray interfaces in the examples folder.

Why another package?

Julia’s traits story has been slowly developing over the past several years. There is nothing official in the language but several people attempted to experiment new ideas about how to make that work. Meanwhile, the Holy Traits pattern is mostly used given that it requires no additional package dependency.

However, I am not personally not satisfied because implementing Holy Traits requires quite a bit of code. SimpleTraits.jl does look very nice but I’m quite intimidated by its syntax/design. So, I end up developing a package that I think it should be easy to use and provide additional functionalities such as formally specifying interface contracts and validating those contracts.

What can it do?

  • Define traits and assigning them to your own data types
  • Define composite traits that exhibits all of the underlying traits
  • Define interface contracts for a trait
  • Check if your data type fully implements all interface contracts

Try it out! :slight_smile:

This package is still in its early stage. I have been messing with the API several times in the past week. I would be pleased to receive any feedback and ideas for improving this package. You can either reply to this post or just submit an issue to my github repo.

The package is being registered at the moment. It will take 3 days due to the mandatory waiting period. But, I am so excited that I am sharing with you about the news now!

-Tom

:tada: :tada: :tada: Update 2020-04-20 (version 0.2.0)

I just tagged a new version after making some significant code changes and taking several very generous PRs from @klacru :slight_smile: Most improvements relate to the interface contracts specification and verification:

  1. Interface contracts can now be specified with keyword arguments & complex argument types. Duck typed arguments are supported as well.

  2. Interface contracts are now propagated to sub-traits for composite traits. Let’s say an abstract type Bird has CanFly trait, which requires a fly method per interface contract. Then all subtypes of Birds are required to implement the same contract.

P.S. More ideas and upcoming enhancements are logged in GitHub.

:tada: :tada: :tada: Update 2020-05-04 (version 0.3.0)

We had some breaking changes but they’re worth 100%.

  1. The @implement macro now accepts an underscore argument, which indicates where the object argument should be passed for that function.

  2. Traits defined from one module can now be referenced/used from another module. This would be useful for framework providers to define traits that implementations should follow.

See updated documentation for details.

:tada: :tada: :tada: Update 2020-05-17 (version 0.4.0)

This release implements a new parametric type design to represent traits. The reason for the change is that the previous design feels too “magical” in the sense that custom types are defined with specific prefixes/suffixes. The new design feels more natural and Julian.

25 Likes

I really enjoyed the “tickle” example

1 Like

That looks really neat. I have a couple questions about the API for the interface contracts. It seems like with this design they aren’t really enforced contracts. If I understand correctly you can assign a trait to a type that doesn’t fully implement the associated interface, and the only way to tell whether they actually do implement it is with the check function. Have you considered checking whether the interface is implemented when you assign the trait, and rejecting the assignment if it isn’t?

My other question is about how you add required functions to the interface. First the trait is created and then separately functions are added to it. This means that in principle anyone can add functions to that interface at any point. I would think that in practice you would probably want to define the whole interface and then anyone wanting to extend it should probably use your composite traits to create a new extended interface. Do you think it would be possible or desirable to force the interface to be defined fully in one place along with the creation of the trait?

Great questions!

Julia, being a dynamic language, would be impossible to truly enforce anything statically. My thought is that you can put call the check function in your module’s __init__ function so everything is validated before you start using the package.

Doing the check when you @assign a type sounds like an interesting idea although it would require you to define all functions before assigning traits. It might be a little unnatural as you would have to order your code in a certain way.

Yes, I would image the the interface designer would do the following:

  1. Define the trait using @trait macro
  2. Define the generic functions as related to the interface (no need to have any method body)
  3. Specify the requirements using @implement macro

Then, as a user implementing the interface, I would do the following:

  1. Define my data type
  2. Extend the required functions
  3. Perform a check to make sure that I’ve done everything properly

Does that make sense?

Yeah that makes sense. I was suggesting something more like the following:

Interface designer:

  1. Define the generic functions related to the interface
  2. Use a single macro to define the trait and specify all of the functions of the interface

Implementer:

  1. Define the type
  2. Extend the functions
  3. Assign the trait (and get an error if all of the functions aren’t extended)

The advantages I see to this would be that the API no longer gives a way to add functions to the interface from anywhere in the code other than where the interface is defined, which I think is not something you would really want people to do. Additionally you don’t have to manually check whether you implemented everything properly, because the API just throws an error when you go to apply the trait if you didn’t.

Obviously you can’t actually stop anyone from messing with the internals and applying traits to things that don’t meet the interface requirements or adding more functions to the interface, but the API could not provide a convenient way to do so.

2 Likes

That’s a really neat idea. Thank you. It’s going to my “try that out next” queue :slight_smile:

Can someone give a basic definition and a base use case for what traits are? I have tried to understand SimpleTraits, but didn’t really understand why such a package would be needed? I feel like most of the examples can be done with “regular” Julia.

1 Like

A trait represents certain characteristics of an object.

For example, if you have implemented a container that supports indexing e.g. myobject[3] for accessing the 3rd element in your container, then any code that uses your object can extract an element using the indexing interface.

But, how do you know the object supports the indexing interface? So it would be nice to “attach” the type of your object to a trait type. Further, it would be nice to also associate the trait type with required interface functions. BinaryTraits.jl solves the problem by giving you such capability. You can review an iteration/indexing example that is borrowed directly from the Julia manual.

Using Holy Traits, you are being explicit about what your code is doing. Rather than assuming that the object supports indexing interface, you first check if the object exhibits the indexing trait and if so you can dispatch to the function that uses the indexing function.

Hope it helps!

1 Like

Excellent work! I tried it out immediately. would you mind if I entered some suggestions as issues in the repository?
I found some missing features in the implement macro I would like to add:

  1. the argument requires to specify an argument type for each argument - could default to Any
  2. the specified argument type needs to be simple, for example no type parameter allowed
  3. keyword arguments are not supported
1 Like

Hi Tom, first I would like to thank you for creating such a nice package.

The Idea of using traits on abstract types somehow reminds me of interface classes in Java. However with Julias “limitation” of single inheritance, I was wondering whether inheriting traits from multiple abstract types would be possible.

Do you know how you could achieve this?

1 Like

Hi @klacru,

Not at all. Please do enter them as separate issues so we can further discuss and work on them independently. In fact, PR’s are welcome as well.

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.

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.

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?

I will transfer my points and your replies into issues - see you there.

Hi @roble,

In Java, interface was originally designed to overcome the lack of multiple inheritance.
Similarly, BinaryTraits supports the notion of composite traits which roughly gives you the same thing as multiple inheritance (with the exception of not being able to inherit memory layout – a conscious design decision for abstract types in the Julia language).

Let’s take the multiple interface example from freeCodeCamp. We can implement the same thing here as follows:

@trait GPS
@implement GPS by get_coordinates()

@trait Radio
@implement Radio by start_radio()
@implement Radio by stop_radio()

struct SmartPhone end
get_coordinates(phone::SmartPhone) = ...
start_radio(phone::SmartPhone) = ...
stop_radio(phone::SmartPhone) = ...
1 Like

How much of this resolves at compile time?

So traits are really for developers rather than end users… For example, for scientists who use Julia for exploratory analysis really have no need for this right?

Developers of packages, however, may find it useful to see if they’ve implemented the interface correctly. So even developers are “end users” of this package. Using your example of Squares. If I was implemented Squares, I’d basically have to run the check() function to see if I’ve implemented the interface correctly. Ultimately, it would have to be Julia developers to implement traits on the actual Iterators interface (so that I, as a package developer, can use the check function)

Does my interpretation make sense?

@JeffreySarnoff I would expect everything to be resolved at compile time. They are just convenient macros to implement Holy Traits.

2 Likes

That sounds right. I would guess that most end users care more about getting things done than coming up with the best design for their code. There are more advanced users though. I would think that people who are proficient in designing data types and integrating with other packages will be the primary users of traits.

If packages expose their use via traits, then even the users of packages must be aware of how to use traits. Isn’t that right?

That’s true if a package expects users to be the implementer of certain interfaces then end users would that have do that. It should be easy to implement traits - most likely just reading documentation to find out how to satisfy interface requirements by implementing certain functions.

Just FYI - I just tagged a new release 0.3.0. See the first post above for a quick summary or just hit our BinaryTraits.jl project page.

2 Likes