Skip to content

Instantly share code, notes, and snippets.

@sj-i
Last active September 30, 2022 05:34
Show Gist options
  • Save sj-i/7981487f879bd9aad8f57a931de1591e to your computer and use it in GitHub Desktop.
Save sj-i/7981487f879bd9aad8f57a931de1591e to your computer and use it in GitHub Desktop.
Should traits in PHP die or not?

I will introduce use cases for which traits can be useful in this post. It also mentions some weaknesses of traits, for helping considerations of alternatives.

This post was created as a response to a discussion I had when I submitted an RFC that would allow for constants in traits [1] and that it should not be improved because traits should die in the first place [2].

Use cases for traits

In my opinion, the following are possible valid use cases for traits, and actual use cases may be a combination of several of these.

  • Providing a default implementation of an interface
  • Achieving multiple inheritance
  • Splitting class implementations
  • Porting code from other languages that have similar mechanisms to trait
  • Sharing implementations of classes coincidentally having the same functionality

Ideally, it would be better to investigate how these use cases are actually being used through publicly available OSS, but let's postpone that for now and move on.

Trait is a variant of multiple inheritance that avoids the problems associated with single inheritance. On the other hand, the C++ folks who have real multiple inheritance had the mantra "composition over inheritance" as late as the 1990s [3].

At present and in the future, traits should be used for relatively minor use cases that are not easy to achieve with compositions.

As you probably know, Marco Pivetta has written an excellent article that helps to discourage the use of inheritance and encourage composition via final [4], and I recommend reading it if you have not read it.

Some of these use cases can also be addressed by the introduction of similar features, such as interfaces with default implementations or real multiple inheritance. I am pessimistic about such competing ideas surviving the vote in the PHP RFC process because of the very existence of traits in PHP right now.

However, if there is overwhelming agreement that traits should be abandoned while the need for language support for the use cases itself is agreed, then this will lead to a future where such alternatives are introduced.

I would be OK with such a future if it is promising, but if you do not have such a strong foresight, please support the modification of the existing trait to make it a little more manageable, as a backing up plan.

Providing a default implementation of an interface

Traits are often used to provide a default implementation of a specific interface. This may allow users to adopt a rich interface [5] in a user-friendly manner, or to extend the interface later without affecting the user code.

A common example is the combination of LoggerAwareInterface and LoggerAwareTrait in PSR-3, or the combination of LoggerInterface and LoggerTrait [6].

Achieving multiple inheritance

Traits were originally designed to allow for a sort of multiple inheritance while avoiding state conflicts.

PHP is a single inheritance language. Because of the multiple inheritance problems in C++ and other languages, including implementation complexity, state conflicts, and name collisions, some later languages such as Java and PHP tried to avoid these problems by limiting inheritance to a single parent.

And now, the single inheritance creates another problem. For the sake of simplicity, let us assume the worst-case design choice, which is to adopt only inheritance as a code reuse mechanism. Suppose that the common functionality among classes A, B, C, and D is packed into a parent class P. Conceptually A, B, C, and D are all also P. However, if a common function only between A and B, namely AB, or a common function only between C and D, namely CD, appears later, we have no choice but to push them all into P under the single inheritance. Worse, it may even be possible to want to use AB or CD functions in places that have absolutely no relation to P. Inheritance in a single inheritance language is a mechanism that tends to lead to bloated components unless it is used in a very restrictive manner.

Currently, PHP traits are implementations without types, and conversely, interfaces are types without implementations. Used together, they can be used as a convenient way to achieve multiple inheritance as long as they avoid the problems associated with name collisions. It avoids the bloat of components caused by single inheritance and allows for separation of concerns.

Again, composition is more convenient as a means of reusing code in most cases; the combination of traits and interfaces does not prevent the problem of implementation detail leakage that exists between classes in an inheritance hierarchy, and in fact is worse than inheritance in this respect, since visibility is not applied at all. Even with such tradeoffs in mind, there are still some cases where it is desirable to pack multiple functionalities into a single class in order to provide a rich interface that is easier rather than simpler for the class user, or to implement functions that are of cross-cutting concern and that require access to internal members of the class, such as some sort of a serializer. Multiple inheritance like mechanisms can be useful when implementing such classes.

Larry Garfield wrote an article on trait [7] that will also help you understand this kind of use case, as well as the previous story about trait providing a default implementation of an interface.

Splitting class implementations

Traits can also be used for simple splitting of classes.

It works in a very limited, but not so very niche, situation where different actors must compose implementations of different parts of a class, with different ways and timing of modification.

One example of a useful situation is where multiple pieces of machine-generated or handwritten code are combined to form a single class. For example, many ORMs require specific types to represent rows retrieved from the DB, and often have mechanisms to machine-generate those types from the DB schema. There are also systems that generate request and response classes from an IDL, and RPC codes that represents those types as function inputs and outputs. It is often wanted to be able to make minor manual modifications to such machine-generated code, such as adding members, etc. Trait partitioning makes it easier for such code generators to focus on generating the code they are interested in, without worrying too much about humans or other generators generating other parts of the codes.

Note that this only works in very limited situations. Since traits currently have no way to control visibility of members into their local, a class divided into many traits is in fact a huge class. Fortunately, PhpStorm can detect when a member in one trait is referenced in another trait used in the same composing class, but without such an intelligent editor, it is difficult to navigate which parts of a component can affect which parts.

When refactoring a large class, splitting by traits is not a bad option as a first step in breaking up the parts incrementally while keeping the code executable. But it should not stop there, and such refactoring should eventually move to compositions.

There is a question on stackoverflow about the proper use of C# partial classes [8], which are a similar feature to traits, and I think something similar can be said about PHP traits.

There are differences that C# partial classes can be completely "exteriorized", whereas PHP traits require explicit adaptation of traits from composing classes and do not allow constant definitions currently, etc., but they are very similar in their use.

Porting code from other languages that have similar mechanisms to trait

There are many mechanisms similar to PHP traits in other languages, such as the ones mentioned in the trait constants RFC as comparison to other languages. And there are many tasks that would be made easier if we could simply port assets from other languages.

Sharing implementations of classes coincidentally having the same functionality

Traits can also be used in very niche situations where you want to give the same functionality to multiple classes, but you don't want them to have the same type and be interchangeable.

One example, which I personally don't like, is Laravel's Macroable [9]. Anyway, there are rare situations where you want to provide a same interface for multiple classes to the programmers and code generators that use the code, but have no intent to make it polymorphically interchangeable at runtime for other components of the system.

Trait weaknesses

On the other hand, compared to classes, traits in their current form have the following weaknesses as a language feature for building components.

  • Niche nature of sharing implementation without types
  • Merging of member namespaces in composing class and lack of access control mechanism
  • Exotic usage rules

Even if one accepts that trait is effective in all or some of the use cases described above, some may want to consider alternatives because of these weaknesses. It may be helpful to clarify which of these weaknesses can be resolved by enhancing traits, and whether the benefits of resolving these weaknesses justify the costs associated with introducing a new language mechanism.

Niche nature of sharing implementation without types

If you look closely at the use cases listed above, you will notice that a trait should be used with a specific interface in many cases. The first two cases clearly assume a combination of trait and intreface, and even the simple splitting of a class often involves clearly separated roles for each part. And most mechanisms equivalent to traits in other languages can be treated as types, regardless of whether they are named traits or not.

It seems strange and inconvenient to me that traits are tied to a specific interface in the main use case, but the actual tying of trait and interface must always be done manually by the programmer on the composing class side. We can only make assumptions about what $this is in the trait, to the extent that what is declared as abstract methods would be usable.

Traits were first proposed in 2008 [10] and finally introduced in PHP 5.4, released in 2012. During this time, two ideas were discussed: one to allow traits to implement interfaces [11], and the other to allow requesting implementation of a specific interface from a trait for a composing class [12]. Neither of these remained in the form introduced in PHP 5.4, but it was a decision to hold off at the time of initial introduction rather than reject them because of their unsolvable problems, so it is worth considering their introduction again. In fact, Hack, a derivative of PHP, introduces both of these into their traits [13][14].

Some of these inconveniences can be avoided if real multiple inheritances or interfaces with default implementations that languages such as Java have are introduced.

Merging of member namespaces in composing class and lack of access control mechanism

The members of traits have a single namespace in the composing class by its flattening property. While the simple copy-and-paste semantics are easy to understand, when we think of trait as a mechanism for packaging members with names, we still want to distinguish between members that are used only inside the package and those that are not. The traditional private/protected/public visibility of a class is not available for trait, and therefore the risk of name collision is much greater. Although there is the compatibility checking for properties in traits, I think it is insufficient for properties, though not so for constants as proposed in the trait constants RFC.

As mentioned in the Future Scope of the trait constants RFC, this problem can be solved by introducing something like trait local visibility.

Since interfaces can usually only define public members, interfaces with default implementations cannot solve this problem. It may be possible to extend interfaces to define protected and private members, though this may be a bit controversial. With real multiple inheritance, we can use the traditional PPP visibility, but when it comes to conflicts, we still need to disambiguate it in some way.

Exotic usage rules

The problem I find most troublesome with trait is that it has exotic rules as a component reuse mechanism. Despite the fact that it can be used in a similar way to class inheritance, there are many cases where we are forced to adopt a completely different notation for the same purpose. A good example is the aforementioned problem where traditional PPP visibility simply cannot be applied. This is an almost inherent problem with traits, and will not be solved in the future, even if we patch the various problems by introducing trait locals, interface binding mechanisms, etc.

If we could eliminate traits completely and introduce real multiple inheritance or interfaces with default implementations, the language as a whole could probably achieve the almost same goal with simpler and fewer rules.

But as I mentioned earlier, I have a very pessimistic view of the solution to this problem.

  • Accept the odd looking and exotic rules and keep the trait-using code workable
  • Get a simpler, cleaner language specification in exchange for trait-using code that no longer works

If the choice is between the two, I just don't see the latter surviving the vote with twice as many votes as the former. Even if we keep traits and introduce another similar feature at the same time, we would still have to win a very hard discussion.

Conclusion

Trait is not a mechanism that should be used just because it is usable. However, there are some use cases, and efforts should be made both to promote the discourse that traits should not be used unnecessarily and to improve traits to make them safer to use.

Considering alternatives to traits is also a good effort, but the value of such alternatives is in reducing the overall complexity of the language as a whole. Since there is already a lot of code that uses the current traits, and since it is difficult to actually abolish them, it would be necessary to have both alternatives and the current traits. If there is not an overwhelming consensus that traits should be eliminated from the language or that they should not be used, there appears to be a high barrier to achieving it, as the complexity of the language would be rather increased by having both.

References

@sj-i
Copy link
Author

sj-i commented Jul 21, 2022

Thanks for your consideration!

I'm currently caught by multiple deadlines so the answer would require more days though...

@mikeschinkel
Copy link

👌

@mikeschinkel
Copy link

@sj-i — How are those those deadlines coming? 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment