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

Enum value names (avoiding collisions). #8

Closed
dmitshur opened this issue Jun 21, 2017 · 8 comments
Closed

Enum value names (avoiding collisions). #8

dmitshur opened this issue Jun 21, 2017 · 8 comments

Comments

@dmitshur
Copy link
Member

dmitshur commented Jun 21, 2017

As discovered in #7, the initial schema I had in mind for the enums will not work, because of name collision between enum values of different types:

// IssueState represents the possible states of an issue.
type IssueState string

// The possible states of an issue.
const (
	Open   IssueState = "OPEN"   // An issue that is still open.
	Closed IssueState = "CLOSED" // An issue that has been closed.
)

// PullRequestState represents the possible states of a pull request.
type PullRequestState string

// The possible states of a pull request.
const (
	Open   PullRequestState = "OPEN"   // A pull request that is still open.
	Closed PullRequestState = "CLOSED" // A pull request that has been closed without being merged.
	Merged PullRequestState = "MERGED" // A pull request that has been closed by being merged.
)

// ProjectState represents state of the project; either 'open' or 'closed'.
type ProjectState string

// State of the project; either 'open' or 'closed'.
const (
	Open   ProjectState = "OPEN"   // The project is open.
	Closed ProjectState = "CLOSED" // The project is closed.
)

...
# github.com/shurcooL/githubql
./enum.go:71: CreatedAt redeclared in this block
	previous declaration at ./enum.go:17
./enum.go:111: Open redeclared in this block
	previous declaration at ./enum.go:8
./enum.go:112: Closed redeclared in this block
	previous declaration at ./enum.go:9
./enum.go:120: CreatedAt redeclared in this block
	previous declaration at ./enum.go:71
./enum.go:121: UpdatedAt redeclared in this block
	previous declaration at ./enum.go:18
./enum.go:149: CreatedAt redeclared in this block
	previous declaration at ./enum.go:120
./enum.go:150: UpdatedAt redeclared in this block
	previous declaration at ./enum.go:121
./enum.go:152: Name redeclared in this block
	previous declaration at ./enum.go:19
./enum.go:170: Open redeclared in this block
	previous declaration at ./enum.go:111
./enum.go:171: Closed redeclared in this block
	previous declaration at ./enum.go:112
./enum.go:171: too many errors

Solutions

These are the solutions that I've got so far and are up for consideration.

Solution 1

Prepend the type name in front of the enum value name, to ensure each identifier is unique and collisions are not possible:

 package githubql
 
 // ReactionContent represents emojis that can be attached to Issues, Pull Requests and Comments.
 type ReactionContent string
 
 // Emojis that can be attached to Issues, Pull Requests and Comments.
 const (
-	ThumbsUp   ReactionContent = "THUMBS_UP"   // Represents the 👍 emoji.
-	ThumbsDown ReactionContent = "THUMBS_DOWN" // Represents the 👎 emoji.
-	Laugh      ReactionContent = "LAUGH"       // Represents the 😄 emoji.
-	Hooray     ReactionContent = "HOORAY"      // Represents the 🎉 emoji.
-	Confused   ReactionContent = "CONFUSED"    // Represents the 😕 emoji.
-	Heart      ReactionContent = "HEART"       // Represents the ❤️ emoji.
+	ReactionContentThumbsUp   ReactionContent = "THUMBS_UP"   // Represents the 👍 emoji.
+	ReactionContentThumbsDown ReactionContent = "THUMBS_DOWN" // Represents the 👎 emoji.
+	ReactionContentLaugh      ReactionContent = "LAUGH"       // Represents the 😄 emoji.
+	ReactionContentHooray     ReactionContent = "HOORAY"      // Represents the 🎉 emoji.
+	ReactionContentConfused   ReactionContent = "CONFUSED"    // Represents the 😕 emoji.
+	ReactionContentHeart      ReactionContent = "HEART"       // Represents the ❤️ emoji.
 )

Usage becomes:

input := githubql.AddReactionInput{
	SubjectID: q.Repository.Issue.ID,
	Content:   githubql.ReactionContentThumbsUp,
}

This is very simple, guaranteed to not have collisions. But the enum value identifiers can become quite verbose, and less readable.

Solution 2

This is a minor variation of solution 1. The idea is to make the enum values slightly more readable by separating the type name and enum value by a middot-like character ۰ (U+06F0).

 package githubql
 
 // ReactionContent represents emojis that can be attached to Issues, Pull Requests and Comments.
 type ReactionContent string
 
 // Emojis that can be attached to Issues, Pull Requests and Comments.
 const (
-	ThumbsUp   ReactionContent = "THUMBS_UP"   // Represents the 👍 emoji.
-	ThumbsDown ReactionContent = "THUMBS_DOWN" // Represents the 👎 emoji.
-	Laugh      ReactionContent = "LAUGH"       // Represents the 😄 emoji.
-	Hooray     ReactionContent = "HOORAY"      // Represents the 🎉 emoji.
-	Confused   ReactionContent = "CONFUSED"    // Represents the 😕 emoji.
-	Heart      ReactionContent = "HEART"       // Represents the ❤️ emoji.
+	ReactionContent۰ThumbsUp   ReactionContent = "THUMBS_UP"   // Represents the 👍 emoji.
+	ReactionContent۰ThumbsDown ReactionContent = "THUMBS_DOWN" // Represents the 👎 emoji.
+	ReactionContent۰Laugh      ReactionContent = "LAUGH"       // Represents the 😄 emoji.
+	ReactionContent۰Hooray     ReactionContent = "HOORAY"      // Represents the 🎉 emoji.
+	ReactionContent۰Confused   ReactionContent = "CONFUSED"    // Represents the 😕 emoji.
+	ReactionContent۰Heart      ReactionContent = "HEART"       // Represents the ❤️ emoji.
 )

Usage becomes:

input := githubql.AddReactionInput{
	SubjectID: q.Repository.Issue.ID,
	Content:   githubql.ReactionContent۰ThumbsUp,
}

The unicode character is not easy to type, but easy to achieve via autocompletion:

image

Solution 3

This is also a variation of solution 1. The idea, suggested to me by @ScottMansfield (thanks!), is to use an initialism of the type name rather than the full name. This makes the identifier names shorter, but still has a risk of there being name collisions if two different types with same initialism have same enum values.

 package githubql
 
 // ReactionContent represents emojis that can be attached to Issues, Pull Requests and Comments.
 type ReactionContent string
 
 // Emojis that can be attached to Issues, Pull Requests and Comments.
 const (
-	ThumbsUp   ReactionContent = "THUMBS_UP"   // Represents the 👍 emoji.
-	ThumbsDown ReactionContent = "THUMBS_DOWN" // Represents the 👎 emoji.
-	Laugh      ReactionContent = "LAUGH"       // Represents the 😄 emoji.
-	Hooray     ReactionContent = "HOORAY"      // Represents the 🎉 emoji.
-	Confused   ReactionContent = "CONFUSED"    // Represents the 😕 emoji.
-	Heart      ReactionContent = "HEART"       // Represents the ❤️ emoji.
+	RCThumbsUp   ReactionContent = "THUMBS_UP"   // Represents the 👍 emoji.
+	RCThumbsDown ReactionContent = "THUMBS_DOWN" // Represents the 👎 emoji.
+	RCLaugh      ReactionContent = "LAUGH"       // Represents the 😄 emoji.
+	RCHooray     ReactionContent = "HOORAY"      // Represents the 🎉 emoji.
+	RCConfused   ReactionContent = "CONFUSED"    // Represents the 😕 emoji.
+	RCHeart      ReactionContent = "HEART"       // Represents the ❤️ emoji.
 )

Usage becomes:

input := githubql.AddReactionInput{
	SubjectID: q.Repository.Issue.ID,
	Content:   githubql.RCThumbsUp,
}

Unfortunately, this approach led to a collision with the existing enums in GitHub GraphQL API:

const ROFCreatedAt ReactionOrderField = "CREATED_AT" // Allows ordering a list of reactions by when they were created.

const ROFCreatedAt RepositoryOrderField = "CREATED_AT" // Order repositories by creation time.
./enum.go:149: ROFCreatedAt redeclared in this block
	previous declaration at ./enum.go:71

It's possible to detect when a collision would occur, and use something more specific that doesn't collide, instead of the initialism. But that introduces complexity, and inconsistency/unpredictability in the naming.

Solution 4

This idea is somewhat related to solution 2, but instead of a hard-to-type character, it becomes a normal dot selector. The idea is to create a separate package for each enum type, and define its values in that package. The enum types are still in main githubql package.

package githubql
 
// ReactionContent represents emojis that can be attached to Issues, Pull Requests and Comments.
type ReactionContent string
// Package reactioncontent contains enum values of githubql.ReactionContent type.
package reactioncontent

import "github.com/shurcooL/githubql"
 
// Emojis that can be attached to Issues, Pull Requests and Comments.
const (
	ThumbsUp   githubql.ReactionContent = "THUMBS_UP"   // Represents the 👍 emoji.
	ThumbsDown githubql.ReactionContent = "THUMBS_DOWN" // Represents the 👎 emoji.
	Laugh      githubql.ReactionContent = "LAUGH"       // Represents the 😄 emoji.
	Hooray     githubql.ReactionContent = "HOORAY"      // Represents the 🎉 emoji.
	Confused   githubql.ReactionContent = "CONFUSED"    // Represents the 😕 emoji.
	Heart      githubql.ReactionContent = "HEART"       // Represents the ❤️ emoji.
)

Usage becomes:

input := githubql.AddReactionInput{
	SubjectID: q.Repository.Issue.ID,
	Content:   reactioncontent.ThumbsUp,
}

The dot character is easy to type, and the code is very readable once written. But this will require importing many new small packages when using enum values. A tool like goimports will make that significantly easier, but it may still be problematic. Also, the documentation will be split up into multiple small packages, which may be harder to read.

Conclusion

All solutions considered so far seem to have certain upsides and downsides. I'm not seeing one solution that is clearly superior to all others. I'm considering going with solution 1 or 2 to begin with, and be open to revisit this decision.

@gmlewis
Copy link
Collaborator

gmlewis commented Jun 21, 2017

Thanks for the detailed descriptions. Here are my $0.02:

I like Solution 4 the best, as it seems like the cleanest one to me.
My second-favorite is Solution 3, with the downside of inconsistent initialisms.
My 3rd-favorite is Solution 1, with the downside of Java-like (long) names.
My least favorite is Solution 2, as getting those unicode dots typed can be a pain and frustrating.

@bradleyfalzon
Copy link

bradleyfalzon commented Jun 21, 2017

I'm not a fan of Solution 4 for two reasons:

  1. Introduces a risk of import cycles, even though githubql may never import reactioncontent, this pattern may be used for other types where import cycles could be a problem.

  2. Although the design shouldn't be determined by the limitations of editors/IDEs, many (but not all: goglang) can only autocomplete for imported packages.

Although Solution 4 is similar to how stripe-go structures itself, so I don't think it's an unusual solution.

I'm not a fan of Solution 3, mostly because the initialisms may have a high chance of collisions, is PStateOpen Project Open or does P mean something else? I believe my reservations on this are similar to @gmlewis's, or I'd also agree with him on the inconsistencies.

Solution 2 (unicode dot) I don't think really brings too much more value over Solution 1, and also it could be difficult to type (although, one could argue it could be a different symbol).

Therefore I think Solution 1 is my favourite, autocomplete works (as the package is probably being imported already), there's no import cycle risk, it's easily discoverable reading docs (godoc.org separates constant blocks well) and just searching works for "reaction" or "thumbs".

Edit: By import cycles risk, I mean, if githubql needs to return one of these types, perhaps in a response or in a helper. For example, you query for an issue the issue has a thumbs up reaction, githubql can't return the type reactioncontent.ThumbsUp because reactioncontent uses githubql.ReactionContent. However, I'm not certain this would be a problem though.

@tallstoat
Copy link

I'd go for 3 if I know the different types of enums (and their possible values) upfront, which I guess should be the case here to a large extent. Next (or rather a safer approach though verbose) would be 1. Last option would be 4.

I wouldn't go anywhere near 2; that road is best avoided.

@alicebob
Copy link

Thanks for the interesting talk at the Berlin meetup this evening!

This is another option for these constants. I would likely not choose it, but I still want to mention it.

package main

var Reactions = struct {
    ThumbsUp, ThumbsDown, Laugh string
}{
    ThumbsUp:   "THUMBS_UP",
    ThumbsDown: "THUMBS_DOWN",
    Laugh:      "LAUGH",
}

func main() {
    print(Reactions.Laugh)
}

If you want to add an extra layer of nesting you can't use the anonymous structs anymore:

package main

type Reaction struct {
    ThumbsUp, ThumbsDown, Laugh string
}
type Colors struct {
    Red, Blue, Green string
}
type ConstsT struct {
    Reaction Reaction
    Colors   Colors
}

var Consts = ConstsT{
    Reaction: Reaction{
        ThumbsUp:   "THUMBS_UP",
        ThumbsDown: "THUMBS_DOWN",
        Laugh:      "LAUGH",
    },
    Colors: Colors{
        Red:   "RED",
        Blue:  "BLUE",
        Green: "GREEN",
    },
}

func main() {
    print(Consts.Reaction.Laugh)
}

@dmitshur
Copy link
Member Author

dmitshur commented Jul 2, 2017

@alicebob, thanks for that suggestion. It's an interesting alternative that I haven't thought of, so it's good to be aware of it.

It has some benefits, and feels most similar to option 2, except without the need for hard-to-type unicode middots. However, it has some flaws, like the fact the enum values are no longer consts, and the name collision between the containing struct name and the enum type.

@dmitshur
Copy link
Member Author

dmitshur commented Jul 2, 2017

I've been thinking about this issue for a while, and I've decided to go with Solution 1 as a resolution for now, but be open to revisit this issue later. I need to resolve the issue so I can make progress on implementing next missing features.

Solution 1 is the simplest, most predictable, and least controversial option. Its only downfall are the verbose enum value identifiers, but it suffers from least other problems.

Since the API of the library is not frozen yet, I think it's a good step to make, and if we can find an even better next step to make, we still can. In fact, I won't close the issue for now, we can keep it open and keep thinking.

dmitshur added a commit that referenced this issue Jul 2, 2017
Go with solution 1, as described in #8. Solution 1 is the simplest,
most predictable, and least controversial option. Its only downfall
are the verbose enum value identifiers, but it suffers from least
other problems.

Since the API of the library is not frozen yet, if another solution
is found which is determined to be better overall, we can still
improve this.

Regenerate with:

	go generate ./...

Updates #8.
Follows #7.
@dmitshur
Copy link
Member Author

dmitshur commented Sep 1, 2017

Some time has passed, and I want to follow up on this.

I've been using our current solution (Solution 1) and continuing to really like it. It's very simple, and feels good to use. The absolutely only downside of slightly higher than expected verbosity pales in comparison to the downsides of the other more involved alternative solutions we've considered above.

One property of the current naming I appreciate is that typing the enum name makes it easy to see what are the possible values via code completion:

image

It'd be helpful if others who have had a chance to use it posted their thoughts too.

However, at this point, I suspect the chance of another solution being discovered which would beat our current simple solution is extremely unlikely. (Of course, if it happens, we can still consider implementing it.) I'll wait some more, and then close this issue.

@dmitshur
Copy link
Member Author

I'll wait some more, and then close this issue.

I've waited, nothing has come up. Closing now because this issue is considered resolved. (Can always be reopened if something new comes up.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

No branches or pull requests

5 participants