Open msadeqhe opened 11 months ago
The unifying functions and blocks as explained by @hsutter in here, will be changed like this:
f: (x: int = init) { ... } // `x` is a parameter to the function.
f: (x: int = init) statement; // same, { } is implicit.
f: (x: int = init) = expr; // { return expr; } is implicit.
: (x: int = init) { ... } // `x` is a parameter to the lambda.
: (x: int = init) statement; // same, { } is implicit.
: (x: int = init) = expr; // { return expr; } is implicit.
(x: int = init) { ... } // `x` is a "let parameter" to the block.
(x: int = init) statement; // same, { } is implicit.
{ ... } // `x` is a "let parameter" to the block.
statement; // same, { } is implicit.
As we see, terser lambda syntax : (x) x + 1
breaks the rules, because x + 1
is not a statement, it's an expression. By making =
to be required in lambda syntax, : (x) = x + 1
may seems to be a better choice, because =
improves its readability and we can specify the return type if necessary.
EDIT: For example, we can have the following lambdas:
: (x) -> void { print(x); }
: (x) { print(x); }
: (x) print(x)
: (x) -> _ { return x + 1; }
: (x) -> _ = x + 1
: (x) = x + 1
a: (x) = x + 1;
is similar to declaring a variable with b: = n + 1;
. It improves the consistency of declarations.
Other related discussions: #634, #761.
As we see, terser lambda syntax
: (x) x + 1
breaks the rules, becausex + 1
is not a statement, it's an expression.
f:(x: int = init) = { ... } // x is a parameter to the function f:(x: int = init) = statement; // same, { } is implicit :(x: int = init) = { ... } // x is a parameter to the lambda :(x: int = init) = statement; // same, { } is implicit (x: int = init) { ... } // x is a parameter to the block (x: int = init) statement; // same, { } is implicit { ... } // an ordinary block statement; // same, { } is implicit
I think :(x) x + 1
is actually a fine,
but separate case from explaining blocks.
That's why the design note was created for, after all (#714).
(In another section further down:)
To me, allowing a generic function
f:(i:_) -> _ = { return i+1; }
to be spelledf:(i) i+1;
is like that...
It is statement (in layman word, it states a fact) that left side equals to right side.
I don't think it's a problem, =
has meaning like a a <- b
i.e. a was something, then it changes value to b. That has sense in runtime context, when variable, well, can vary mutable. It doesn't have much sense at compile time context, because we usually cannot modify or remove things from program, only add facts about it, but after compile, all those facts are set in stone and cannot be changed, so =
is moot. That's all humble IMO of course.
It doesn't have much sense at compile time context, because we usually cannot modify or remove things from program, only add facts about it, but after compile, all those facts are set in stone and cannot be changed, so
=
is moot.
The same is true of const
and constexpr
variables.
Would you say those use =
for consistency?
@realgdman The a <- b
relation doesn't work well with = {}
. For example:
fnc1: () -> i32 = x = { return y; } // ERROR
fnc2: () -> i32 = { return x; } = y; // ERROR
fnc3: () -> i32 = x = y; // OK
Generally = {}
doesn't work consistently in other parts of the language. {}
cannot be set to anything, also we cannot set it to anything, because {}
is not an expression, it`s a block statement:
x: = something;
x = { /* ... */ }; // ERROR
x = another; // OK
On the other hand, =
(as most programmers expect it that way) works with expressions.
Does that works because =
assignment returns/has a value? It is natural for C/C++, but there are languages where assignment doesn't return value. Also returning value from =
doesn't have much sense at global compile time level? Or do you mean need of chaining v32: type == myT : type == std::vector
No, I mean = {...}
doesn't work in Cpp2 generally:
x: TYPE = 10;
x = { /* ... */ } // Syntax error
Because it's meaningless to put a block statement in a side of assignment.
Let's discuss the new syntax if we drop =
from declarations.
The new syntax improves consistency with control structures.
For example, this is how they look in old syntax:
if x < 10 {
return x;
}
f: () -> i32 = {
return 0;
}
Now, comparing them in new syntax:
if x < 10 {
return x;
}
f: () -> i32 {
return 0;
}
Both of {}
don't have =
because they are block statements.
inspect
Expressions)For example, this is how they look in old syntax:
var1: = n + 1;
var2: i32 = n + 1;
fnc1: (x) x + 1; // Inconsistent
fnc2: (x) -> i32 = x + 1;
Syntactically fnc1
is not consistent with other declarations.
Now, comparing them in new syntax:
var1: = n + 1;
var2: i32 = n + 1;
fnc1: (x) = x + 1;
fnc2: (x) -> i32 = x + 1;
All of them have =
because they are defined with an expression. This is also true within inspect
expressions:
var1: = inspect x < 10 -> bool { is true = x; is _ = x - 10; };
= ...;
is used, it's consistent with how we use it to declare variables and functions from expressions. Also {}
in inspect
is a block expression, thus it's within expression in a side of assignment.
Let's explore other concepts of {}
in programming.
{}
can be a block expression, but we don't have it yet in Cpp2. Considering if
-expression:
var1: = if x < 10 { x } else { x - 10 };
Those {}
's are not block statements, they are block expressions. So it's meaningful to put them within expression in a side of assignment.
Also {}
can be a list of items:
var1: std::vector<i32> = {1, 2, 3};
That {}
is not a block statement, it's an expression. Its value is a list of items.
Considering those two concepts and function declaration syntax, if Cpp2 will support them, we will have:
// `{}` is a block statement.
fnc1: () -> std::vector<i32> = { /*...*/ }
// `{}` is a block expression or a list of items.
fnc2: () -> std::vector<i32> = { /*...*/ };
The meaning of {}
is confusing. It's meaning can be changed with a single ;
at the end (that doesn't feel right). Now, let's consider the lambda syntax:
// `{}` is a block statement.
call(: () -> std::vector<i32> = { /*...*/ });
// `{}` is a block expression or a list of items.
call(: () -> std::vector<i32> = { /*...*/ });
Lambda syntax doesn't have ;
at the end. So both of them looks the same now. Cpp2 has to look inside {}
to find what it means. It's ambiguous for empty {}
, and it harms both readability and toolability.
If @hsutter would approve this bug report with the new syntax, fnc1
declaration would be invalid. On the other hand we would have fnc3
declaration:
// `{}` is a block expression or a list of items.
fnc2: () -> std::vector<i32> = { /*...*/ };
// `{}` is a block statement.
fnc3: () -> std::vector<i32> { /*...*/ }
Obviousely {}
is a block expression or a list of items in fnc2
, because it's on the right side of assignment operator, and {}
is a block statement in fnc3
. The lambda syntax of them is like this:
// `{}` is a block expression or a list of items.
call(: () -> std::vector<i32> = { /*...*/ });
// `{}` is a block statement.
call(: () -> std::vector<i32> { /*...*/ });
Fortunately they are different, the tool can easily know what {}
means in both of them as its readability is improved.
The point is, the consistency between declarations and control structurs will be improved with the new syntax, and C++ programmers are familiar with it (because of strict separation of block statements and expressions). Also it avoids ambiguity if Cpp2 will support block expressions and list of items with {}
. Thanks.
Today, name: signature = statement
is used for all declarations.
You want to drop the =
when the statement
is a block.
All to increase consistency with statements that end in a block.
That makes declarations themselves inconsistent. Also, what happens to declarations that don't always have a block?
f: () -> _ { return g(); } // Proposed.
f: () -> _ = { return g(); } // Today.
f: () -> _ = g(); // Today.
f: () g(); // Today.
Although you haven't suggested these, I'll mention them.
I'm against dropping =
for variables.
I'm against using {}
for initialization
(e.g., from x := 0;
to x: {0};
or the further inconsistent x{0};
).
Today,
name: signature = statement
is used for all declarations.
Today we have the following declarations:
name: signature = block-statement
name: signature = statement
name: signature = expression;
name: parameter-list statement
name: parameter-list expression;
Am I right? So we don't have only one declaration syntax today. I've explained why they are confusing.
That makes declarations themselves inconsistent.
My suggested syntax is:
name: signature block-statement
name: signature = expression;
name: parameter-list statement
Why do you find it inconsistent? It improves Cpp2 code, because it makes block-statement
and statement
not to be confused with expression
. It makes assignment in = expression;
a known notation to be used everywhere. Hence = expression;
is the only way to declare variables.
Today,
name: signature = statement
is used for all declarations.Today we have the following declarations:
name: signature = block-statement name: signature = statement name: signature = expression; name: parameter-list statement name: parameter-list expression;
Am I right? So we don't have only one declaration syntax today. I've explained why they are confusing.
The first tree are covered by the second one.
The last two are https://github.com/hsutter/cppfront/wiki/Design-note%3A-Defaults-are-one-way-to-say-the-same-thing.
Are you also refuting defaults when =
is involved?
That makes declarations themselves inconsistent.
My suggested syntax is:
name: signature block-statement name: signature = expression; name: parameter-list statement
Why do you find it inconsistent?
Not all declarations follow the same syntax name: signature = statement
.
Also, it's not clear how that supports parameterized expressions like :(x) x
.
Are you also refuting defaults when = is involved?
No, they can have defaults:
From this comment: EDIT: For example, we can have the following lambdas:
: (x) -> void { print(x); } : (x) { print(x); } : (x) print(x) : (x) -> _ { return x + 1; } : (x) -> _ = x + 1 : (x) = x + 1
a: (x) = x + 1;
is similar to declaring a variable withb: = n + 1;
. It improves the consistency of declarations.
=
is consistently for expressions only.
Not all declarations follow the same syntax
name: signature = statement
. Also, it's not clear how that supports parameterized expressions like:(x) x
.
Yes, it doesn't treat statements and expressions in the same way. On the other hand, current Cpp2 tries to treat them in the same way. That's why I'm reporting this bug.
I simply think that pursuing this issue is not worth it. Personally, while I was initially of the same opinion, cpp2 syntax has grown on me.
And I think there's some language that uses ==
for constants and the way cpp2 uses it kinda makes sense (except for constexpr functions).
Edit: About lists, zig uses .{}
syntax for braced initialisers, cpp2 could have the same. Another (better) way could be to have a trailing or leading comma to signify lists/tuples (pls have those in cpp2).
This bug report is about = {}
in declarations. I ask to fix it by dropping =
from them, so it leads to a new syntax.
The ways we declare functions in new syntax are:
// With new syntax
name: signature statement
name: signature = expression;
That's it. statement
may be either a block statement ({ print(x); }
) or a single statement (print(x);
).
Infact the second one is a variant of the first one. I mean = expression;
is a syntax sugar to { return expression; }
, so the new syntax can be just one grammar (with different default return types) instead of two grammar.
Considering this with the current syntax = {}
:
// Today in current Cpp2.
name: signature = something
name: parameter-list something
Just like the new syntax, the second is a variant of the first declaration in current Cpp2 (today).
That's it. Today (in current Cpp2) something
is confusing to both programmers and tools, because we cannot simply find if something
is a statement
or an expression
.
In the current Cpp2's grammar:
// Today in current Cpp2.
name: signature = something
name: parameter-list something
The something
part can be either a block statement, a single statement or an expression. So why is something
confusing? Let's consider the grammar of current Cpp2 (today) if we don't specify the return type of functions:
// Today in current Cpp2.
name: parameter-list = something
name: parameter-list something
parameter-list
means we didn't have specified the return type in the signature
. So the first something
doesn't return a value, but the second something
returns a value! It is inconsistent and confusing:
a: (x) = fnc1(); // It doesn't set/return a value!
b: (x) fnc2(); // But it sets/returns a value!
Please let's review it again, fnc1()
doesn't return a value, but fnc2()
returns a value! What?! =
in the first declaration has exactly the opposite behavior of assignment. That falsely means when the return type is not specified, the programmer have to read it like the following sentence to find out the default return type:
"=
is written before fnc1()
, so it doesn't have a value!".
It's absolutely inconsistent, and unfamiliar to C++ programmers.
On the other hand, if fnc1()
has a return value, we have to write it like this one instead, otherwise it'll be a compiler error!
a: (x) = _ = fnc1(); // It doesn't set/return a value!
b: (x) fnc2(); // But it sets/returns a value!
How much do you find it readable/consistent?
It's the main reason behind of this bug report.
Also it's misleading, and also inconsistent with how we currently declare variables:
fnc1: (x) = fnc1(); // It doesn't set/return a value!
var1: i32 = fnc2(); // But it sets/returns a value!
Also = something
without signature
doesn't have a meaning in itself. They highly depend on the signature
of the function, tools have to check the signature, and programmers have to read the signature. Otherwise no one will know if something
is a statement or an expression.
Although it has a higher level view in the language, because the programmer writes code regardless of if it's a statement or an epxression, but it complicates the grammar of Cpp2 for parsing, tooling and reading the code.
That's the reason I've created this issue as a bug report instead of a feature suggestion.
=
from them?On the other hand, if we drop =
from = something
, the grammer of Cpp2 will have a distinct syntax for each block statement and expression. I mean:
= expression;
will be an expression everywhere. It will have a value everywhere.{}
will be a block statement everywhere if =
is not present before it. {}
will be without =
in both declarations and control flows consistently.statement
and expression
, the rules will be specified simpler than when we don't have that distinguish syntax. Tools and programmers can visually find the meaning of the code with the help of this distinguish syntax. This also allows to consistently add more features to the language:
Thanks for your patience.
To fix this bug:
a: (x) = fnc1(); // It doesn't set/return a value! b: (x) fnc2(); // But it sets/returns a value!
Please let's review it again,
fnc1()
doesn't return a value, butfnc2()
returns a value! What?!=
in the first declaration has exactly the opposite behavior of assignment. That falsely means when the return type is not specified, the programmer have to read it like the following sentence to find out the default return type:"
=
is written beforefnc1()
, so it doesn't have a value!".It's absolutely inconsistent, and unfamiliar to C++ programmers.
We may consider the following solutions to fix the bug of the default return type.
Add =
to all declarations:
a: (x) = fnc1(); // ERROR! It doesn't set/return a value!
b: (x) = fnc2(); // But it sets/returns a value!
In this way we have to make _
to be the default return type for all declarations. So a
declaration is a compiler error! and we have to explicitly write it like:
a: (x) -> void = fnc1(); // It doesn't set/return a value!
b: (x) = fnc2(); // But it sets/returns a value!
The problem is that for functions with block statements, the default return type will be deduced too:
@hsutter's comment: Quick ack: Making
-> _
deduced return types the default for namespace- and type-scope functions (as it is today forauto
-declared trailing return syntax functions) feels problematic because it requires callers to be aware of the implementation details. And that's fine and useful, but it's more work at compile time and occasionally more brittle to changes in the function body, so it should be opt-in I think. The reason I made it the default for local anonymous functions (lambdas) at first was because they're inherently local and inline anyway so don't really hit the composable-at-scale issues.
This solution doesn't allow Cpp2 to have different default return types in terser function syntax when it's needed in different context. So we always have to write =
regardless of if the function is declared with a block statement or an expression.
Swap =
in declarations:
a: (x) fnc1(); // It doesn't set/return a value.
b: (x) = fnc2(); // But it sets/returns a value.
It looks natural. The default return type is natural for both of them. The programmer simply looks at =
and says:
Hey, it has =
before func2()
, so it must have a value.
That's natural in other part's of the language. It improves readability. It makes =
an important part of function declarations. It's consistent with how we declare variables, or assign values to them:
b: (x) = fnc2();
x: i32 = 2;
x = 10;
This change leads to drop =
from full function declaration syntax too:
a: (x) -> void { fnc1(); }
a: (x) { fnc1(); }
a: (x) fnc1();
b: (x) -> _ { return fnc2(); }
b: (x) -> _ = fnc2();
b: (x) = fnc2();
Also for consistency, other declarations have to be changed:
f: (xy) { }
t: type { }
n: namesapce { }
g: (xy) = value;
v: TYPE = value;
It's consistent with control flows, inspect
expressions, and etc.
To fix this issue, solution 2 is superior to solution 1. Thanks.
The number of up votes on this comment, is a data to consider that many Cpp2 programmers find f: (x) = x;
to be more readable and understandable than f: (x) x;
:
Maybe it is just me, but I think I am loving the
=
simply because of readability.For the same reason I don't like the terse function syntax in the form
func: (x) x;
Furthermore, I am concerned about the
==
syntax forconstexpr
, because I am immediately thinking of an equality comparison operator and that's simply wrong....
That's because programmers expect ==
and especially =
to have a familiar and consistent behavior.
Now considering type
and namespace
declarations, {}
defines a new one (from a block declaration), =
sets its definition from a previousely defined one (from a name or fully qualified name):
// {} defines a new type:
cls1: type {
// declarations...
}
// cls2 definition is set from cls1 definition:
cls2: type = cls1;
It's similar to using
statement (type and namespace aliases) in Cpp1.
Considering lambda expressions (unnamed functions), it will be terser without =
in full lambda syntax:
: (x) -> void { print(x); }
: (x) { print(x); }
While it's somehow similar to lambdas in Cpp1, but currently we have to write them in Cpp2 (today) with =
:
: (x) -> void = { print(x); }
: (x) = { print(x); }
Discussion #793 is related to this topic.
Description
Considering @hsutter's comment from discussion #623:
The part "we should closely follow the built-in operators' meanings" is important here.
To Reproduce
Notation
==
Considering type and namespace aliases:
A typical C++ programmer expects
==
to return the result of a comparison, but it setsv32
type definition from another type instead. Therefore==
is an assignment operator in declarations, but it is a comparison operator in expressions.Considering
constexpr
functions and variables,==
is more confusing:The first
==
setsfnc1
function definition, but the second==
compares two literals and returns the result. A typical programmer may ask why it's happening while they are next to each other.Considering
concept
declarations in Cpp2:Unlike types and namespaces, it uses
=
instead of==
. Cpp2 is/was designed for C++ programmers of which they are more familiar with declaring stuff by=
than==
in Cpp1:Notation
=
Considering function declaration syntax:
If we ask programmers who are not familiar with Cpp2 that "What does
fnc1
function do?", they probably will answerfnc1
returns an empty list{}
. Because that's natural to programmers.=
puts the result of its right side to its left side.{}
is on the righ side, so it must have a value.But this known behavior doesn't work in Cpp2.
=
doesn't set anything in the example, and{}
doesn't have a value too! Of coursevoid
is nothing, it's not even similar tonull
andnullptr
which they are values.EDIT: Considering this note from this comment:
Looking at
fnc2
, a typical programmer which is not familiar with Cpp2, thinkssomething()
has a value and the function returns the value from the right side of=
, that's expected because of the assignment operator in the declaration. Although we had this natural behavior in Cpp2 before, but @hsutter changed this behavior (because of issue #257) because of the push-back he got on issues/comments as he explained it here.Why not bring back the original behavior for function declarations with
= expression
only?Additional context
To solve the issue, Cpp2 may have a simple defined rule to separate statements from expressions in declarations, if one extra
=
could be dropped from them:fnc1
is defined by a block statement, because it doesn't have=
, butfnc2
is defined by an expression, because it has=
. This rule allows to categorize function decleration to two syntaxes.First, the function declaration (also lambdas) with block statement: it has not a return type by default:
Second, the function declaration (also lambdas) with an expression after
=
: it has a generic return type by default:In a similar manner, types and namespaces would be (more familiar to C++ programmers):
In general
{}
is a block statement and= ...
is an expression or a name (or a fully qualified name).Also for
constexpr
functions and variables, other solutions such as using a keyword can be considered.Discussions #742 and #714 are related to this topic. Thanks.
Follow-up Readings
More discussion about how the new syntax may improve Cpp2 in this comment:
inspect
Expressions)"Why is it a bug?" from this comment:
=
from them?"Why the new syntax is necessary?" from this comment.