Representing Optional/Variant Types Idiomatically (or not)

292 views
Skip to first unread message

Joshua

unread,
Jun 17, 2021, 3:43:07 AM6/17/21
to golang-nuts
Hello,

I have 2 questions about idiomatic ways to represent optional values,
and variant values in types.

1) I'm modelling a data type which has a field which may or may not be
   there, would most Gophers reach for a pointer here, and use `nil' to
   represent a missing value?

2) Another field of this type contains an "Action", the variants of
   which hold different information. In nonsense code, I want this:

   ,----
   | type EmailAction struct {
   |      Summary string
   |      Decsription string
   |      To string
   |      Attendee person
   |      }
   | 
   | type DisplayAction struct {
   |    Description string
   | 
   |    }
   | 
   | type Event struct {
   |    OptionalField *MyType
   |    Action EmailAction OR DisplayAction
   |    }
   `----


How would you go about modelling this type in Go?  Or alternatively, do
you think trying to use "Type Driven Design" like this is not a good fit
for Go, and rather I should just have a generic set of components and
validate when functions call it.

Personally I prefer the previous, I really like the idea of knowing a
type can't exist in an invalid state! Helps me not have to keep track of
so much in my head.

Axel Wagner

unread,
Jun 17, 2021, 4:02:48 AM6/17/21
to Joshua, golang-nuts
On Thu, Jun 17, 2021 at 9:43 AM Joshua <joshua.o...@gmail.com> wrote:
Hello,

I have 2 questions about idiomatic ways to represent optional values,
and variant values in types.

1) I'm modelling a data type which has a field which may or may not be
   there, would most Gophers reach for a pointer here, and use `nil' to
   represent a missing value?

I don't know about "most Gophers" (it's hard to generalize), but I would.
 

2) Another field of this type contains an "Action", the variants of
   which hold different information. In nonsense code, I want this:

   ,----
   | type EmailAction struct {
   |      Summary string
   |      Decsription string
   |      To string
   |      Attendee person
   |      }
   | 
   | type DisplayAction struct {
   |    Description string
   | 
   |    }
   | 
   | type Event struct {
   |    OptionalField *MyType
   |    Action EmailAction OR DisplayAction
   |    }
   `----


How would you go about modelling this type in Go?

Either using an interface, or by having two pointer-fields and only setting one.
Depending on the details, it might also be worth considering inverting the relationship:

type Event struct {
    OptionalField *MyType
    …
}

type EmailEvent struct {
    Event
    Summary string
    …
}

type DisplayEvent struct {
    Event
    Description string
    …
}
 
  Or alternatively, do
you think trying to use "Type Driven Design" like this is not a good fit
for Go, and rather I should just have a generic set of components and
validate when functions call it.

Personally I prefer the previous, I really like the idea of knowing a
type can't exist in an invalid state! Helps me not have to keep track of
so much in my head.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/8ed722d4-9111-4e23-aa97-f6601f964f0cn%40googlegroups.com.

Amit Saha

unread,
Jun 17, 2021, 4:06:03 AM6/17/21
to Joshua, golang-nuts
I will share my thinking for (2):

You could do something like this:

type Action interface {
        # your methods
}

type Event struct {
        OptionalField *MyType
        Action field
}

In your code:

e := Event{..}
Switch e.field.(type):
  case “EmailAction”: do somehting
  case “DisplayAction”: do something else
  default: Invalid action

Jan Mercl

unread,
Jun 17, 2021, 4:20:47 AM6/17/21
to Joshua, golang-nuts
On Thu, Jun 17, 2021 at 9:43 AM Joshua <joshua.o...@gmail.com> wrote:

> 1) I'm modelling a data type which has a field which may or may not be
> there, would most Gophers reach for a pointer here, and use `nil' to
> represent a missing value?

That's the usual approach seen in the wild and IMO often the wrong one.

Unless the size of the field's type is big, I'd suggest just a plain
field and a boolean value that represents the "present/valid"
information.

Less GC pressure, improved cache locality.

Brian Candler

unread,
Jun 17, 2021, 8:12:39 AM6/17/21
to golang-nuts
On Thursday, 17 June 2021 at 09:02:48 UTC+1 axel.wa...@googlemail.com wrote:

How would you go about modelling this type in Go?

Either using an interface, or by having two pointer-fields and only setting one.

I'd just add for clarity: in Go an interface value is also something inherently "nillable", so lets you test for the presence/absence of a value, like a pointer does.

If you do this, then you can switch on the type of the value - so you don't need to keep a separate field for "type".  But probably the most persuasive argument is if your actions share some common behaviour, such as "Perform this action" or "Describe this action".  Those sit very nicely as methods on the interface.

A benefit of pointers is that it's very easy to serialize and deserialize as an optional field (to JSON, say), without any extra work.  You can also do it with interfaces, and you get more control, but you need to do some plumbing.  This looks like a nice overview: http://gregtrowbridge.com/golang-json-serialization-with-interfaces/

Joshua

unread,
Jun 17, 2021, 2:28:00 PM6/17/21
to golang-nuts
Thanks a lot for the thoughts everyone, I will take all your suggestions under advisement.

Jan:
Thanks for the tips for performance, however with this project I'm not particularly trying to squeeze as much performance as possible, and I prefer the "safety" of the otherway.
I mean safe in that, with your suggestion you would have the "nonsense" value still accessible, even if I really shouldn't touch it because the bool is "false".
Maybe it's me being lazy, but I much prefer the compiler not letting me mess with something that I shouldn't (I'm very jealous of Rust's "container" Mutexes).

Bakul Shah

unread,
Jun 17, 2021, 3:45:36 PM6/17/21
to Jan Mercl, Joshua, golang-nuts
On Jun 17, 2021, at 1:20 AM, Jan Mercl <0xj...@gmail.com> wrote:
This can get error prone as you may forget to update the validity field
and the error may not be caught easily. And painful if you have more
than one optional field. Now you need to invent more names or use
longer names (foo_valid). The pointer alternative will fail more obviously
if you mess up!

>
> Less GC pressure, improved cache locality.
>
> --
> You received this message because you are subscribed to the Google Groups "golang-nuts" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/CAA40n-XGP2edWe1b13kpE4PhdFdCsJWGk79z8ngEpj19ycwmxQ%40mail.gmail.com.

Henry

unread,
Jun 19, 2021, 10:59:15 PM6/19/21
to golang-nuts
Instead of exposing the fields in a struct like that, you may consider using constructor function to have better control over what the user can set into the struct. That way you don't need to worry about the user setting invalid values. For instance,
```Go
type EmailEvent struct{
  id string
  date time.Time 
  description string 
}

func NewEmailEvent(id string, date time.Time, description string) EmailEvent {
  return EmailEvent{id,date,description}
}

func NewEmailEventByDescription(description string) EmailEvent {
  return EmailEvent{
    id: randomID(), //validate or set the default value for optional or missing fields
    date: time.Now(),
    description: description,
  }
}
```
Or you can getter and setter methods instead of constructors.
```Go
func (e *EmailEvent) SetID(id string) {
   //validate id
   if id == "" {
      //do something
   }
}
```
Or you can set the missing value upon use.
```
func (e EmailEvent) Write(writer io.Writer) {
  if e.id == "" {
    e.id = randomID()
  }
  //do processing here
}
```
About type variant, the common way is to use interface as others have suggested since it is easily extensible. Alternatively, you may merge them into one struct. This is less flexible but may be useful when you are dealing with small type variants. For example:
```Go
type EventType int 
const (
   Undefined EventType = iota
   DisplayEvent 
   MailEvent
)

type Event struct {
   from string 
   to string 
   description string
   eventType EventType
}

func NewDisplayEvent(desc string) Event {
  return Event {
     description:desc,
     eventType: DisplayEvent,
  }
}

func NewMailEvent(from, to, desc string) Event {
  return Event {
    from: from,
    to: to,
    description: desc,
    eventType: MailEvent,
  }
}

//implement your getters here
```
I hope this helps.
Reply all
Reply to author
Forward
0 new messages