Skip to content
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

proposal: Go 2: unmuddle syntax of if, conditional return, and several other removals #28245

Closed
l0k18 opened this issue Oct 17, 2018 · 7 comments
Labels
FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Milestone

Comments

@l0k18
Copy link

l0k18 commented Oct 17, 2018

So, I already have submitted the suggestion of the conditional return using an optional semicolon separated conditional expression, and in the process of responding to Ian's reply, I thought of a bunch of other connected things. They are all removals:

Colon termination on switch/case sections - it serves no obvious purpose and violates the general rule that colons are used in definitions.

Fallthrough in switch/cases. If statements are shorter and neater, and fallthrough cases should be before non-fallthtroughs anyway (that will give you some debugging fun if you don't structure it that way). So just put if blocks before it for single response conditions.

Labels and goto. Why, oh why did they even get into the spec. Or at least, why are labels gofmt'd onto their own line. There is no ambiguity about one symbol and a colon after it before any statement. The colon is almost as unequivocal as a semicolon, except it imparts meaning to the preceding symbol. But to hell with goto. It was necessary for basic with its line numbers and line editor, but not in modern full screen editors. Goto and labels seriously out of place.

(and just a few semi-nonserious suggestions):

Change the &, which is logically also an and, in the prefix unary for returning the reference, to @. This symbol is widely understood due to email URI's as indicating the location of something. The * is not quite so badly ambiguous, and really its use as multiply was probably a makeshift at the time due standard keyboard layouts, plus being a star it makes you think of points, and for its wildcard use, referring to something that fits in a slot. And with this you can eliminate the && as implicitly & is bitwise for integers and boolean for booleans. And the || was probably doubled because of the &&, so that can also now become singular and require both parameters be either integer or boolean.

Change the unary NOT operator to ! - there is no ambiguity because currently it can only be used in front of a boolean anyway, and you can't bitwise invert a boolean or more exactly, it's the same thing, logically speaking.

Connected with the above, we now have a freed up ^ symbol, which can be used as most languages do, as an exponential operator. It makes sense as an exponential operator because it's like 'just pretend the value after is a superscript'.

The last thing that just springs to mind is the 'func' keyword. Technically a function in mathematics produces a result. A procedure may not, and could also give you the result back anyway through indirection. But then 'function' means also a role as well as indicating operability. No I don't think I'd like to change func and likely nobody would, but strictly speaking, a non-returning thingy is a subroutine, to use procedural language.

There's another thing too. if there was a distinction between returning and non-returning functions, non-returning functions would be able to take pointer parameters, but returning functions really should not, unless they are methods, where it can be quite useful for one-parameter setting methods for a structure by passing through a pointer receiver. Continuing - on the base level of the source file, where functions are declared, EVERYTHING starts with a keyword. Any other thing than a keyword is either part of a declaration of something or inside brackets or on one line.

I think you could probably leave the 'func' out completely, and make a rule that if there is a return there can be no pointers going in. If you need to pass in pointers and want a return, you can use a procedure (non returning function). This would encourage more clean functions in go. This (generally) tends to catch more bugs on the way in than out (as in, you don't have to debug).

As for parsing, as I mentioned, the func is not needed to disambiguate a function. It does resemble a function call but function calls are not allowed at the top level of the source file. Brackets, like in a method, do not also exist without a keyword prefix. And lastly, only functions and things inside functions use paretheses. var, const, import, all use brackets.

Also, the thing about func, even if it is redundant for parsing, it looks cool and serves the other purpose of conceptually linking closures and functions, since they both use this keyword. Plus it makes a consistent rule that every line inside the root scope of a source file starts with a keyword or is inside brackets.

I don't think about this idea seriously at all but at the same time I think it's something that would be worth thinking about. I know, and I feel the same way, I don't want Go turning into a complicated, bug-encouraging hodge podge of me-too syntax changes as is the norm in the industry. So I am making some change suggestions that run completely the opposite way to most, and I think they keep with the spirit of the language. As an avid proporent and user of the language, a lot of the things in Go that are contrary to the programming language herd I have really become attached to. The strict parsing, the readability, the collection of best practices promoted to learners. I think paring a few things out of the language would go a long way towards improving efficiency, stopping common errors and so on. Adding things is necessarily going to mean complexity, unless it's done really carefully.

I hope you guys finalise upon at least one or two omissions. I am pretty strong about in particular fallthrough, and conditional return also, I am quite strong towards that as well. I have been doing a lot of work on code with multiple unrelated conditions and I ran the gamut of gotchas. Fallthroughs that have non-fallthroughs above them is a particularly nasty one and it's more pernicious for the fact it discourages the use of switch cases.

@ianlancetaylor ianlancetaylor changed the title Go 2 Proposal - unmuddle syntax of if, conditional return, and several other removals proposal: Go 2: unmuddle syntax of if, conditional return, and several other removals Oct 17, 2018
@gopherbot gopherbot added this to the Proposal milestone Oct 17, 2018
@ianlancetaylor ianlancetaylor added LanguageChange v2 A language change or incompatible library change labels Oct 17, 2018
@deanveloper
Copy link

deanveloper commented Oct 17, 2018

Colon termination on switch/case sections - it serves no obvious purpose and violates the general rule that colons are used in definitions.

One purpose it has is to prevent a semicolon from being inserted

Fallthrough in switch/cases. If statements are shorter and neater, and fallthrough cases should be before non-fallthtroughs anyway (that will give you some debugging fun if you don't structure it that way). So just put if blocks before it for single response conditions.

What's the point of switch/case if fallthrough doesn't exist?

And with this you can eliminate the && as implicitly & is bitwise for integers and boolean for booleans. And the || was probably doubled because of the &&, so that can also now become singular and require both parameters be either integer or boolean.

&& and & actually have two separate meanings. Because && is logical, the right side is not evaluated if the left side is false. Same with || and |, but the right side is not evaluated if the left side is true.

Change the unary NOT operator to ! - there is no ambiguity because currently it can only be used in front of a boolean anyway, and you can't bitwise invert a boolean or more exactly, it's the same thing, logically speaking.

I think it's good to separate logical and bitwise operators. Especially coming from C, the logical NOT ! operator is not the same as the bitwise NOT ~ operator.

Connected with the above, we now have a freed up ^ symbol, which can be used as most languages do, as an exponential operator.

This is just plain false. C, JS, Python, and Java ALL use ^ as the binary XOR operator. It is noteworthy that the unary ^ is just an XOR with a value that is all 1s, which is equivalent to an inversion.

The last thing that just springs to mind is the 'func' keyword. Technically a function in mathematics produces a result. A procedure may not, and could also give you the result back anyway through indirection. But then 'function' means also a role as well as indicating operability. No I don't think I'd like to change func and likely nobody would, but strictly speaking, a non-returning thingy is a subroutine, to use procedural language.

Functions in computer science have a different meaning than functions in mathematics. A "function" in the computer science sense nearly perfectly describes a function in Go.

I think you could probably leave the 'func' out completely, and make a rule that if there is a return there can be no pointers going in. If you need to pass in pointers and want a return, you can use a procedure (non returning function). This would encourage more clean functions in go. This (generally) tends to catch more bugs on the way in than out (as in, you don't have to debug).

Weren't you just talking about how all top-level declarations start with a keyword? Wouldn't this violate that principle?

As for parsing, as I mentioned, the func is not needed to disambiguate a function. It does resemble a function call but function calls are not allowed at the top level of the source file.

Yes they are. _ = functionCall() (although just calling the function in an init() function is generally better practice. Also, calling a function when the package is initialized is just bad practice in general)

Also, the thing about func, even if it is redundant

It's verbose sure, but it makes code more readable. Go makes several sacrifices which may add verbosity, but the extra verbosity makes code more readable. It's part of the design.

@l0k18
Copy link
Author

l0k18 commented Oct 17, 2018

ah, it's because the parser does a lot of really simple preprocessing and in most cases needs several indicators to confirm the identity of a symbol. Determining type of the parameter requires a lot more processing. Well, to be honest I think it looks better that way, I wasn't very serious about removing them.

What's the point of a switch case for a fallthrough? Compare these two:

switch {
case condition:
    statement
    fallthrough
case condition2:
    statement2
case !condition1:
    other statement
}

versus:

if condition {
    statement
}
switch {
case condition2:
    statement2
case !condition2:
    other statement
}

Yes, they are the same length. But there's a lot less typing for the second and if you are stupid enough to put a fallthrough after the first conditional, you may end up with a bug where a condition isn't being tested even though a subsequent condition is more important.

I am quite aware of the difference between boolean and bitwise logic operators. I also know that the boolean AND can be short-circuit evaluated. That is irrelevant. Ok, 45 years of C everyone is used to it and I wouldn't be serious if I said I thought this would be considered, but I also type on dvorak layout and my wrists have been eternally grateful since 2004 when I learned it. So I'll just leave it at that.

But in theory, and I'm talking ancient CPU engineering theory, Booleans, in space-squeezed systems, are often encoded many into one byte, and the bitwise operator... is operating on a bit, but it's a boolean. C very slightly extended this concept with assigning truth to 0 and nothing else. You can bitwise NOT in C from true to false and back. My point simply being that on the implementation level, there is no difference except booleans are not countable.

Hm I didn't know that about the ^ symbol.

It probably shows how old I am but CS did not always view it this way, and the entire functional programming community definitely opposes this view, that a function should be a black box. A procedure call does not produce a return so it can't cause side effects in the code its inside. I think that if a procedure has no return, ie it's not a function, pointers allow programmers to break this principle and it does not belong with non-returning functions. With methods, pointers can be used in combination with receiver in that way, but let's not forget that even in this case, passing pointers in the parameters raises the issue of nil variables that have to be tested before they can be used, as if they are nil there is nothing to resolve and nothing can happen.

Sure, it gives you the ability to use nil as an error condition but then what is the error interface for? I have ideas about how it could be improved, so that all variables have an error value implicitly, and it would help reduce the boilerplate of error handling a lot more than the numerous confusing schemes that make sense to people deeply embedded in try/catch syntaxes of java and other languages. Go definitely needs improvement here, that's a core resolution of the version bump.

I do understand pretty well that Go compiles so fast because its grammar is very linear, and enforces structure and this saves a lot of time parsing. So, yes, I wasn't really serious about removing func, but at the same time I wanted to point out that from the BNR form grammar perspective, the parsing looks simple, but that can hide complexity in the scanner. The reason, as I surmise it, for the uniform rule of top level blocks and lines all starting with a keyword, is that you can literally snip the source code into little pieces with an algorithm that a 4 year old could probably devise. Maybe I exaggerate a little but that's the point. It's fast because it's rigid and simple. I might even say that such a character of change is exactly what we DON'T want to be done for go 2. (I really hope the version upgrade won't cause a fork but hey, this is the teenies!)

I did not know that about function calls at top level. That also would be a very good reason to not remove the func :)

My ideas are quite off the wall, I know, but I think that such discussions are very helpful for clarifying the criteria for making the decision about what is in, what remains and what is taken out. The acute problem of Go at this time is pretty mild really, to be honest it's not that big a chore to do error handling, and I alluded to the fact that one can extend the error interface and effectively implement implicit error status. I have used this extensively in my own code though it was easier to figure out how to do it with simpler types (in this case, simply byte slices, with or without MMU fences around them to stop snooping. But with more complex types it became harder to do.

But man, that error handling library I wrote ( https://github.com/parallelcointeam/duo/blob/master/pkg/core/status.go ) as I have progressed implementing more parts of my application, I have tended to forget as it was weeks ago I first wrote that code, but a few days back as I was writing some higher level stuff I plugged the State type into it and a whole heap of bugs I was encountering because of error handling just evaporated.

I don't know if anyone else has proposed making implicit errors states for every type - there is only one case one might want to relax this - mainly it would be embedded programming with tight resources, which isn't a Go target platform anyway, but also you might not want the overhead when you are implementing mathematically intensive functions. But for that, I think my inclination would be towards that built-ins don't have it, and make a really simple way to put them on them. Or maybe better still, and actually I am more inclined even towards saying this: Go's error interface needs to be revamped. The use of a pointer to the error state string itself requires you to sometimes check for nil, but that's habit for anyone who's been using go for a while.

But what that little set of functions above (there is an interface in the same package in interfaces.go) Especially the 'SetStatusIf()' and 'OK()' methods, they make error handling almost blissful. I think that they could skip the major version bump if error handling is the main problem being addressed, just by changing the stdlib, to look something like my code.

@deanveloper
Copy link

I don't currently have time to read the whole thing, but in response to the following:

What's the point of a switch case for a fallthrough?

First of all, your statement is incorrect.

If condition1 is true and condition2 is false, then both statement and statement2 run in the first example, but in the second one, statement and other statement run.

Putting that aside, removing fallthrough vastly increases in complexity as more cases are added. Consider this:

switch {
case days > 0:
	str += days;
	if days == 1 {
		str += "day, "
	} else {
		str += "days, "
	}
	fallthrough
case hours > 0:
	str += hours;
	if days == 1 {
		str += "hr, "
	} else {
		str += "hours, "
	}
	fallthrough
default:
	str += minutes;
	if days == 1 {
		str += "min"
	} else {
		str += "mins"
	}
}

That was originally written in JS, so there may have been some errors when translating. There's not really an easy way to translate that to if/else statements without deep nesting or lots of code repetition

@l0k18
Copy link
Author

l0k18 commented Oct 19, 2018

It is not difficult to convert between if and switch blocks. if you change to case, remove the final parenthesis and put a colon after the condition.

Also, what is the point of using those fallthroughs in that code. This is how I would write it:

if days > 0 {
    suffix := "day"
    if days > 1 {
        suffix = "s"
    }
    str += fmt.Sprint(days, suffix, ", ")
}
if hours > 0 {
    suffix := "r"
    if days > 1 {
        suffix = "hours"
    }
    str += fmt.Sprint(days, suffix, ", ")
}
str += fmt.Sprint(minutes, " ")
suffix := "min"
if minutes >1 {
    suffix += "s"
}
str += suffix

Yes, very badly translated because Go does not allow you to use the + operator (or +=) with different types on each side. I fixed it for you.

Now, you see, there is arguments both sides for whether in switch blocks fallthrough should be implicit or explicit. The thing is that the switch block has a very very readable syntax. I don't know what word would suit but having to devote a whole extra line, and a big long word, makes the if version more concise. It is of course a matter of preference which form you like.

The only reason they are comparable is because unlike the case, which uses a sort of label variant structure, does not force either a new line, OR a final empty bracket on its own. Single statement instead of only a block after if statements would move the benefit way in favour of if.

But the main reason I disagree with fallthrough not being implicit is because you can put them after a non-fallthrough, and then at the end the condition isn't tested. This can cause the most squirrelly kinds of bugs. If cases were implicitly fallthrough you would use cases instead of if. But then you have the problem, and likely reason for the implicit breakout, of having to add another line for each exclusive case.

So, in fact, implicit fallthrough would help you avoid this kind of mistake, would allow you to use switch instead of if, and get its nice readable syntax, and make it very clear when reading it that each case evaluates unless you escape the block. And you can purge 'fallthrough' from the parser completely, and the syntax of break would perfectly fit.

Then your code would look like this:

switch {
case days > 0:
    suffix := "day"
    if days > 1 {
        suffix += "s"
    }
    str += fmt.Sprint(days, suffix, ", ")
case hours > 0:
    suffix := "hour"
    if days > 1 {
        suffix = "s"
    }
    str += fmt.Sprint(days, suffix, ", ")
default:
    str += fmt.Sprint(minutes, " ")
    suffix = "min"
    if minutes >1 {
        suffix += "s"
    }
    str += suffix
}

@deanveloper
Copy link

deanveloper commented Oct 19, 2018

Your if/else is an incorrect solution. Notice that if there is more than one day, then the hours and minutes should be printed no matter what, even if they are zero. Don't ask me why it works like that, the client wanted the duration formatted that way. It is actually quite hard to design the code this way with if-else, unless you're willing to goto.

So, in fact, implicit fallthrough would help you avoid this kind of mistake

Implicit fallthroughs also silently fail in a lot of other areas. Consider the following extremely common usage of switch, with implicit fallthroughs:

switch state {
case ErrState:
    icon = "error.png"
    break
case BusyState:
    icon = "busy.png"
case SuccessState:
    icon = "success.png"
    break
}

Notice that the BusyState fails silently and icon would end up being success.png rather than busy.png. Being a TA in CS classes, these kinds of errors happen a lot. The thing is, when you want fallthrough behavior, you know you want fallthrough behavior and consciously think about it. When you want to handle several cases, it's not intuitive to break after each case. It's much more intuitive, and less bug-prone, to have explicit fallthroughs.

@ianlancetaylor
Copy link
Contributor

Thanks. This is a collection of different ideas with different effects on the language. This would have been interesting at an earlier stage of the development of the language, but for most of these ideas it is too late. At this point these ideas don't seem to solve real problems that are getting in people's way, they are more a different style of doing things that can already be done. Making any such change would incur a bunch of work for little benefit.

@l0k18
Copy link
Author

l0k18 commented Nov 28, 2018

I basically agree :) For anyone who has already mastered most of Go there really isn't any sense of irritation about the limitations as the capabilities that are there are more flexible anyway.

The bigger problem is about getting people to write good code. Even some of the best golang projects have silly things in them. I'd guess that for those sooner or later tools will exist to make it easy, and right now, the only issue is it can sometimes be exhausting to fix badly formed code. But having said that, for example, adding a second, slightly different RPC endpoint to btcd (different only regarding mining block templates) took me about 3 hours, and required changes to about 10 files.

@golang golang locked and limited conversation to collaborators Nov 28, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests

4 participants