Maybe we get sum types by 1.30 :)
Maybe we get sum types by 1.30 :)
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.
not the object
//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?
Exactly. You can have immutable primitives. You can have immutable classes. And you can combine them to form thread-safe immutable classes.
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.
Do you trust yourself to write perfect code 100% of the time ? No? Then padlock it is
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.
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?
But I was glad it existed because it enabled a whole set of valuable business automation at the time.
This is all solvable - C++ and Rust both do it, for example. But it introduces a lot of complexity to the language.
FWIW my personal opinion is that the resulting complexity is necessary, and we as engineers just have to bite the bullet and deal with that mental overhead (and if it means that coding becomes too complicated for some, so be it). But the market ultimately decides; and it decided in favor of lots of code that's cheap and fast to write even at the expense of bugs, so we have tools catering specifically to that.
Rust can do this with less complexity by embracing choices that, while conceptually simple, are very unorthodox and unintuitive, such as everything being a move rather than a copy with few exceptions (whereas in pretty much every other PL it has always been the other way around, if move semantics is supported at all).
I wrote that, after 15 years of baggage, there's nothing purposely ridiculous about stating that Go sum types must have a zero value (either nil or something else).
Either Go sum types have a zero value; or they can't be used everywhere a type can be used in Go; or you're radically and backwards incompatibly changing the language.
But adding sumtypes in 2025 and using a default nil or zero value/type to it, yeah that I do call ridiculous and I stand by it.
It might be a good decision even (I don't know), but it's still ridiculous by my understanding of that word.
Additionally, for it to fit the 2025 implementation of the runtime, its representation in memory must have a fixed size, with fixed locations for any pointers, and the memory representation of the zero value must be zero.
The runtime can change, but for the runtime to change to accommodate new concepts, the change can't obliterate the expectations of 15 years of existing code.
Given those restrictions, you can either not have sum types (which is the current state of affairs, and will be for the foreseeable future), or you can pick a zero value for your sum types.
If you find having a zero value for sum types ridiculous, you're simply rejecting sum types in Go, which is fine. There is, after all, a reason the proposal wasn't accepted.
Otherwise, we're all happy to accept suggestions that meet the criteria of not breaking compatibility or wreaking havoc with existing code.
Requiring a value to be provided at usage/definition site would go a long way. Also, of course, having to provide a default value by hand when initializing an array. Go also has easy-to-use callbacks, so having also APIs that take a callback returning a sentinel/default value should be easy enough. The problem isn't having defaults, but having pervasive defaults that are set in stone by the language itself, even in places where it doesn't make sense.
For low-level code that fiddles with uninitialized memory buffers and allocators, things might get complicated, if zero values are not allowed. (Rust's struggles around `MaybeUninit` is a poster child example of the ensuing complexity.) However, I think that a very Go-style solution would have been that the type system allows for zero-initialized types, but everything around the provided APIs and language semantics makes creating them hard. It's a similar solution with "our strings are UTF-8, but we don't check and world doesn't explode if they are not". I generally dislike this kind of "worse is better" design, but it certainly fits Go very well.
The main problem with Go, arguably, would be what to do with null pointers. They could have gone with separate reference types for "definitely not null" and "may be null", with a specialized if-like check-and-coerce-operation, without having full generics and sum types like Rust does.
Because Go has a GC and thus is capable of having arbitrary object graphs, there shouldn't be a problem of initializing multiple values with a reference to the same object, so user providing a default value and the API cloning it to fill the array should work. And in case of a "may be null" ref, having it to be null is not a problem.
You don't need rules, you just don't allow for uninitialized fields.
> Which means that you need full-fledged constructors for everything, and syntax for invoking them in all cases where they may be needed
Rust doesn't have constructors. It's not needed.
> - e.g. think about what should happen if you try to create an array of structs with ref fields in them.
struct Ref<'a> {
x: &'a i32,
}
fn main() {
let a = 5;
let array = [
Ref { x: &a },
Ref { x: &a },
];
}
No big deal.This all being said, that doesn't mean that I think zero values are a mistake in Go, exactly. They make sense with the design of the language. But I do think both sides of this have tradeoffs, and I personally prefer the tradeoffs of no nulls and zero values.
It sounds simple, but it's the kind of thing that has very far-reaching effects across the language. If you start with this premise and then design everything else around it, things feel natural. If you take a language already designed around the notion of null values and bolt things on, either idiomatic code changes massively, or you leave enough loopholes in the type system to still let people write the stuff they have always done and that always worked (even though it's technically unsound).
And yes, of course it's a tradeoff. For my own part, I also think that null values and the simplicity that comes with them in some things aren't worth the trouble that they bring. I'm also not a fan of Go, to put it mildly. But given where the language is already, and given their explicit stated design goals (which I disagree with), I can totally see why on this particular issue they went with nulls just to keep things simple that were traditionally simple.
I fully agree, which I tried to say at the end. All I was saying is that you don't need all of the stuff you're talking about to make this work.
[1] https://www.infoq.com/news/2019/07/go-try-proposal-rejected/
Not just every interface, every single type. And it’s at the very core of the language. For any type T you can name, you can write
var v T
And it’ll give you a v you can interact with, no opt out. You can barely opt out of implicit shallow copies via hacks based around lock method names.Most languages settle for something close and users defend their choice/Stockholm syndrome.
Union types and type-classes are of similar importance. Any modern language not having them (or an equal powerful feature) just feels sad.
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. TIdeally sum types would be concrete/non-nilable somehow.
type NotificationWrapper struct {
// This field holds the actual notification data
Value interface{}
}
And use it like: func NewPushNotification(token, title, message string, expires time.Time) NotificationWrapper {
return NotificationWrapper{
Value: PushNotification{
DeviceToken: token,
Title: title,
Message: message,
ExpiresAt: expires,
},
}
}
And destructure it with something like func ProcessNotification(notification NotificationWrapper) string {
switch n := notification.Value.(type) {
Roll it all up in a nice syntax with a preprocessor haha type NotificationWrapper struct {
// This field holds the actual notification data
value interface{}
}
Within `ProcessNotification` you'd also want to always assert the struct is initialized. You have to handle that error case via `err` or `panic`.With a true sum type, you could be assured that the value is always concrete, removing the need for error handling, which otherwise complicates the business logic of your application. Errors that should be safeguarded against by the compiler become run time checks, eating up cycles on the CPU, or paniced upon, potentially leading to segfaults which can only be caught by tests (not asserted valid via the act of compiling).