-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Implement generic number literals #6919
Conversation
I'm very happy to see this! Are there plans to provide support for pattern matching too? |
I have not thought about it. Do you have a proposal how to do that? |
bdeda22
to
77e9179
Compare
Regarding pattern matching, if the expansion of a literal creates something matchable (such a case class instance) we are good. The problem is what to do if that's not the case. Right now, we treat an application
If we have that, the inline expansion of a number literal can add the braces (or whatever) to make it work. So the problem is really more general than just literals. |
I had only ever thought about it casually, but the
(which was probably the most obscure feature I ever used in Scala 2.) but it's quite high up my list of desirable features. Relatedly, I also often thought it would be nice to be able to explicitly specify types to extractors, like so:
I'll think about the general idea some more. |
77e9179
to
5a3f872
Compare
It turns out that the idea to treat contents of blocks in patterns as expressions works with some minor tweaks. So this means we support generic literals as patterns now. So far, this is all internal; we do not support |
That sounds like it can open up a whole variety of interesting possibilities. Thank you! |
Evaluating this expression throws a `NumberTooLarge` exception at run time. We would like it to | ||
produce a compile-time error instead. We can achieve this by tweaking the `BigFloat` class | ||
with a small dose of meta-programming. The idea is to turn the `fromDigits` method | ||
of the into a macro, i.e. make it an inline method with a splice as right hand side. |
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.
of the...?
@odersky Do you think there is any use in tweaking the parser to support multiple adjacent
This would obviate |
I like |
@anatoliykmetyuk @nicolasstucki Can you figure out why the CI does not pass? Everything works fine locally. |
@odersky given as Liftable[BigInt] {
def toExpr(x: BigInt): given (qctx: QuoteContext) => Expr[BigInt] =
'{BigInt(${x.toString.toExpr})}
} It should also fail locally. However, I encountered troubles recompiling the PR: even after doing |
Or slightly more compact given as Liftable[BigInt] {
def toExpr(x: BigInt) = '{ BigInt(${x.toString.toExpr}) }
} |
May also need a rebase to have the 0.17.0-RC1 reference compiler. |
2f26213
to
6bacde0
Compare
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.
Otherwise LGTM
title: Numeric Literals | ||
--- | ||
|
||
In Scala 2, numeric literals were confined to the promitive numeric types `Int`, Long`, `Float`, and `Double`. Scala 3 allows to write numeric literals also for user defined types. Example: |
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.
In Scala 2, numeric literals were confined to the promitive numeric types `Int`, Long`, `Float`, and `Double`. Scala 3 allows to write numeric literals also for user defined types. Example: | |
In Scala 2, numeric literals were confined to the primitive numeric types `Int`, Long`, `Float`, and `Double`. Scala 3 allows to write numeric literals also for user defined types. Example: |
The companion object of `BigFloat` defines an `apply` constructor method to construct a `BigFloat` | ||
from a `digits` string. Here is a possible implementation: | ||
```scala | ||
object BigFloat extends App { |
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.
object BigFloat extends App { | |
object BigFloat { |
case digits => | ||
'{apply($digits)} | ||
} | ||
} |
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.
} | |
} // end BigFloat |
@@ -703,7 +767,8 @@ class Typer extends Namer | |||
(index(stats), typedStats(stats, ctx.owner)) | |||
|
|||
def typedBlock(tree: untpd.Block, pt: Type)(implicit ctx: Context): Tree = track("typedBlock") { | |||
val (exprCtx, stats1) = typedBlockStats(tree.stats) | |||
val localCtx = ctx.retractMode(Mode.Pattern) |
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.
Where did this Mode.Pattern
come from?
Was it from the pattern of something like?
bigFloat match {
case 123_344_537_244_453E433 => ???
}
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.
Yes, exacty. We might make it user-accessible syntax though.
inferImplicit(fromDigitsCls.typeRef.appliedTo(target), EmptyTree, tree.span) match { | ||
case SearchSuccess(arg, _, _) => | ||
val fromDigits = untpd.Select(untpd.TypedSplice(arg), nme.fromDigits).withSpan(tree.span) | ||
val firstArg = Literal(Constant(digits.toString)) |
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.
val firstArg = Literal(Constant(digits.toString)) | |
val firstArg = Literal(Constant(digits)) |
6bacde0
to
f1a26bd
Compare
The main open issue is what the right typeclass infrastructure should be. Question 1: Should Merging is attractive because it simplifies the design but in summary I believe the two classes are better kept separate. The contract for Question 2: Should we collapse the four classes Again, we trade off conceptual simplicity for ease of implementation. Here the choice actually does make a difference. Say, we have class FooNum that implements val x: FooNum = 2.0 Under the current 4-class scenario, Under a collapsed scenario, 2.0 would be treated as a So the current 4-class scheme is closest to the status quo; it only treats a literal as a user defined type if the user defined type has declared that it accepts that kind of literal. |
In summary, I propose to stick with the status quo and merge it now. We can still change it later if different arguments come to light. |
I'm not convinced by this feature. For starters, what is really the motivation? The user-defined literals are expected-type-driven. This means that they work in some cases, but not in others. In particular:
Can I have actual constant expressions of type Why does this work for numeric literals, but not for other literals? In particular, could I recover symbol "literals" from string literals with something like this? Why is this considered when we already have string interpolators and we could write Why is this feature considered morally better than implicit conversions, which are basically deprecated? |
Just define an overloaded equality
Yes, that looks like a hard limitation.
No. Primitive number types are treated as before. But
Simplicity. I find that string interpolator really ugly. It's one of the clear advantages of languages with unlimited precision numbers that you can define numbers without artificial restrictions. Let's face it: the whole way we define numeric literals with suffixes and so on is artificial and unnatural. A literal is just what it is. It is given meaning by ascribing a type. |
That has the same problem with the left-hand-side:
I disagree. The whole way we have different types of numbers with differing precisions is artificial and unnatural. It goes against math. But given that we have different types of numbers with different behaviors (based on ad hoc polymorphism to boot!), I'm very happy we have to use explicitly typed literals. They prevent potential warts. |
I believe the examples with
and
both work and can be made to work by library code for arbitrary number types. So the main improvement that this PR brings is that we can write large number literals which are not constrained by the ranges of existing number types. However, in practice you'd rarely write a large number literal inline. Common practice recommends that you define a constant instead. So, with this proposal it's val x: Int = 100
val y: Long = 10_000_000_000
val z: BigInt = 10_000_000_000_000_000_000_000 instead of the status quo:
I find the first much more pleasant and regular than the second. The limitations when you can or cannot use a really large number inline in an expression are very similar to mixing existing small number literals and custom types. There is as far as I can see only one restriction for large literals:
will not work. It works for small literals on the left since there are implicit conversions from |
I should also note that it's not my idea. I got this from Guy Steele, who proposed this some time ago. |
An extra concern in Scala is that so far, all literals have a corresponding singleton literal type (symbol literals are deprecated so I'll ignore them), but with this proposal, this would no longer be true. It's not necessarily a deal-breaker but I think it's worth exploring alternative designs that would support singleton types. |
Amazing idea. Spire needs a lot of complex/hackish code to help people write mathematical like notation and it works 85% of the time (especially with all the Int implicit conversions already in the stdlib). I'll take a closer look later on this. |
Note that both Java and Scala already do this to some degree: An |
It seems to me that this feature simply adds more irregularity to the language than it gets rid of. The current status quo, as I understand it, is either defining a string interpolator and using it everywhere, or the complex/hackish implicit conversions that @denisrosset mentioned. Since I do not actually write any code that would use this feature, I will refrain from having any opinion, but I will point out what will (still) not work in the current version of the proposal: Equality comparisons with polymorphic literals will not work. If we're handling math vectors, then we may want to treat In general, binary operators will not work. If I defined a custom Defining constants with expressions will not work: val tau: BigFloat = 2 * 3.1415926535897932384626433832795028841971 That is, the "natural" way of defining Two notable languages which do have polymorphic literals are Haskell and Rust. However, both of them also have complete local type inference, which avoids all of the above problems and makes the feature work that much better. fn print(i: i64) {
println!("{}", i)
}
fn main() {
let is = vec![1, 2, 3];
for i in is {
print(i);
}
} No matter whether |
Regarding equality comparison, we have to look how it plays with multiversal equality. Regarding non-commuting operators, something that would be really useful is if we could define a right-associative operator with the colon notation: |
As a suggestion for educational materials, instead of using the phrase " |
@odersky What is the merge timeline for this? I'd love to try this on a "minispire" library and provide feedback, but I'm quite overwhelmed by academic collaborations at the moment. |
@denisrosset I'd like to merge this by the end of the month, at the latest. But even after merge it still has to be discussed in the SIP process. |
In an application with named and default arguments, where arguments have side effects and are given out of order, and some arguments are missing, the previous algorithm worked only if typed and untyped argument trees were the same. Test i2916 started failing once literals were represented as Number trees, since then untyped and typed versions of the argument were different.
But avoid inlining: - if the typechecking the body to inline generated errors - if checking inlined method generated errors - if we are in an inline typer and the same inline typer already generated errors. As part of this commit, merge hasBodyToInline and bodyToInline. An erroneous body can suppress inlining by returing an EmptyTree.
### Example: ``` val x: BigInt = 111111100000022222222222 ``` ### Also allow generic literals in patterns Wrap generic literals in patterns in blocks, which force expression evaluation. This allows to match (say) a scrutinee of BigInt type against large numeric literals. But it does not work if the scrutinee is of type `Any` because then the expeected type for the pattern is missing. This would be addressed by a feature that's still missing: patterns of the form `<literal> : <type>`. We have to change the syntax of patterns in general for this one.
f45af02
to
a276f0e
Compare
8af7117
to
93291a4
Compare
as suggested in scala#7134 by @vn971
In Scala 2, a typed pattern `p: T` restricts that `p` can only be a pattern variable. In Dotty, scala#6919 allows `p` to be any pattern, in order to support pattern matching on generic number literals. This PR aligns the syntax with Scala 2 by stipulating that in a typed pattern `p: T`, either - `p` is a pattern variable, or - `p` is a number literal
In Scala 2, a typed pattern `p: T` restricts that `p` can only be a pattern variable. In Dotty, scala#6919 allows `p` to be any pattern, in order to support pattern matching on generic number literals. This PR aligns the syntax with Scala 2 by stipulating that in a typed pattern `p: T`, either - `p` is a pattern variable, or - `p` is a number literal
In Scala 2, a typed pattern `p: T` restricts that `p` can only be a pattern variable. In Dotty, scala#6919 allows `p` to be any pattern, in order to support pattern matching on generic number literals. This PR aligns the syntax with Scala 2 by stipulating that in a typed pattern `p: T`, either - `p` is a pattern variable, or - `p` is a number literal
In Scala 2, a typed pattern `p: T` restricts that `p` can only be a pattern variable. In Dotty, scala#6919 allows `p` to be any pattern, in order to support pattern matching on generic number literals. This PR aligns the syntax with Scala 2 by stipulating that in a typed pattern `p: T`, either - `p` is a pattern variable, or - `p` is a number literal
Example: