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 16, 2022

trait private

This is exactly one of the forms of trait local I was thinking of.
I think a deeper consideration of semantics is needed to determine if this is really the right syntax, though.

@mikeschinkel
Copy link

mikeschinkel commented Jul 17, 2022

I think a deeper consideration of semantics is needed

Well let's get after it then, shall we?

I'll start.

Scoping

Given that Traits are not types it would seem acceptable for Trait-private properties to potentially have semantics that differ from properties for objects as objects have types.

The more I analyze it, the more it seems to be that Trait-specific properties should be lexically-scoped vs. dynamically scoped.

In other words they should only be visible within the text file where the Trait is defined, and nowhere else.

Lexically-Scoped Local Properties

This would lead me to question my original suggestion of using trait private $var.

Trait-specific variables will almost by-necessity have different semantics. Using the keyword private could be confusing because it could mean significantly different things if prefixed with trait.

Better to have trait local $var;, or just trait $var; as the latter would be unambiguous.

Modifier Keyword for Properties, Methods and Constants

For other declarations within a Trait the keyword trait could be a modifier, and these would also be lexically scoped; e.g.:

trait OnlyLocals {
	trait constant $alpha = 123; 
	trait $beta;
	trait static $delta;
	trait function gamma(){}
	trait static function epsilon(){}
}

__construct() and __destruct()

There would need to trait-specific __construct() methods to allow setting Trait-local properties, and the constructor would either be called manually within a Class constructor, or automatically if not already called in the Class constructor:

trait function __construct() {
	OnlyLocals::__construct();
}

If not manually called, it would be called after the Class constructor but no guarantee of order with respect to constructors for other Traits.

There would be no difference in syntax for regular Class constructors and Trait-specific constructors with the exception of the trait keyword on the function declaration.

__destruct()

A Trait-specific __destruct() would effectively mirror the semantics of the Trait-specific __construct().

Other Magic Methods

The other magic methods such as __call(), __callStatic(), __clone(), __get(), __set(), __isset(), __unset(), __sleep(), __wakeup(), __serialize(), __unserialize(), __toString(), __invoke(), __set_state(), and __debugInfo() would all be callable with the following syntax, but it would be the responsibility of the useing Class to invoke them manually:

OnlyLocals::<magicMethod>()

Needed: __compare($o1,$o2): bool

Given that == compares two object's attributes and values, then when == is used to compare two objects of the same Class that use a Trait with a __compare() method, PHP would need to call that method in order to evaluate ==, e.g.:

trait OnlyLocals {
	trait $beta;
	trait function __compare($o1,$o2):bool {
		return $o1->beta == $o2->beta;
	}
}

__compare() for all objects

All classes could potentially benefit from a __compare() method vs. just Traits, but unless that implementation were trivial it should be out of scope for a Trait-local RFC.

Reflection

An initial RFC should not need to include reflection for these properties, methods, or constants, and it is possible that no RFC would ever need to as the proposed Trait-locals would all be lexically-scoped.

But nothing would keep from adding reflection in a future RFC is it was found to be needed.

Dynamic Access

Similarly Trait-specific Properties and Methods would not (need to) be dynamically accessible, i.e. this would throw an error saying there is no such Property:

trait OnlyLocals {
	trait $beta;
	trait function gamma(){
		$name="beta";
		echo $this->${name};
	}
}

FUTURE Dynamic Access (maybe...)

If we discover there is need then we could add it in a future RFC.

Proposed Functions

However, given how Trait-locals would be lexically-scoped it would be appropriate to have Trait-specific standard library functions for invoking and access. Here are some proposed functions where TraitLocal would be a class similar to ReflectionProperty and ReflectionMethod but specific to Trait locals and that could encompass Property, Method and Constant.

trait_properties():TraitLocal[]
trait_property_values():array
trait_methods():TraitLocal[]           
trait_constants():TraitLocal[]
trait_constant_values():array
trait_method(object $inst,string $func_name):TraitLocal
trait_call(object $inst,string $func_name,mixed $args...):mixed|void
trait_call_static(string $func_name,mixed $args...):mixed|void
trait_property(object $inst,string$func_name[,mixed $value]):TraitLocal
trait_property_value(object $inst,string$func_name[,mixed $value]):mixed
trait_constant(object $instance,string $func_name):TraitLocal
trait_constant_value(object $instance,string $func_name):method

If we find they would be needed a future RFC could add them.

Duplicate Names

A Trait can have a Trait-specific Method, Property or Constant whose name matches a name in the using Class because it would not conflict.

It would, however, keep a developer from calling or accessing that same name on the Class. Considerations:

  1. If a Trait-specific name conflicts with the same name in a Class then the developer needs to just rename the Trait-specific name, and
  2. Referencing Class Properties, Methods and Constants from within a Trait is bad practice and should be avoided anyway.

Caveat for Dynamic Access

However, since Trait-specific variables would be lexically scoped then dynamic access or access with Reflection should work without issue even when there as same-named Properties, Methods and/or Constants that are Trait-specific.

Closures

Trait-specific properties, methods and constants should work in closures, array functions, etc. just like any other Property, Method or Constant would work when referred to by name, i.e:

trait AddIn {
	trait $internalState;
	function getClosure() {
		return function () {
    		return $this->internalState;
		};
	}
}

Object Iteration

Given that Trait-specific Properties would be lexically-scoped and not dynamic they would not participate in Simple Object Iteration. If this was needed then we should look to the section on "FUTURE Dynamic Access"

Final Keyword

The final keyword would have no meaning if applies to Trait-specific Properties and Methods as by nature there is no concept of "Child Traits." IOW, having both a final and a trait modifier should throw an error.

Conclusion

While I am sure I probably missed something I feel this is a good first-pass at the semantics for Traits that would allow Traits to be used in a much more robust manner that currently possible.

Your turn?

Feel free to take anything I wrote and remix it as you feel appropriate.

@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