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: user-defined equality and copying #30557

Closed
henryas opened this issue Mar 4, 2019 · 6 comments
Closed

proposal: Go 2: user-defined equality and copying #30557

henryas opened this issue Mar 4, 2019 · 6 comments
Labels
FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Milestone

Comments

@henryas
Copy link

henryas commented Mar 4, 2019

Overview

User-defined data types may require custom equality and copy mechanism. It would be great if there is a way to override the equality operator (==) and the copy mechanism.

Equality

Upon encountering the equality operator ==, the compiler will check whether the object implements the following method:

func Equal(other interface{}) bool

If the object implements the method, the method will be used for comparison. Otherwise, it will default to the standard type comparison.

Copying

Upon encountering the type copying mechanism, whether it is struct re-assignment or passed by value or by any other means I haven't thought about,

var one MyStruct
two := one //struct re-assignment
CallFunction(one) //passed by value

the compiler will check whether the object implements the following method:

func Clone() interface{}

If it implements the method, the method will be called to copy the object. Otherwise, it performs shallow copying.

Benefits

Readability

Equality comparison is one of the fundamental operations that one may perform to a type. The default equality comparison is inadequate to cover all cases, especially for complex user-defined data types. Each library ends up implementing its own way to allow user-defined equality. For instance, gomock uses Matches method. My own internal library uses Equal method. By unifying these approaches into a standard way of doing things, it improves readability. Readers would know what to expect and where to look for equality comparison.

It also allows uniformity. Instead of seeing something like this:

//... some code
return a.itemA == b.itemA && 
     a.itemB.Equal(b.itemB) && 
     itemC.SameAs(itemD)

it becomes something like this

//... some code
return a.itemA == b.itemA && 
     a.itemB == b.itemB && 
     itemC == itemD

Scalability

One of the benefits of using Go is that it is a very forgiving language to design myopia. It is a common situation to not being able to see the whole picture when starting a project. It is only later that you realize you make mistakes in earlier design. Go allows code refactoring with minimal changes to existing code. I believe this proposal will enhance this aspect of Go.

In my projects, I often make changes to existing types. What was previously adequate with the default equality comparison (==), is now no longer valid. Hunting down every uses of this == operator in order to replace it with a custom equality method is tedious and error prone. With this proposal, one can simply implements the Equal method and all existing comparison using the == operator will work as intended. There will no longer be a need to replace every instance of == operator on the type.

The same goes with the copying operation, and it is often more tedious with the copying operation.

No Copying

Some types may be designed to not allow copying. For instance, it may contain a mutex. By implementing the Clone method and make it panics, we can prevent accidental copying the given type.

Backward Compatibility

The only potential problem is when there is a method with the same name as the method override (Equal and Clone) but with a different signature. The compiler will complain about method redeclaration as Go does not support method overloading. Otherwise, I don't think there is any compatibility problem with existing code. Users who already use compatible Equal or Clone method can continue to call it directly.

Let me know what you think.

Thanks.

Henry

@gopherbot gopherbot added this to the Proposal milestone Mar 4, 2019
@mvdan
Copy link
Member

mvdan commented Mar 4, 2019

This seems like a subset of operator overloading; see #27605. It would probably be best if you added to that discussion instead of opening another proposal.

@dpinela
Copy link
Contributor

dpinela commented Mar 4, 2019

The copying method is essentially the same that was proposed in #21604.

@gopherbot gopherbot added v2 A language change or incompatible library change LanguageChange labels Mar 4, 2019
@ianlancetaylor ianlancetaylor changed the title proposal: User-defined Equality and Copying proposal: Go 2: user-defined equality and copying Mar 4, 2019
@ianlancetaylor
Copy link
Contributor

Note that the current implementation relies on stack copying. If a type with a Clone method is stored on the stack, the method would have to be called while copying the stack, which is unlikely to work. But I guess that it would be possible to declare that types with Clone methods may never be stored on the stack.

Note that the language currently does not treat any user defined methods as special. This proposed change would be the first case in which that is done. Even the Error method is only special in that it permits satisfying the language-defined error interface. (One minor exception, not really in the language proper, is that when crashing due to an unrecovered call to panic, the runtime will call the Error or String methods, if available, when printing the value.)

I agree with the above that this is similar to #21604 (which was closed) and #27605.

@henryas
Copy link
Author

henryas commented Mar 5, 2019

Thank you for bringing up other proposals #21604 and #27605 which I wasn't aware of. In fact, one of them is my old proposal.

I am not sure whether my current proposal should be closed and I should join the discussion in those other proposals. While there are some overlapping areas, those proposals aren't the same as this proposal.

#27605

#27605 proposes operator overloading, but does not mention cloning/copying at all. The listed must overload operators are mathematical operators, such as +, -, *, /, etc. The equality operator (==) falls under a maybe.

In my opinion, mathematical operator overloading isn't critical. You may have defined a type such as

type Money int64

and uses it all over the place such as ..

var sum Money = 10000
sum += 5000

When later you change Money into this:

type Money struct {
    CurrencySymbol string
    Amount int64
}

The existing code with mathematical operators will just break and the compiler will refuse to compile. This allows you to conveniently fix the affected code. The same cannot be said with equality operators.

Let's take the Money example above, and it is used all over the place such as ..

if myMoney == herMoney {
    //do something
}

and later we changed money into something like..

type Money struct {
    CurrencySymbol string
    Amount int64
    ExchangeRate int64 //exchange rate should be ignored in equality comparison. 
        //JPY 10000 should be equal to JPY 10000 regardless of their original purchase cost.
} 

The existing code will still compile, but it will introduce a subtle bug. Hunting down the affected code isn't trivial. Searching for every occurrence of == operator is ridiculous.

Hence, in my opinion, #27605 is more about ergonomics, while this proposal is more about the language's flexibility when dealing with design myopia and refactoring.

In addition, I don't agree with having too many special methods to overload these operators, as this will take away good method names from the users. #27605 lists way too many operators to overload. Hence, in this proposal, I limit those to only Equal and Clone. #27605 does not mention Copy/Clone (perhaps because there is no specific operator for copying?).

#21604

#21604 is my old proposal. I had forgotten all about it. #21604 discussed about constructor overloading, copy/clone override, and type inheritance.

The copy/clone override part is essentially the same as this proposal's copy/clone suggestion. #21604 does not mention user-defined equality override, which I think is important. After an extra one year of experience using Go, #21604's constructor and inheritance proposals aren't probably that important.

Closing

The idea for this proposal came up from my own work. Having to search through every equality comparison and copying mechanism every time there is a change in data types that affect their equality/copying mechanism, is tedious and error prone. No IDE is smart enough to detect those. So, some sort of fundamental language support would be really nice.

In Java and C#, you can just override the base object's Equal method and be done with it. In Go, there is no base object and no inheritance. In Go, if you have the foresight, in a new project, you may have provided the Equal and Clone methods and warned in the comment that the users should use the provided methods instead of the built-in operators. Still, this isn't elegant and somebody may not read the documentation.

Now what if you haven't had the foresight and there are new requirements (eg. new features) to an existing codebase? Now you have to refactor, replacing those existing == and copy mechanism with your custom equal/copy methods. I did that in my work and there are still subtle bugs that creep months later. It gets even hard to detect when it is buried deep inside your business logic (when some decisions have to be made based on the equality comparison).

@networkimprov
Copy link

I support the idea of limiting copying allocation (many user-defined types are invalid on copy) but not redefining it via implicit copy constructor.

I'm drafting a proposal re default initializers on user-defined types, for both original & copying allocation.

BTW, these are the cases of copying allocation; assignment is a different case:

c := v
func(c T){}(v)
[]T{v}
append([]T{}, v)
[1]T{v}
struct{c T}{v}
map[K]T{k: v}
m[k] = v /*new k*/

@ianlancetaylor
Copy link
Contributor

Although this proposal is simpler than #21604, the same counter-arguments apply. We are not going to make this change.

@golang golang locked and limited conversation to collaborators Mar 25, 2020
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

6 participants