Skip to content

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

jmdavis
Copy link
Member

@jmdavis jmdavis commented Jun 24, 2025

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.

@jmdavis jmdavis added the Phobos 3 The PR/issue is for Phobos V3. label Jun 24, 2025
@dlang-bot
Copy link
Contributor

Thanks for your pull request, @jmdavis!

Bugzilla references

Your 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 locally

If you don't have a local development environment setup, you can use Digger to test this PR:

dub run digger -- build "master + phobos#10805"

@jmdavis
Copy link
Member Author

jmdavis commented Jun 24, 2025

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)
Copy link
Contributor

@rikkimax rikkimax Jun 24, 2025

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.

Copy link
Member Author

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.
@pbackus
Copy link
Contributor

pbackus commented Jun 24, 2025

Bikeshed: I would use the name ExpressionType instead of PropertyType, to match the way it's described in the docs.

PropertyType evaluates to the type of the symbol as an expression (i.e. the
type of the expression when the symbol is used in an expression by itself),
and SymbolType evaluates to the type of the given symbol as a symbol (i.e.
the type of the symbol itself).

@jmdavis
Copy link
Member Author

jmdavis commented Jun 24, 2025

Bikeshed: I would use the name ExpressionType instead of PropertyType, to match the way it's described in the docs.

Well, the docs explain why it isn't called that:

TLDR, PropertyType should be used when code is going to use a symbol as if
it were a getter property (without caring whether the symbol is a variable
or a function), and the code needs to know the type of that property and
$(I not) the type of the symbol itself (since with functions, they're not
the same thing). So, it's treating the symbol as an expression on its own
and giving the type of that expression rather than giving the type of the
symbol itself. And it's named PropertyType rather than something like
$(D ExpressionType), because it's used in situations where the code is
treating the symbol as a getter property, and it does not operate on
expressions in general. SymbolType should then be used in situations where
the code needs to know the type of the symbol itself.

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 PropertyType is to get the type of a symbol which is being used as a property, PropertyType fits. But the explanation has to talk about expressions, because that's what's going on at a technical level, and it's what's causing the ambiguity that makes typeof insufficient on its own.

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 PropertyType and SymbolType are supposed to solve requires a fairly detailed explanation.

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 alias parameters only accept symbols, not general expressions), and ExpressionType arguably implies that it does (though not as much as TypeOfExpr, which is what I had originally).

But I went back and forth quite a bit on what to name these - particularly with PropertyType.

@atilaneves
Copy link
Contributor

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.

This suggests to me that the names should be TypeOfAsExpression and TypeOfAsSymbol.

@jmdavis
Copy link
Member Author

jmdavis commented Jun 25, 2025

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.

This suggests to me that the names should be TypeOfAsExpression and TypeOfAsSymbol.

Well, that's basically what they are. I was originally calling them TypeOfExpr and TypeOfSym, but aside from Adam's complaints about abbreviating things, when writing all of the tests, I found that they were surprisingly hard to tell apart, because they started the same. Spelling the names out entirely would help with that, so TypeOfAsExpression and TypeOfAsSymbol would have less of that problem, but they're also quite long, and these symbols are going to be used heavily in template constraints which already are often long and complicated, and having long names in there is going to make them that much harder to read (at least it certainly will for me). So, I'm not a fan of making these symbols particularly long.

Flipping the names around helped make it easier to tell them apart, and it reduced their length somewhat, since having Of on the end didn't seem necessary or appropriate. And I ran into the issue of trying to make it clear when exactly the expression version should be used, and since it only works with functions which return a value and don't take any arguments - so function's which can be used as getter properties - and it's needed in situations where you're going to use a symbol as a getter property without caring whether it's a variable or function (or whether it has @property), PropertyType seemed appropriate. But I'm not a huge fan of any of the names really, and no matter how this goes, there's probably going to be a frustrating amount of confusion around these given how subtle the issue is.

So, if the consensus is on something like TypeOfAsExpression and TypeOfAsSymbol (or you and/or Adam want to pull rank on this), then I can use those instead. But I really don't like how long they are, particularly for symbols that are going to replace typeof in template constraints in many situation.

Personally, I'd never use Expression in a symbol name (except maybe Expression by itself) because it's quite long, and Expr is a very common abbreviation for it - in which case we could go with TypeOfAsExpr and TypeOfAsSymbol, but Adam doesn't like the fact that one is then abbreviated an the other isn't (personally, while I favor consistency in general, I'm really not a fan of long symbol names, and I'd take shortening the name to something closer to reasonable with something that's going to be used as heavily as these are likely to be).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Phobos 3 The PR/issue is for Phobos V3.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants