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: add match statement #44022

Closed
vit1251 opened this issue Jan 30, 2021 · 29 comments
Closed

proposal: Go 2: add match statement #44022

vit1251 opened this issue Jan 30, 2021 · 29 comments
Labels
Milestone

Comments

@vit1251
Copy link

vit1251 commented Jan 30, 2021

The current state

Using "switch" provide torn syntax, reuse parent scope, may process part cases.

Problem statement

Source code with "switch" provide simple case processing.

Proposal

Using match statemtnt without destructurization:

Example:

var number int = 5
var result string = match number {

    0 => {
       return "Zero"
    }

    10 => {
       return "Positive";
    }

    _ => {
       return "Other";
    }

}

There we have new variable scope, ability to checking all variants is checks, return value, more uniform syntax.

Using match expression:


func main() {

    var index int = 5
    var ok bool = true
    
    value := match index, ok {
        0, true => {
             return "Zero"
        }

        -100, false => {
           return "Ice"
       }

       100, true =>
           return "Hell"
       }

       _, _ => {
            return "Unknown state"
       }

    }

@gopherbot gopherbot added this to the Proposal milestone Jan 30, 2021
@vit1251 vit1251 changed the title proposal: match statment proposal: match statement Jan 30, 2021
@seankhliao seankhliao added v2 A language change or incompatible library change LanguageChange labels Jan 30, 2021
@seankhliao
Copy link
Member

Please fill out https://github.com/golang/proposal/blob/master/go2-language-changes.md when proposing language changes

@vit1251
Copy link
Author

vit1251 commented Jan 30, 2021

@seankhliao ok.

Would you consider yourself a novice, intermediate, or experienced Go programmer?

I really like the simplicity and powerful of Go, but the ability crash on "null pointer" is push me away.

Right now I want use Golang and want make more usually for me language items liek protection in language
more caring about memory protection and etc.

What other languages do you have experience with?

I meet with Assembler ( Intel ), Ada, Basic, C, C--, C++, Delphi, ECMAScript, Java, Lisp, Lua, Modula, Pascal, PHP, PostSript, Prolog, Python, Rust, Swift, SQL, TeX, Vala.

Would this change make Go easier or harder to learn, and why?

I think that match is similar to understand like switch.
But actually only research may actually answer on this question.

Has this idea, or one like it, been proposed before?

I think it could be discusses somewher, but quick search does not provide such a proposal.

Who does this proposal help, and why?

The flow control syntax can be used by all developers in the language and no restrictions.

What is the proposed change?

Add match statement and provide more compact control flow syntax.

Please describe as precisely as possible the change to the language.

Original post provide two source code example.
I welcome disscussion about patern match it may provide disadvantage this statement.

What would change in the language spec?

I think this may take article about flow control constructs will add new one statment
and golang source code.

Please also describe the change informally, as in a class teaching Go.

If you want to process more complex case processing, then you can use the match statemnt expression.
Let's look at smart home processing hub code example:

type struct Event {
    Source string
    Value int
}

for {
    event := <- homeBridge
    match (event.Source, event.Value) {

        "iot.Temperature", x => {
             // ... disable heating system ...
        }

        "camera.Activity", trushold => {
            match (trushold) {
                0 .. 5 => {
                      alarm.Stop()
                      camera.StopRecording()
                }
                5 .. 10 => {
                    match (secureMode) {
                       true => {
                          alarm.Start()
                      }
                      false => {
                          camera.StartRecording()
                      }
                },
            }
       }

       unknownEventName, _ => {
            log.Printf("Unhandle event %s", unknownEventName)
       }

    }

Is this change backward compatible?

Yes.

Breaking the Go 1 compatibility guarantee is a large cost and requires a large benefit.

N/A

Show example code before and after the change.

See original message.

What is the cost of this proposal? (Every language change has a cost).

I'm not a pro at estimating the cost of language changes.

How many tools (such as vet, gopls, gofmt, goimports, etc.) would be affected?

At the option each one of developer. Use match pattern is optional construction.

What is the compile time cost?

Every match want to use additional variable scope, every pattern is want to have local variable name.
Actually it may take some additional time. I assume some like O(N) where N is match cases.

What is the run time cost?

Every match case amy take a more than one comparision and may take
new stack and push one or more variable name.

Can you describe a possible implementation?

No, sorry.

Do you have a prototype? (This is not required.)

No, sorry.

How would the language spec change?

Actually I think you add some additional PEG grammar and make AST processing.
No idea actually.

Orthogonality: how does this change interact or overlap with existing features?

No overlapping.

Is the goal of this change a performance improvement?

No in term of computing.
Yes in term devloper time.

Does this affect error handling?

Yep. match may catch unhandled variant / cases.

Is this about generics?

No.

@ianlancetaylor ianlancetaylor changed the title proposal: match statement proposal: Go 2: add match statement Jan 31, 2021
@ianlancetaylor
Copy link
Contributor

Can you write down the precise semantics that you are proposing?

Why introduce a brand new syntax like => when we already have case?

Why require a brace-enclosed block when the switch statement does not work that way?

What are the exact semantics of ..? What happens when multiple .. cases overlap?

More generally, what big advantage do we get from match that we can't already write with switch?

@vit1251
Copy link
Author

vit1251 commented Jan 31, 2021

@ianlancetaylor response as follow:

Can you write down the precise semantics that you are proposing?

You asking pattern match statement EBNF?
I actually not ready to write it now but some like that:

control_flow_statement :== if_statement | switch_statement | match_statement 
match_statement :== "match" SP pattern_match_expression SP { pattern_match_cases }
pattern_match_expression :== variable_name | expression
pattern_match_case :== pattern SP "=>" SP code_block
pattern :== new_variable_name | constant | ?expression?

Why introduce a brand new syntax like => when we already have case?

At match body we should separate the pattern from the lambda function being executed and here you need some kind of separator in switch you use keywork "case".

Main problem case on my option in Python syntax and Assembler symantics with non clear internal logic (i.e. using break in C and falltruth keyword in Golang). Brace-enclosed provide strict execution range and provide make "match" retun value. I does not know if it possible write some like:

var stable boolean = switch {
    case version % 2:
        return true;
    default:
        return fasle;
};

Why require a brace-enclosed block when the switch statement does not work that way?

Brace-enclosed provide human intuituve and well known variable scope and provide execution block bounds.
For switch you should provide additional information about variable scope (how about variable created in case)
and flow behavior (i.e. provide breake and falltruth keyword).

switch ... {
    case ...:
          var number int = 5
          falltruth
    case ...:
         // How about .. number here ...
}

It make switch more complcated language unit and match look like more simple to understanding.

What are the exact semantics of ..? What happens when multiple .. cases overlap?

Actually I think that ".." is advanced math feature. Let's talk about this idea later.

More generally, what big advantage do we get from match that we can't already write with switch?

I suspect that switch statement may cased only one arguemnt, which requires building a lot of forest for switch checks. Of course, you can refactor the code and create more and more related functions with switch, but it create more and more line of code.

For example, in original message check structure two variable at same time.

@seankhliao
Copy link
Member

See also #40353 for multivalue switch

@ianlancetaylor
Copy link
Contributor

Can you write down the precise semantics that you are proposing?

You asking pattern match statement EBNF?

No, that is the syntax. I'm asking for the precise semantics. What precisely does it mean?

@ianlancetaylor
Copy link
Contributor

Brace-enclosed provide human intuituve and well known variable scope and provide execution block bounds. For switch you should provide additional information about variable scope (how about variable created in case) and flow behavior (i.e. provide breake and falltruth keyword).

I'm sorry, but this is not a good argument. The switch statement works a certain way. Perhaps you don't like the way it works, but it exists, it does the job, and people are accustomed to it. Changes to the language mst correspond to the existing language. We are not going to introduce a completely different syntax for something that is very similar to a switch statement.

@vit1251
Copy link
Author

vit1251 commented Feb 1, 2021

@ianlancetaylor

We moved from the language syntax advantages of match structure (explicit scoping, parallel case handling, more explicit execution block) to political reasons (historical). This is a completely different conversation in new mindset space.

I think that world already have more spartan language, remember the Assembler with direct cmp and jmp and I don't see much desire to use it today. I know that Golang community declare newcome welcome (i.e. learning simplicity) and at the same time have a unclear structure in the language guarded by historical reasons. Today is a great moment to annonce new language structure and Go 2 sounds like revolution as Python 2 and 3, Perl 5 and 6 and other large language changes.

Another part of you argument is classical ad hominem political argument.

@beoran
Copy link

beoran commented Feb 1, 2021

Sorry to jump in, but I think some misunderstanding is brewing here. so let's stay calm and focus at the issue at hand.

I think what @vit1251 is proposing is the classic "pattern matching" feature which is present in many contemporary programming languages, often even in conjunction with a switch statement. For example Switch has both switch and match statements, (https://docs.swift.org/swift-book/ReferenceManual/Patterns.html), while C# has extended their if and switch statements to support pattern matching (https://docs.microsoft.com/en-us/dotnet/csharp/pattern-matching).

In these examples, the switch and the pattern match feature are significantly different, the one is not another way to write the other. So, when asking for what semantics are being proposed, we can look at these existing implementations of pattern matching and consider which of those could have semantics that are for inclusion in Go. Syntactical considerations come in later.

I am also jumping in because pattern matching is quite a popular programming feature nowadays, and I think we should at least consider whether or not they should be added to Go and if not, why, and perhaps even add a FAQ to that extent. Pattern match semantics might also help solving certain issues around type switches and generics, or type switches and the proposed union types. With a pattern match we could statically match not only an exact type but also similar types that match the pattern.

@DeedleFake
Copy link

@beoran

How is that different from type assertions and type switches? In C#, there's an is that can be used with an if, similar to in Kotlin, but in Go you can do

if v, ok := v.(someType); ok {
  // No magic required.
}

Similarly, you can use a type switch for pretty much the same thing but with multiple branches:

switch v := v.(type) {
case int:
  // In this case, v is an int.
  println(v + 2)
case fmt.Stringer:
  // Works fine with interfaces automatically, too.
  println(v.String())
default:
  // This works just fine, too.
  doSomething(v)
}

@ianlancetaylor
Copy link
Contributor

@vit1251 I am not making an ad hominem argument. Also, citing the historical development of the language is not making a political argument.

For better or for worse, it is extremely unlikely that we Go will ever significantly break backward compatibility. See https://go.googlesource.com/proposal/+/refs/heads/master/design/28221-go2-transitions.md .

@vit1251
Copy link
Author

vit1251 commented Feb 2, 2021

@ianlancetaylor

I am not making an ad hominem argument.

Your write "Perhaps you don't like the way it works, ... people are accustomed to it". I see here "you" and "like" and "people" as result I feel that argument to my personal preferences between people. You weight my preferences with historical reasons. It argument definitely ad hominem. To be more precise, here is a variant: Argumentum ad populum ( ... most people walk street on red signal and it right ).

Citing the historical development of the language is not making a political argument.

Please watch on argument "... by hisorical reasons" from another side. Today we have a large count of experience by resolve errors in past ( .. by definition of expirience ). But people contain a an amazing feature to continue improves. Once we were young and did not know how to walk, but today we runing and jumping. Is the historical argument constituted an excuse for stay as children?

For better or for worse, it is extremely unlikely that we Go will ever significantly break backward compatibility ...

I don't quite understand why you talk about breaking backward compatibility. I talk about implement match as new one structure like threnary operator and you may already save "switch" as improve option for "if" and they all overlaps semantics. If "switch" little better than "if" why you prevent to resolve "switch" problems in new one construction?

Resume: I understand your argument as most people are used to the flaws of "switch", but in the next big promise to improve the language these problems will not be solved or improves.

@beoran
Copy link

beoran commented Feb 2, 2021

Introducing a new keyword breaks backward compatibility. Match as a new key word is not possible because some people are now using the word "match" as a variable name. But there are other ways to get match semantics. It would be possible to enhance the current switch statement with the semantics you want, for examples by using two key words in a way that is not allowed now (e.g., "switch range") in such a way that it is backwards compatible, or perhaps with built-in functions.
Which brings us to the important question: which semantics should this feature have?

@DeedleFake, Pattern matching generally allows not to match a single type but also ranges of types, sometimes with wildcards (looking as some functional languages). Especially when using a type switch in conjunction with generic types, this could be handy.

@ianlancetaylor
Copy link
Contributor

@vit1251 Thanks for the reply. I won't continue the exchange as it seems unlikely to be productive.

Let me just say two things as clearly as I can.

First, if we add a new language construct that is similar to switch, it must use a syntax that is similar to switch. That means using the case keyword rather than introducing a new => token. It also means not introducing additional curly braces that are not present in switch.

Second, you must document the precise semantics of the new language construct. You have not done so.

Thanks.

@vit1251
Copy link
Author

vit1251 commented Feb 2, 2021

@ianlancetaylor

if we add a new language construct that is similar to switch ...

Perhaps this is another kind of unsolved problem - impossibility to extending. Computer language may provide some mechanisms for extending and enable addtional language features. Example:

  • Python - from __future__ import print
  • Python - # -- encoding: utf-8 --
  • Node - --experimental-loader=module
  • Node - --experimental-modules
  • C++ - extern "C" { }

If you have motivation to solve, then you could be found solution.

it must use a syntax that is similar to switch ...

In this case does not resolving problem with variable scope and syntax overload.

you must document the precise semantics of the new language construct ...

At the initial stage, we encountered a gap that there is simply no way to expand the language with new constructions: keywords and arrows.

Let's again watch simple example:


var original bool = true
var number int = 5

var result bool =
    match original, number {

      false, 1 => {
          return true; 
      }

      true, _ => {
          var newNumber int = 5;
          return false;
      }

      _, _ => {
          return false;
      }

    };

An result boolean variable initialize by pattern match expression and step-by-step check with pattern until first match.
Once pattern is match we execute code block and return value in variable result.

In this case newNumber variable scope implicitly braced after pattern declaration "true, _".

When you want to implement some sort "falltruth" behavior it possible by use underscore "_" as universal value.

Let's on first version look at this variant (without range ".." and another one advanced pattern match feature).

@DeedleFake
Copy link

DeedleFake commented Feb 3, 2021

This seems like a large number of changes, almost all of which introduce patterns that are completely different from any existing Go structures, for what is essentially a very light syntax sugar variant of a switch or an if-else chain. In particular, it introduces a case in which a return does not return from a function, it introduces a complex expression that can contain statements, it uses underscores in a way similar to being values, and it seems to handle an arbitrary number of comma-separated expressions. It introduces all of these things that don't fit the existing pattern of Go code in order to do something that can very easily be done with existing constructs.

@beoran
Copy link

beoran commented Feb 3, 2021

Hmmm, well if those are the semantics you want, you can already do that with a different syntax: https://play.golang.org/p/Q_iv2EGmB3Z

package main

import (
	"fmt"
)

func main() {
	var original bool = false
	var number int = 5
	var newNumber int = 0
	var result bool = func(b bool, i int) bool {
		switch {
		case !b && i == 1:
			return true
		case b:
			newNumber = 5
			return false
		default:
			return false
		}
	}(original, number)
	fmt.Printf("%d %d %v\n", number, newNumber, result)
}

Admittedly, the anonymous func is needed to introduce a new scope, and the call of that function with (original, number) is somewhat unclear because it after the body of the anonymous function. But there are other issues about defining anonymous functions more easily.

For this feature a bit more ambitious semantics would be needed, not just syntactic sugar.

@vit1251
Copy link
Author

vit1251 commented Feb 3, 2021

@beoran

You rewrite code to avoid intuitive unclean situation. Let's I return back more intuitive unclean situations in your code:

var original bool = false
var number int = 5
var newNumber int = 0

var result bool = func(b bool, i int) bool {
	switch {

	case !b && i == 1:
		return true

	case b:
		var newNumber int = 5
		falltruth

	case b == 3:
		newNumber = 10

	default:
		return false
	}
	}(original, number)
	fmt.Printf("%d %d %v\n", number, newNumber, result)
}

It is clear that you can always rewrite or refactor the source code in accordance with the rules of the language,
or even always use "if" for clean variable scope.

My point is switch create sub-scopes without brackets and it alient syntax (i.e. Pyhton syntax) in Golang in switch statement.

When we meet lexical brace in "switch {" we intuitive await new switch variable scope like we await it between "if !b {" and "}". But actually every case provide new one and in a "falltruth" situation it's continue worsen.

Let's little rewrite source code to mark scopes by lexical braces (i.e. since that time it stay more similar to proposal match design):

		switch {
			case !b && i == 1 {
				var newNumber int = 5
				return true
			}
		}

Actually does not matter switch or match and case or => we use when you return back lexical scope brackets switch stay overdesign at "if" construction and it create anoter one question about what mean bracket on switch it create new scope or provide group. For group in Golang in import use parentheses.

I talk about intuitive language design and most people right it sounds like sugar as an any language over Assembler.

@beoran
Copy link

beoran commented Feb 3, 2021

So your point is more about the syntax of the current switch statement? The switch syntax of Go was taken by and large from the C/C++/Java family of languages, but then enhanced with more capabilities in the cases. but yes, in Go, every case statement introduces a new scope for variables, and you consider that counter-intuitive because there are no braces to signal that?

It is still possible to use braces to do that, although the fallthrough statement will have to come after, a bit like this:

package main

import "fmt"

func main() {

	var original bool = false
	var number int = 5
	var newNumber int = 0

	var result bool = func(b bool, i int) bool {
		switch {

		case !b && i == 1: {
				return true
			}
		case b:	{
				newNumber = 5
			}
			fallthrough
		case i == 3: {
				newNumber = 10
			}
			fallthrough
		default:
			return false
		}
	}(original, number)

	fmt.Printf("%d %d %v\n", number, newNumber, result)
}

Actually, I think that on the level of the lexer/parser, allowing the syntax of starting the body of a case with a { in stead of a : and ending it with a } would certainly be possible, because it produces a syntax error now (syntax error: unexpected {, expecting :), so changing that would be backwards compatible. The matter though, is then if such a small syntax changes is worth the effort, especially for the Go programmers who will have to read this code, and now will find that there are two ways in which you can write a case statement in stead of one.

After 10 years of programming Go, the current Go syntax has become intuitive for me. It seems you are relatively new to Go, so I would say it takes some time to get used to, like anything new really.

@deanveloper
Copy link

Actually, I think that on the level of the lexer/parser, allowing the syntax of starting the body of a case with a { in stead of a : and ending it with a } would certainly be possible, because it produces a syntax error now (syntax error: unexpected {, expecting :), so changing that would be backwards compatible

This is because you are using an int. I can't think of any ambiguities myself, so it may still be possible. However it gets more complicated when you consider structs.

@griesemer
Copy link
Contributor

The proposal review committee appreciates the fact that the author prefers to use "flow-control syntax", but it's also straight-forward (if perhaps less elegant and less compact) to achieve the same result using existing Go mechanisms.

Any language addition needs to be considered in light of existing Go: not only should it (ideally) fit well with the existing language, but it also should provide a significant functional benefit not easily achieved (or impossible to achieve) with the existing language. The proposed change suggests a language feature that is quite different in style from what we have, and doesn't really provide a significant functional benefit. There's ways to write the equivalent code and those alternatives seem adequate. Finally, the fact that other languages provide mechanisms similar to the one suggested, or that other languages have a mechanism for extending the language does not mean that Go need to do the same.

Based on the discussion above, and the lack of strong support in the emoji voting, this is a likely decline. Leaving open for four weeks for final comments.

@ghost
Copy link

ghost commented Feb 13, 2021

I've been trying to see if someone has already talked about it but I can seem to find it, so please let me know if this has already been discussed!

I think one of the good things pointed out in this match example, is not the match itself, but the fact that you can assign its value to a variable. I think it would be useful to have if and switch return their value, like in functional languages, or languages that have the ?: operator.

I know the idea behind Go is to have a simple language that doesn't need to be copying others, but there are some really good functional practices that can shorten code by a lot, while also making it more clear.

For example:

var x = if someVar > 0 {
  "String A"
} else {
  "String B"
}

var z = switch someVar {
  case 1:
    "something"
  case 2:
    "something 2"
  default:
    "oops"
}

I know you could do this, which is very similar.

var x string
if someVar > 0 {
  x = "String A"
} else {
  x = "String B"
}

However this can become tedious in some cases where you have a switch with many case statements. You need constantly repeat yourself, and it gets too clobbered if your variable name is somewhat long. Even just having the braces is like too much in some cases, especially when you have many different binary if and you could write var x = if a > 1 then 1 else 0, making the overall code much easier to read.

I find this to be very practical in the languages that support it, and I don't think this would be a breaking change either, but rather just an extension of if and switch.

This is not a must by any means and I can happily live without it, but I'd like to know if this has ever been a point of discussion.

Thanks!

@griesemer
Copy link
Contributor

@nferrario Your suggestion has come up before, at the very least in the form of a ":?" operator. It has been a point of discussion before. There are lots of constructs in other languages that may be very practical. That is not a strong reason for including such constructs in Go, especially if there are alternatives available that are almost equivalent.

@beoran
Copy link

beoran commented Feb 17, 2021

@nferrario As @griesemer says, there are alternatives available. Just to explain how, you can already do this at the cost of defining closures like this:

https://play.golang.org/p/Wgo_YIBkvBB

package main

import (
	"fmt"
)

func main() {
	someVar := 1
	z := func() string {
		switch someVar {
		case 1:
			return "something"
		case 2:
			return "something 2"
		default:
			return "oops"
		}
	}()
	x := func() string {
		if someVar > 0 {
			return "String A"
		} else {
			return "String B"
		}
	}()

	fmt.Printf("Hello, %s %s\n", x, z)
}

This example is a bit artificial, but it might be useful to do it like this, since it allows the closures to be factored out later as separate functions.

@ghost
Copy link

ghost commented Feb 17, 2021

Thank you @griesemer and @beoran. Yes I know we can easily solve it using closures and it's not a terrible solution either. It's just a little bit of code after all. But I don't also see a strong reason NOT to include such functional features. It's a nice to have that's useful, and it also avoids having a funcion call. I'm all in on keeping Go simple, I love the way it is and I wouldn't like it to become like other languages like Scala, where you have 5 different ways of writing the same thing. But I do believe we could be a little bit more flexible and allow/include more things. I've seen people saying they don't wanna use ?: because it can lead to more difficult to read code, and that's not true. You can have an equally unreadable code just with ifs.
Just like Generics is gonna help simplifying code and avoid having a bunch of casts on interface{}, I think we can come up with other ways of reducing the verbosity of Go code in some scenarios, without affecting its readability.

@beoran
Copy link

beoran commented Feb 17, 2021

@nferrario With generics, we can implement the ?: operator very easily ourselves as a generic function, if you excuse my use of Canadian Aboriginal Characters:
https://go2goplay.golang.org/p/wbueDIcvew_U

package main

import (
	"fmt"
)

func [T any](b bool, ifTrue T, ifFalse T) T {
	if b {
		return ifTrue
	}
	return ifFalse
}

func main() {
	i := 7
	v := (i > 5, "greater", "lesser")
	l := (i > 5, 5, i)
	fmt.Println("i > 5?", v, l)
}

In fact, I like this so much that on the 1st of April 🤡 I will propose to make ᕈ a build-in generic function , then we can from now on say that yes, we do have ?: in Go language 😉 .

Seriously though, if this would be in generics package the name would likely to be gen.if, but this example does go to show that generics will be powerful enough to handle the ?: use case. As for the match, I'll leave that as an exercise for the readers.

@ghost
Copy link

ghost commented Feb 17, 2021

@beoran lol no, that'd be terrible, no doubts! But that's a very good explanation since I wasn't aware we coule achieve such things with Generics. Thanks!

@earthboundkid
Copy link
Contributor

The usual name for is cond, eg https://gohugo.io/functions/cond/

@ianlancetaylor
Copy link
Contributor

No change in consensus since #44022 (comment).

@golang golang locked and limited conversation to collaborators Apr 6, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

9 participants