5 ms·
Nice! For anyone wondering, Go 1.25 won't be adding any actual language features. It's a minor release. Maybe we get sum types by 1.30 :)
by kubb 2y ago
Nice! For anyone wondering, Go 1.25 won't be adding any actual language features. It's a minor release.
Maybe we get sum types by 1.30 :)
- qaq 2y agoHonestly that's basically my last item on the go wish list :)
- ashishb 2y agoI wish we can have immutable runtime objects (e.g. final in Java or const in C++). Go has constant (`const`) type that can be evaluated at compile-time but no const type for something that can only be evaluated at Run time.
- blibble 2y agofinal in java only makes the primitive (or reference) immutable not the object
- ashishb 2y agoClass Rect { final int x, y; //Constructor goes here } Rect r1 = new Rect(1,2); Is this not sufficient to create an immutable rectangle that I can pass around safely in a multi-threaded code?
- maleldil 2y agoWhen x and y are primitives, yes. But if they're objects, final only prevents you from re-assigning them. You could still call methods that mutate them.
- loginatnine 2y agoUnless the object is immutable, like String, Integer, Long, ImmutableCollections, etc. Or your own immutable objects.
- ashishb 2y ago> Unless the object is immutable, like String, Integer, Long, ImmutableCollections, etc. Or your own immutable objects. Exactly. You can have immutable primitives. You can have immutable classes. And you can combine them to form thread-safe immutable classes.
- maleldil 2y agoNow you're restricting yourself to a subset of the language. There's nothing stopping someone from adding a mutable member to your "immutable" type later. It's much better to have immutable bindings/references so that nothing that mutates the object can be done through them. Rust does it very well, for example. Even C++ has a good version of this.
- ashishb 2y ago> It's much better to have immutable bindings/references so that nothing that mutates the object can be done through them. Rust does it very well, for example. Even C++ has a good version of this. Yes, that might be superior but even Java is doing better than Go here.
- pie_flavor 2y agoNon-deeply-immutable reference fields are a joke and solve no problems. They don't save you from needing to make defensive copies to stop shared mutation, and everything else is a padlock on your left pocket to stop your right hand reaching in.
- jcelerier 2y agoI can't count how many times adding `const` in c++ saved me from myself, I do t understand why you would ever not want it. You even say it: "padlock on your left pocket to stop your right hand reaching in" Do you trust yourself to write perfect code 100% of the time ? No? Then padlock it is
- stouset 2y agoI think they’re saying it doesn’t go far enough, but it was hard for me to parse the sentiment.
- Olreich 2y agoConst only saves you most of the time if you have a “const” all the way down the type structure. If I have a type Foo with a field that is a pointer to a mutable value, instantiating a Const Foo just means I’m always pointing at the same mutable value, not that I have an unchangeable Foo. Const support in languages never makes all modifications to data accessed through the variable locked out, just the top level, which makes it much more difficult to ensure that the assumptions about immutability hold without constantly doing deep copies or having to double and triple check that your Const definitions are correct. Const often leads to a false sense of security.
- jcelerier 2y ago> If I have a type Foo with a field that is a pointer to a mutable value, instantiating a Const Foo just means I’m always pointing at the same mutable value, not that I have an unchangeable Foo. yes and that's fine ? for instance how would you encode a graph operation where you want the graph structure to be immutable and the content of the nodes to be variable?
- drdaeman 2y agoI'm afraid that'll only result in some people using those inappropriately then other people using `reflect` to mutate those objects. Just like with "private" methods and fields when library authors create a good library but don't expose something crucial for anther person's task at hand.
- XorNot 2y agoMan I can't remember whichever black magic I invoked to bust into some C# sealed classes for some office application. But I was glad it existed because it enabled a whole set of valuable business automation at the time.
- School-Cotton 2y agoNothing in any programming language I’m aware of prevents you from opening /proc/<pid>/mem and changing the state arbitrarily. I don’t think we should avoid adding features to programming languages because a malicious coworker might work around them.
- int_19h 2y agoAll they need to do at this point is allow type union interfaces to be used as regular types and not just generic type constraints.
- kubb 2y agoAnd modify the switch statement to be able to ensure that all variants are handled.
- carlmr 2y agoExactly, since messing around with OCaml/F#/Rust I feel like sum-types + exhaustive match (with deconstruction) is such a powerful construct to prevent logic errors and ensure maintainability, that every language without it feels so limited.
- valenterry 2y agoYep, one of the reasons I could never be happy with Go. Union types and type-classes are of similar importance. Any modern language not having them (or an equal powerful feature) just feels sad.
- greenavocado 2y agoGo doesn't have direct sum types, but we can use interfaces and type switches: package main import "fmt" // Define types for our "sum type" type Success struct { Value string } type Error struct { Message string } // Interface for our sum type type Result interface { isResult() } // Implement the interface func (s Success) isResult() {} func (e Error) isResult() {} // Pattern matching using type switch func handleResult(r Result) string { switch v := r.(type) { case Success: return fmt.Sprintf("Success: %s", v.Value) case Error: return fmt.Sprintf("Error: %s", v.Message) default: // Go requires a default case, which can help catch new types panic("Unhandled result type") } } func main() { result := Success{Value: "Operation completed"} fmt.Println(handleResult(result)) } Here is a practical example of it: package main import ( "fmt" "time" ) // Common interface for our "sum type" type Notification interface { Send() string isNotification() // marker method } // Email notification type EmailNotification struct { To string Subject string Body string } func (n EmailNotification) Send() string { return fmt.Sprintf("Email sent to %s with subject '%s'", n.To, n.Subject) } func (EmailNotification) isNotification() {} // SMS notification type SMSNotification struct { PhoneNumber string Message string } func (n SMSNotification) Send() string { return fmt.Sprintf("SMS sent to %s", n.PhoneNumber) } func (SMSNotification) isNotification() {} // Push notification type PushNotification struct { DeviceToken string Title string Message string ExpiresAt time.Time } func (n PushNotification) Send() string { return fmt.Sprintf("Push notification sent to device %s", n.DeviceToken) } func (PushNotification) isNotification() {} // Function that handles different notification types func ProcessNotification(notification Notification) { // Type switch for pattern matching switch n := notification.(type) { case EmailNotification: fmt.Printf("Processing Email: %s\n", n.Send()) fmt.Printf("Email details - To: %s, Subject: %s\n", n.To, n.Subject) case SMSNotification: fmt.Printf("Processing SMS: %s\n", n.Send()) fmt.Printf("SMS length: %d characters\n", len(n.Message)) case PushNotification: fmt.Printf("Processing Push: %s\n", n.Send()) timeToExpiry := time.Until(n.ExpiresAt) fmt.Printf("Push expires in: %v\n", timeToExpiry) default: // This catches any future notification types that we haven't handled fmt.Println("Unknown notification type") } } // Function to record notifications in different ways based on type func LogNotification(notification Notification) string { timestamp := time.Now().Format(time.RFC3339) switch n := notification.(type) { case EmailNotification: return fmt.Sprintf("[%s] EMAIL: To=%s Subject=%s", timestamp, n.To, n.Subject) case SMSNotification: return fmt.Sprintf("[%s] SMS: To=%s", timestamp, n.PhoneNumber) case PushNotification: return fmt.Sprintf("[%s] PUSH: Device=%s Title=%s ExpiresAt=%s", timestamp, n.DeviceToken, n.Title, n.ExpiresAt.Format(time.RFC3339)) default: return fmt.Sprintf("[%s] UNKNOWN notification type", timestamp) } } func main() { // Create different notification types email := EmailNotification{ To: "user@example.com", Subject: "Important Update", Body: "Hello, this is an important update about your account.", } sms := SMSNotification{ PhoneNumber: "+1234567890", Message: "Your verification code is 123456", } push := PushNotification{ DeviceToken: "device-token-abc123", Title: "New Message", Message: "You have a new message from a friend", ExpiresAt: time.Now().Add(24 * time.Hour), } // Process notifications fmt.Println("=== Processing Notifications ===") ProcessNotification(email) fmt.Println() ProcessNotification(sms) fmt.Println() ProcessNotification(push) // Log notifications fmt.Println("\n=== Logging Notifications ===") fmt.Println(LogNotification(email)) fmt.Println(LogNotification(sms)) fmt.Println(LogNotification(push)) // We can also store different notification types in a slice notifications := []Notification{email, sms, push} fmt.Println("\n=== Processing Notification Queue ===") for i, notification := range notifications { fmt.Printf("Item %d: %s\n", i+1, LogNotification(notification)) } } The marker method pattern isNotification() prevents other types that happen to have a Send() method from being considered notifications. T
- ajb 2y agoThere's a proposal[1] by Ian Lance Taylor of the go team, but it includes every sum type implicitly including nil. Which very much not what you'd expect of a sum type, but every interface being zeroable seems to be embedded quite deeply in the language. [1]https://github.com/golang/go/issues/57644 https://github.com/golang/go/issues/57644
- thiht 2y agoEvery proposal they make start with something absurd, gets backslash in the comment, and after a few iterations it ends up with a lovely proposal. I almost suspect they do it on purpose to make sure each significant proposal gets enough engagement
- ncruces 2y agoSum types in Go need to have a zero value, if it's not nil, it needs to be something else. There's nothing purposely ridiculous about it.
- valenterry 2y agoIt is ridiculous. Nulls were not called "the billion dollar mistake" in Java for no reason.
- int_19h 2y agoThing is, it's not an easy problem to solve while retaining the simplicity. It means that now you need to have rules in the language for definitely-initialized fields (since you can't just default-init them to null as there is no null). Which means that you need full-fledged constructors for everything, and syntax for invoking them in all cases where they may be needed - e.g. think about what should happen if you try to create an array of structs with ref fields in them. This is all solvable - C++ and Rust both do it, for example. But it introduces a lot of complexity to the language.
- 2y ago
- karmakaze 2y agoFor some reason, sum types is something that rarely gets done right. Most languages settle for something close and users defend their choice/Stockholm syndrome.