-
Notifications
You must be signed in to change notification settings - Fork 109
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
Disallow redefine names in the same scope of do/let #441
base: main
Are you sure you want to change the base?
Conversation
9e0cfd0
to
c78beaf
Compare
c78beaf
to
9a7a9dc
Compare
There are other possibilities for it to go wrong let
a = (2, 3);
(x, a) = a; // rebind a
in
print(a); // outputs 3 and let
a = {x=3, y=4};
{x=p, y=a} = a; // rebind a
in
print(a); // outputs 4 Being a Haskell-alike lang, those behaviors should not be allowed. But fixing them takes more work |
But in the examples I gave, they are in the same let clause |
Don't let
Note, the variable |
I was aware of this rewriting to nested
Based on the principle of least astonishment, we have some options,
|
Hobbes has no notion of Does the following also look illegitimate to you? Also, note that your current change addresses this issue only from the perspective of the parser. There are more ways (especially using the C++ bindings) one can build One more thing, the title of the ticket implies that there's a notion of "the same scope on do or let" in Hobbes. This is not true and it is misleading as, otherwise, these scopes would be somehow destructive. Each introduction of a new binding creates a totally new scope, different from the previous one. It's described in the doc in the section "local variable definitions". It may be that the doc isn't clear enough, so it should be improved ( I did mention earlier a more subtle (serious bug) issue in hobbes with the handling of
The pattern matching engine is a critical piece of Hobbes, and the above 2 issues are the closest to what you're trying to fix here. I believe that, fixing them will also address the |
But how could a hobbes programmer expect names in a simple If there's a good reason to be so, then it should be better documented. If not, then this behavior should be changed |
As for the Local Variable section in doc. It uses an However, the problem we're talking about is more like this, still in REPL
I know REPL works differently, but people are expecting "If it looks like a duck, swims like a duck, and quacks like a duck, then it probably a duck" How do we expect others to spend time on understanding this subtlety for a simple |
What you say about Haskell isn't true! Hobbes is inspired from Haskell, not C, not Java, and not C++. Again, it's important to read documentation, and not make assumption that everything that uses curly braces quacks, then it's [like] C++. |
Hopefully you do! And if so, then it should be easy to explain what happens in the REPL. When you When in the REPL, what would that
Document it in simpler words that If you want to assign a new value, then you have the a = newPrim() :: int
a <- 2
a <- 3 |
foo = \_ -> let a = 3
a = 4
in a + a reports
in Haskell. Even for people have c++/c/java background expect this behavior In hobbes, foo = \_.let a = 3;
a = 4;
in a + a
I might argue there are some similarities in those code snippets, but surprisingly differently behaviors. I'm not saying hobbes IS haskell, despite multiple places in documentation mentions haskell. I'm saying hobbes should have the same behavior as haskell But at least, we should limits language surprises as much as possible. Just like you said
Then at least, we should let compiler panic and tell programmer his/her code probably has logic error, error message can be something like
|
The following works in Haskell, and that's the simplest behavior for let that Hobbes is also implementing ghci> foo = \_ -> let a = 1 in let a = 4 in a + a
ghci> foo ()
8 Like I said earlier, Hobbes rewrite all There are various ways to implement
I believe you didn't follow my previous explanation; that's exactly what the compiler complained about when you tried it in the REPL: > a = 3
> a = 4
stdin:1,5-5: Variable already defined: a It's a matter of scope definition, that's the point I was making earlier. In a REPL, that scope is the whole session. In a compiled program, that scope is delimited by
Haskell allows the following, which Hobbes doesn't allow. Your whole point is that we shouldn't allow binding variable names 2x in let expression; and Haskell does allow this in certain cases. And you want Hobbes to have the same behavior as Haskell, which is to allow binding variable names to new values as in Haskell. ghci> let a = 1
ghci> let a = 4
ghci> a + a
8 The above is plain Haskell. I don't follow you anymore. |
If you are so concerned about the possibility for Java developers to get confused about the above, then the issue is that they don't understand the rewrite rules around let blocks. Remove that rewrite rule around syntactic sugars such as |
Let me set the record straight on what I've been talking about
Everything boils down to, in your words, My whole point from my first post is simple: Should the compiler be more helpful that it should tell programmers that theirs code makes no sense? |
Why not? the principles of let evaluations stay the same if you understand they always evaluate in a context of variable assignments. These principles are the same whether you're in the REPL or in a compiled program. I strongly recommend you read about propositional logic and their evaluation judgments if you haven't already, because this is no different.
This is highly subjective. What's a normal programmer? A Java developer? A C++ dev? or one who reads about the semantics of a symbol such as If that's not clear,then it is important you document/remind to your users the semantics of In Hobbes,
In C++,
It's okay to be different. Hobbes is not a C++ replacement language, and it serves different purposes.
You are ignoring the context (the scope), so this isn't 100% correct. This is absurd if you consider a single scope (as is done in the REPL), but it is not absurd if each binding introduces different scope (as that's the case in One can't make a statement, show it's True for a single instance, then consider it a proof of it being universally True like you do here.
Good point, but remember: the example you are flagging as being How about the following? Is this absurd to you as well? mo.C ✕ +
1 auto main () -> int {
1 │ auto a = 3;
2 │ │ a = 4;
3 │ return a+a;
4 } |
I'm surprised to see your response to the word "absurd", that makes me realized that I might misunderstand what you've said
say, in hobbes code, when you see something like let
...
a = ...
...
a = ...
in ... Current compiler is happy to compile this code, but are there any better(correct?) ways to do whatever the programmer wants it do? From your previous post, I thought your answer is Yes, there are better ways to fulfill his/her purpose Seems I got your answer wrong. So it really depends on your answer to this, then we might have a totally different discussion My standing on this PR is simple, it is all about ergonomics
Let's say we have a c++ code like this struct X {
T foo() const;
}; Any C++ compilers will be happy to compile this code, because it is legit C++. but clang-tidy still warns me that it might be a good idea to add We write bugs with perfect grammar, tools can help I couldn't figure out what kind of functionalities in reality can only be achieved by |
What you want is a way for the compiler to check for potential name shadowing and flag them for you. You don't want to prevent advanced users from writing these expressions. You want a way to lint your programs, so the compiler can warn you on potential name shadowing issues. Make it an option, then choose a default that works for the largest audience. Guess what? We do have support for Optional flags and the ability to extend the compiler with new flags. @dawa79 (Dan Wang) can walk you through how he added the There's a Don't ever change the parser though, for no reasons. You hardly ever need to change the parser, except for compelling cases. See the parser as the contract Hobbes has with its users. You never want to break this contract.
I'm lazy and find it hard to invent new variable names each time I need one. Seriously, I don't think variable names are an infinite resource; they are not. The compiler is the buddy that figures names out for me, so I can focus on engineering solutions for harder problems. Isn't that the whole point about Lambda calculus? Names don't really matter... But I'm digressing, I'll stop it here now. |
for the moment, following code compiles
This behavior is weird for a functional programming lang
(edited) and for
let
expressionThis should be allowed neither