-
-
Notifications
You must be signed in to change notification settings - Fork 740
Add SymbolType, PropertyType, and ToFunctionType to phobos.sys.traits. #10805
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
base: master
Are you sure you want to change the base?
Conversation
Thanks for your pull request, @jmdavis! Bugzilla referencesYour PR doesn't reference any Bugzilla issue. If your PR contains non-trivial changes, please reference a Bugzilla issue or create a manual changelog. Testing this PR locallyIf you don't have a local development environment setup, you can use Digger to test this PR: dub run digger -- build "master + phobos#10805" |
I don't like how large the diff is, but I can't split up the functions into separate PRs, because they use each other in their tests, and there are a lot of tests to make sure they work correctly even in the corner cases. |
See_Also: | ||
$(LREF SymbolType) | ||
+/ | ||
template ToFunctionType(T) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay so this is normalising the different function types into the symbol variation.
I would suggest a name like NormaliseFunctionType
.
Using the prefix To
here, suggests that it is coming from something that isn't a function to a function type.
When in fact its coming from a function type to a different function type that is predictable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, I guess we'll see what others say, but I don't see how it's any different from std.conv.to
in the sense that you can convert a type to itself.
ToFunctionType
says exactly what the trait is doing, and it's much shorter (and avoids issues with American vs British spelling). If anything,
The Phobos v2 version is FunctionTypeOf
, but it works on both symbols and types, which makes it more complicated (including having to deal with @property
), and IMHO, that name is not appropriate for a trait which is converting a type rather than getting the type of a symbol (and it's already confusing enough that it can be used for both in std.traits). ToFunctionType
was the most obvious name that I could think of, and personally, I don't see how NormalizeFunctionType
is any better.
SymbolType and PropertyType are completely new. They're both wrappers around typeof, and the idea is to work around the fact that typeof(foo) is syntactically ambiguous. Specifically, depending on the context, you want typeof(foo) to give you the type of foo as an expression, or you want typeof(foo) to give you the type of foo as a symbol. For non-functions, this is a non-issue, because the type of the symbol and the type of the expression when using the symbol on its own in an expression are the same. A variable of type int has the type of int, and if you use the variable in an expression - e.g. returning it from a function - then that expression also has the type of int. However, for functions, the type of the expression and the type of the symbol are not the same. int foo(); has the type of int(), but when it's used in expression - e.g. auto bar() { return foo; } - the type is int. If functions always had to be called with parentheses, this would not be an issue, because then typeof(foo) could not be a function call, and therefore it would have to be the type of the symbol, whereas typeof(foo()) would be the type of the expression. However, while typeof(foo()) is clearly an expression, because of optional parens, typeof(foo) could be either the type of the symbol or the expression. And @Property makes the situation even worse. The compiler _could_ of course give an error due to the ambiguity, but it doesn't (and that would cause its own set of problems anyway). Rather, if the function is not marked with @Property, then typeof(foo) gives the type of the symbol, whereas if it is marked with @Property, then typeof(foo) gives the type of the expression - i.e. the type that you get when calling the function (which results in an error if the function cannot be called with no arguments or returns void, since the expression is then invalid). The theory behind that behavior was that because an @Property function is emulating a member variable, typeof should treat it like a member variable so that the result would be the same whether the property was a variable or a function. As such, typeof gives the same type as it would have if it were a member variable, which means giving the type the expression has when the property is used on its own (and thus is called). However, in practice, this is a mess, because in practice, whether you want the type of the expression or the type of the symbol actually depends on what the code is doing, not on whether the function is supposed to act like a member variable or not. In addition, because we still have optional parens in spite of having @Property, it means that how a function is used actually has nothing to do with @Property. foo could be a variable, an @Property function, or a non-@Property function, and `return foo;` would work just fine so long as foo is not a function which requires arguments and so long as it has a return type. So, in a case where the code is going to use foo in an expression, you're probably going to want typeof(foo) to give you the type of foo as an expression regardless of whether it's a varable, an @Property function, a non-@Property function, or any other symbol with a type. However, if you're trying to get information on the symbol itself (e.g. because the code actually needs to know whether it's a function, or because it needs to get the function's attributes), then you want typeof(foo) to give the type of the symbol regardless of what that symbol is. But there is no way to tell typeof which behavior you want. Rather, its behavior differs depending on the type of the symbol and on whether it's been marked with @Property. This means that typeof is inherently error-prone. std.traits has had to work around that in a number of places, and who knows how much generic code exists in the wild which happens to work correctly with the symbols that it's been tested with but which would fail if given something else (e.g. if every function that a piece of templated code currently operates on is an @Property function, it could easily have assumed that typeof(foo) gave the type of the expression and thus would fail when given a non-@Property function, and it likely wouldn't be obvious to the programmer without testing). What this means is that ideally, the language would have something like typeof_expr and typeof_sym instead of typeof, but it's not like we're going to get rid of typeof at this stage, and adding variants of it like that would mean adding new keywords. So, that's probably never going to happen. So, to try to fix this problem for Phobos v3, we're adding the traits SymbolType and PropertyType. SymbolType will give the type of the actual symbol, so it will be used in contexts where the introspection is beind done on the symbol itself. PropertyType on the other hand will give the type of the symbol as an expression. It's PropertyType rather than ExpressionType, because it does not work on general expressions (because template alias parameters only accept symbols), and the only situation where a function is a valid expression on its own is if it can be used as a getter property (i.e. it can be called with no arguments and returns a value). So, in effect, PropertyType treats all functions as if they were marked with @Property. The ones that can be used as getter properties will then give their return types, and the others won't compile with PropertyType (just like typeof won't compile with setter @Property functions, because they're not valid expressions on their own). So, SymbolType will then be used in cases where the actual type of the symbol is needed, and PropertyType will be used in cases wher a symbol is going to be used as a getter property, and the code needs to know what type it will have in an expression without caring whether it's a variable, an enum, a function, or whatever. Having these two traits will make it possible for other traits to not have to work around @Property like a number of them currently do in std.traits. Rather, the programmer will indicate which they need in a given situation by choosing SymbolType or PropertyType and pass that result to whatever other trait or is expression which tells them what they're trying to find out. So, we'll simultaneously be putting the choice in the hands of the programmer and simplifying the other traits. Furthermore, the plan here is that the function-related traits in phobos.sys.traits will operate solely on types (except in situations where the actual symbol is required - e.g. to get the names of parameters), like most traits typically do. std.traits has a number of its function-related traits operate on symbols in part to work around the issue with @Property functions (as well as stuff like trying to treat variables with opCall as functions instead of requiring that the symbol for opCall itself be passed). And that not only makes the traits more complicated, but in general, having traits which operate on both types and symbols which aren't types makes the code error-prone and harder to understand, since it's harder to see what the code is actually operating on (and can have unintended consequences whenever an AliasSeq mixes types and other symbols). Having SymbolType and PropertyType will have the benefit of reducing the number of cases where traits operate on both types and other symbols. It should also help with clarity when a trait doesn't try to handle everything itself. The programmer will therefore have full control over the behavior that they get, because they can use typeof, SymbolType, or PropertyType depending on what they're trying to do and what information they want to get about the symbol or expression from other traits. Exactly how this will affect each trait will of course depend on the trait in question, but the idea is to simplify things and ultimately end up with traits which are easier to understand and less error-prone. And those traits will require less "magic" and special-casing, because the programmer will have already dealt with whether they want the type of the symbol or the type of the expression when getting the type to instantiate the trait with. The other new symbol, ToFunctionType, is a template which converts function types, function pointer types, and delegate types to the corresponding function type. So, something like `int function(string)` or `int delegate(string)` would become `int(string)`. This is primarily useful in implementing other function-related traits, but it also provides a way to get function types to use in is expressions for testing or providing examples (since it's not possible to write out a function type in D outside of an actual function declaraton / definition). In terms of functionality, ToFunctionType is a replacement for std.traits.FunctionTypeOf. FunctionTypeOf attempts to operate on anything that's "callable" (both types and symbols), which makes it a bit of a mess (and which actually cannot work in some cases due to templated types or templated functions not having been instantiated). And looking over where it's used in std.traits, it's usually used on types anyway. So, ToFunctionType operates exclusively on types. The change in name is because FunctionTypeOf definitely sounds like a trait that's operating on a symbol to get its type (even if it also accepts types), whereas ToFunctionType sounds much more like it's converting the given type, which is what it's doing. Also, because some tests needed some module-level functions, this adds the PhobosUnittest version identifier to the build. Phobos v2 has StdUnittest, but it seemed kind of silly to use that, since this isn't std, but we can change it later if we want to. And actually, on that note, some of the phobos.sys documentation currently uses StdDdoc when it should probably use something like PhobosDdoc, but the documentation build still needs to be sorted out anyway.
Bikeshed: I would use the name
|
Well, the docs explain why it isn't called that:
Since the only kind of function which can be a valid expression on its own is one which can be used syntactically as a getter property, and the main use of I've rewritten the documentation dozens of times at this point in an effort to make it clear, and while working on this, it became pretty clear to me that there was going to be some confusion about when to use the trait, so part of the point of the current name is to try to make it clearer when it should be used. And maybe the documentation needs yet another pass, but nothing I've come up with for the short description at the top has been particularly satisfying. You pretty much have to read the full explanation to understand it. I added a TLDR at the top in the hopes that that would help, but the problem that I'd also like to avoid complaints about it not working on expressions in general (which it couldn't do even if we wanted it to, because But I went back and forth quite a bit on what to name these - particularly with |
This suggests to me that the names should be |
Well, that's basically what they are. I was originally calling them Flipping the names around helped make it easier to tell them apart, and it reduced their length somewhat, since having So, if the consensus is on something like Personally, I'd never use |
SymbolType and PropertyType are completely new. They're both wrappers around typeof, and the idea is to work around the fact that typeof(foo) is syntactically ambiguous. Specifically, depending on the context, you want typeof(foo) to give you the type of foo as an expression, or you want typeof(foo) to give you the type of foo as a symbol.
For non-functions, this is a non-issue, because the type of the symbol and the type of the expression when using the symbol on its own in an expression are the same. A variable of type int has the type of int, and if you use the variable in an expression - e.g. returning it from a function - then that expression also has the type of int.
However, for functions, the type of the expression and the type of the symbol are not the same.
has the type of int(), but when it's used in expression - e.g. auto bar() { return foo; } - the type is int.
If functions always had to be called with parentheses, this would not be an issue, because then typeof(foo) could not be a function call, and therefore it would have to be the type of the symbol, whereas typeof(foo()) would be the type of the expression. However, while typeof(foo()) is clearly an expression, because of optional parens, typeof(foo) could be either the type of the symbol or the expression. And @Property makes the situation even worse.
The compiler could of course give an error due to the ambiguity, but it doesn't (and that would cause its own set of problems anyway). Rather, if the function is not marked with @Property, then typeof(foo) gives the type of the symbol, whereas if it is marked with @Property, then typeof(foo) gives the type of the expression - i.e. the type that you get when calling the function (which results in an error if the function cannot be called with no arguments or returns void, since the expression is then invalid). The theory behind that behavior was that because an @Property function is emulating a member variable, typeof should treat it like a member variable so that the result would be the same whether the property was a variable or a function. As such, typeof gives the same type as it would have if it were a member variable, which means giving the type the expression has when the property is used on its own (and thus is called).
However, in practice, this is a mess, because in practice, whether you want the type of the expression or the type of the symbol actually depends on what the code is doing, not on whether the function is supposed to act like a member variable or not. In addition, because we still have optional parens in spite of having @Property, it means that how a function is used actually has nothing to do with @Property. foo could be a variable, an @Property function, or a non-@Property function, and
return foo;
would work just fine so long as foo is not a function which requires arguments and so long as it has a return type.So, in a case where the code is going to use foo in an expression, you're probably going to want typeof(foo) to give you the type of foo as an expression regardless of whether it's a varable, an @Property function, a non-@Property function, or any other symbol with a type. However, if you're trying to get information on the symbol itself (e.g. because the code actually needs to know whether it's a function, or because it needs to get the function's attributes), then you want typeof(foo) to give the type of the symbol regardless of what that symbol is. But there is no way to tell typeof which behavior you want. Rather, its behavior differs depending on the type of the symbol and on whether it's been marked with @Property. This means that typeof is inherently error-prone.
std.traits has had to work around that in a number of places, and who knows how much generic code exists in the wild which happens to work correctly with the symbols that it's been tested with but which would fail if given something else (e.g. if every function that a piece of templated code currently operates on is an @Property function, it could easily have assumed that typeof(foo) gave the type of the expression and thus would fail when given a non-@Property function, and it likely wouldn't be obvious to the programmer without testing).
What this means is that ideally, the language would have something like typeof_expr and typeof_sym instead of typeof, but it's not like we're going to get rid of typeof at this stage, and adding variants of it like that would mean adding new keywords. So, that's probably never going to happen.
So, to try to fix this problem for Phobos v3, we're adding the traits SymbolType and PropertyType.
SymbolType will give the type of the actual symbol, so it will be used in contexts where the introspection is beind done on the symbol itself.
PropertyType on the other hand will give the type of the symbol as an expression. It's PropertyType rather than ExpressionType, because it does not work on general expressions (because template alias parameters only accept symbols), and the only situation where a function is a valid expression on its own is if it can be used as a getter property (i.e. it can be called with no arguments and returns a value). So, in effect, PropertyType treats all functions as if they were marked with @Property. The ones that can be used as getter properties will then give their return types, and the others won't compile with PropertyType (just like typeof won't compile with setter @Property functions, because they're not valid expressions on their own).
So, SymbolType will then be used in cases where the actual type of the symbol is needed, and PropertyType will be used in cases wher a symbol is going to be used as a getter property, and the code needs to know what type it will have in an expression without caring whether it's a variable, an enum, a function, or whatever.
Having these two traits will make it possible for other traits to not have to work around @Property like a number of them currently do in std.traits. Rather, the programmer will indicate which they need in a given situation by choosing SymbolType or PropertyType and pass that result to whatever other trait or is expression which tells them what they're trying to find out. So, we'll simultaneously be putting the choice in the hands of the programmer and simplifying the other traits.
Furthermore, the plan here is that the function-related traits in phobos.sys.traits will operate solely on types (except in situations where the actual symbol is required - e.g. to get the names of parameters), like most traits typically do. std.traits has a number of its function-related traits operate on symbols in part to work around the issue with @Property functions (as well as stuff like trying to treat variables with opCall as functions instead of requiring that the symbol for opCall itself be passed). And that not only makes the traits more complicated, but in general, having traits which operate on both types and symbols which aren't types makes the code error-prone and harder to understand, since it's harder to see what the code is actually operating on (and can have unintended consequences whenever an AliasSeq mixes types and other symbols).
Having SymbolType and PropertyType will have the benefit of reducing the number of cases where traits operate on both types and other symbols. It should also help with clarity when a trait doesn't try to handle everything itself. The programmer will therefore have full control over the behavior that they get, because they can use typeof, SymbolType, or PropertyType depending on what they're trying to do and what information they want to get about the symbol or expression from other traits.
Exactly how this will affect each trait will of course depend on the trait in question, but the idea is to simplify things and ultimately end up with traits which are easier to understand and less error-prone. And those traits will require less "magic" and special-casing, because the programmer will have already dealt with whether they want the type of the symbol or the type of the expression when getting the type to instantiate the trait with.
The other new symbol, ToFunctionType, is a template which converts function types, function pointer types, and delegate types to the corresponding function type. So, something like
int function(string)
orint delegate(string)
would becomeint(string)
. This is primarily useful in implementing other function-related traits, but it also provides a way to get function types to use in is expressions for testing or providing examples (since it's not possible to write out a function type in D outside of an actual function declaraton / definition).In terms of functionality, ToFunctionType is a replacement for std.traits.FunctionTypeOf. FunctionTypeOf attempts to operate on anything that's "callable" (both types and symbols), which makes it a bit of a mess (and which actually cannot work in some cases due to templated types or templated functions not having been instantiated). And looking over where it's used in std.traits, it's usually used on types anyway.
So, ToFunctionType operates exclusively on types. The change in name is because FunctionTypeOf definitely sounds like a trait that's operating on a symbol to get its type (even if it also accepts types), whereas ToFunctionType sounds much more like it's converting the given type, which is what it's doing.
Also, because some tests needed some module-level functions, this adds the PhobosUnittest version identifier to the build. Phobos v2 has StdUnittest, but it seemed kind of silly to use that, since this isn't std, but we can change it later if we want to. And actually, on that note, some of the phobos.sys documentation currently uses StdDdoc when it should probably use something like PhobosDdoc, but the documentation build still needs to be sorted out anyway.