-
Notifications
You must be signed in to change notification settings - Fork 6k
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
Extend "using for" #9211
Comments
Restrict proposal to:
|
I wanted to address one point from today's call: while doing This is very similar to imports in Python where importing every name separately is in fact the recommended style. So this: from math import abs, sin, ceil; is recommended over this: from math import *; Wildcard imports are actually frowned upon. With fine-grained imports you don't pollute the namespace with names you're not going to use (which helps avoid collisions) and they make it very straightforward to trace the source of every name in the file without having to refer to any other files. It seems like something that would be helpful when auditing. It's also something I loved when I switched to Python from Ruby which does not have these fine-grained imports and it can be very hard to find the definition of something. I'm missing it a bit in C++ too. Solidity is not Python, the circumstances are a bit different (contracts are not full applications and are therefore short; also new definitions can't be added dynamically at runtime) and the scale is smaller (it's not about all imports, just bound functions) but I wanted to add another perspective on this. For some people (including me) importing things one by one may actually be the more natural way to use this feature. |
@cameel this is actually a valid point - I also very much like it if you can see where each item was defined or imported. Since
|
Wouldn't Braces are good too. They would allow doing this, which some people might like when the list gets long and needs to get wrapped: using {
math.add,
math.sub,
math.mul,
convert.toString,
convert.toBytes16,
} for uint[]; |
Alternatively, this would work too: using math.{add,sub,mul}, convert.{toString,toBytes16} for uint[]; This is similar to braces work with shell expansion in Bash so it might seem familiar to some people. |
Yeah, I'm for having that form too. Even in Python both ways to import are available and preference for one or the other is just a convention, not something forced upon you by the language. |
It would be nice to somehow also "import" the effects of using. This could be especially useful for #11531 Proposal: If the definition of |
Especially with #11531 I think it would be useful to map operators to user-defined functions. This could be especially useful to define fixed point (or even floating point) types and make computations with them readable. It also removes the problem of how to apply compiler-supplied operators to user-defined value types (because the compiler-supplied operator is actually on a different type). Proposal:
So instead of a function or module, you can use For now, custom operators should not resolve inside unchecked blocks. |
Question brought up by @hrkrshnn : How to handle mixed-type operators for user-defined operators: Apart from exceptions like
The operator is resolved as follows: In |
As an alternative: Lookup fails if both |
Would it be allowed to modify storage? I hope not. Side-effects in operators do not sound like a good idea. I'd require the user to have a function rather than an operator if he really wants that. Even letting them be |
I'm not sure there is a good way to restrict access for functions implementing operators. Operators on storage structs sound useful, so if we want to restrict something else, we would have to look at the body of a function which I think is not a good idea. On the other hand this raises another question: If you want to implement an operator for reference types, you need several different functions for different storage locations. This would contradict the requirement above ("no overloads") - but I guess we can relax that to "no overloads, except for differences in storage locations". In order to make this work well, we also need #1256 |
Yet another alternative - which is way more permissive and maybe also achieves the same goals and is more consistent with how There can be only one For each
We can ignore "built-in" operators because |
Fixed point math example: https://gist.github.com/chriseth/cd6d825df5a13055b1c5d7bcf1e614a7 |
I'm not sure about allowing mixed types. Consider The problem is that the current syntax hides away all these complexities. For example, the operations may be non-commutative. Or how associativity of operators isn't well specified, which can affect the final value or introduce ambiguity during parsing. A second problem with the using syntax is that it doesn't allow easy extension into unchecked block. I think some form of traits, without mixed types, may be better. Here's a rough syntax:
This still doesn't make any guarantees about commutativity of Another approach is to allow defining global infix operators similar to free functions (and disallow redefining existing operators). type UFixed18 is uint256;
infix function <*>(UFixed18 a, uint b) returns (uint) { ... }
uint constant a = UFixed18.wrap(25) <*> 50; Maybe a stricter definition that allows specifying the precedence as a number, as well as associativity to resolve parsing ambiguity. Something similar to what Haskell already does. |
Mixed types are also useful for things like |
Scalar vector space multiplication should get its own operator :-). |
I wager this is the only other signatures you'd ever want, i.e. monoidal multiplication |
If you also consider the inner product, I think we have now collected at least one example for all the possible combinations of two types ( Given that, my crystal ball clearly says we should just allow all possible signatures for all operators. |
type UncheckedInt is int;
function mul(UncheckedInt a, int b) returns (UncheckedInt c) {...}
using {mul as *} for UncheckedInt; Going back to the associativity issue, this creates an annoying problem on whether the expression I'm generally in favour of operators, but allowing mixed operators for Custom operators (which can be mixed) surprisingly solve this problem: infix function <*>(UncheckedInt a, int b) returns(Unchecked c) {...} Now for |
Another option would be to introduce a new set of operators: |
Another option would be to extend the concept of This would even prevent obscure operator definitions that cannot be recognized locally, since if I don't use well-behaved standard-signature operators, I locally have to use e.g.
|
Rust restricts custom operators to be defined only on the left hand side type. This makes lookup easier and maybe also eliminates the associativity problem, but I currently don't see how you can do things like |
It might be "clean" to (at least for now) restrict the target of a "using for" statement at file-level to data structures only, i.e. disallow the use for contract-like types and of course non-nameable types. |
As far as I understood @ekpyron 's arguments, the only benefit of restricting operators to a fixed type signature (i.e. You can write functions If we relax the restriction (like rust does it) and say that operators can be defined on different types (like Since I'm in general in favour of requiring explicit types and think that auto-type-deduction is nice but can lead to errors since explicit types always also force you to condense your thinking into categories that specify semantics (like |
@hrkrshnn proposed to make applying the using statement upon import more explicit. One solution there would be to use
I.e. if we have "export using" then the effect of |
Updated the issue description. |
Some alternatives for the
latest: |
@hrkrshnn wanted to know hy
I think only allowing file-level (and library) functions for using statements at file level is the simpler and saner approach. |
EDIT: [this wasn't entirely thought through] Thinking about it, I'm not so sure the whole "export using" or "using.. globally" stuff is a good idea... I'd do the following:
The result is a situation in which you can:
And I avoid to have weird implicit scoping effects and strange behaviour due to diamond-like cross-importing exported using statements on the same types and all such issues that will come up eventually with exporting EDIT: Ok, I missed the fact that the intention was to only allow |
When we have |
This is a proposal to extend the "using for" directive in the following ways:
*
The semantics are as follows:
The directive
using * for ...
matches all free functions in the current file (including non-scoped free functions imported from somewhere else), but it does not match functions in libraries or functions imported scoped into a module (moduleName.functionName
).The directive
using functionName for ...
matches the specified function. Here,functionName
can be a path likemoduleName.functionName
.The directive
using moduleName for ...
matches all free functions in the imported module (viaimport "fileName" as moduleName
) including free functions imported from that module at file level. Here,moduleName
can be a path likemoduleA.moduleB
.Note that most of the new uses are incompatible with flatteners.
Update (2021-09-20):
Extend the
using A for B
statement everywhere to allowA
to also be*
If used at file-level,
B
cannot be*
.If used at file-level also allow
export using A for B
, whenB
is a type declared at file-level in the current file.Clarification: In the above, all "items" can be paths (
A.B.f
) and don't have to be single identifiers.The text was updated successfully, but these errors were encountered: