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

[RFC] many: add types.Option generic and use in customizations #860

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

mvo5
Copy link
Contributor

@mvo5 mvo5 commented Aug 14, 2024

For customizations we currently use pointers when we want to distinguish between set or unset value. This works fine and is supported by json/toml marshaling natively. However we lose some safety because there is always the risk of dereferencing a pointer and extra checks for nil.

This PR introduces a new types.Option that wraps the primitive types bool,int,string [0] so that they are there is a distinction between "set" (Some) and "unset" (None) [1]. This allows us to do things like:

-       if hostname := c.GetHostname(); hostname != nil {
-               osc.Hostname = *hostname
-       } else {
-               osc.Hostname = "localhost.localdomain"
-       }
+       osc.Hostname = c.GetHostname().TakeOr("localhost.localdomain")

or

-       if hostname := c.GetHostname(); hostname != nil {
-               osc.Hostname = *hostname
-       }
+       osc.Hostname = c.GetHostname().Unwrap()

And there is overall a much smaller risk of making mistakes while also be more explicit (well, minor as arguably is as * is also a strong indicator).

This PR converts a small subset to show what it would look like, it's a lot of busywork so maybe not worth the effort but I like it over the pointers (but I understand if other wouldn't).

[0] Sadly the go type system and toml make it hard to do the same for structs which would be extremely nice, see the commit message/code comments for details.
[1] Of course this is very much inspired by the rust std::Some type and the go-optional libraries.


// Option is a data type that must be Some (i.e. having a value) or None (i.e. doesn't have a value).
// This type implements database/sql/driver.Valuer and database/sql.Scanner.
type Option[T OptionTomlTypes] []T
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fwiw, we could call this Optional too which I actually slightly prefer but then Option is what rust uses and lots of people are familar with this terminology now.

)

// Some is a function to make an Option type value with the actual value.
func Some[T OptionTomlTypes](v T) Option[T] {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fwiw, we could call this differently, e.g. types.NewOptional("foo") or somesuch but again rust is the inspiration and it seems nice to keep close to their terminology as many people will be familiar with it.

supakeen
supakeen previously approved these changes Aug 15, 2024
Copy link
Member

@supakeen supakeen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I quite like this though I don't think it gets us much benefit in Go (there's no nice syntax and stdlib to deal with options) there's also no real downside.

It's nice being able to see at the calling side of the code that we're handling an option and not a random pointer.

@@ -39,18 +40,18 @@ func NewUsersStageOptions(userCustomizations []users.User, omitKey bool) (*Users
users := make(map[string]UsersStageOptionsUser, len(userCustomizations))
for _, uc := range userCustomizations {
// Don't hash empty passwords, set to nil to lock account
if uc.Password != nil && len(*uc.Password) == 0 {
if len(uc.Password.Unwrap()) == 0 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is different behavior from the Rust we're trying to emulate where .unwrap() would panic if uc.Password is not Some.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I took this from the upstream go-optional lib, but happy to change and call it e.g. "UnwrapWithDefault" or "TakeWithDefault" or just "Take" to make this clearer. It convinient but I agree that for rusians it might be confusing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does it do in this library if the value is None? Still panic?

This is heavily inspired by https://github.com/moznion/go-optional
with is super nice. However we cannot use it as is because it
lacks support for toml unmarshal and go makes it really hard to
extend existing types. Submiting toml unmarshal upstream would
also be an option but it's difficult in the general case because
the toml unmarshal interface is very limited for complex types
(it only passes `data any` with the full decoded complex struct
into map[string]inteface{} for example which leaves it to the
user of the library to convert to the actual type struct which
is a pain with reflect). I really whish go was more expressive
here.

So this commit defines a subset of https://github.com/moznion/go-optional
in our own internal types lib.
It seems we are currently using *bool a lot to distinguish between
{unset,true,false}. This commit instead uses the new `interal/types`
library to distinguish between unset/undefined and the value.

It uses generics to works on all primitive toml type.
@mvo5 mvo5 force-pushed the go-optional-less-ptr-internal branch from 23e0f8b to 07741d4 Compare September 10, 2024 13:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants