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

Best practices for use cases requiring nullary union members #928

Closed
lunaris opened this issue Oct 4, 2021 · 3 comments · Fixed by #980
Closed

Best practices for use cases requiring nullary union members #928

lunaris opened this issue Oct 4, 2021 · 3 comments · Fixed by #980
Labels
feature-request A feature should be added or improved.

Comments

@lunaris
Copy link
Contributor

lunaris commented Oct 4, 2021

Smithy's @enum trait supports use cases in which a value must be one of a known set:

@enum([
  {
    name: "FIRST",
    value: "FIRST",
  },
  {
    name: "SECOND",
    value: "SECOND",
  },
])
string MyEnum

which when generating TypeScript code (say) could produce:

type MyEnum = "FIRST" | "SECOND"

Smithy's union shapes support use cases in which a value must have a tag from a known set and some associated value (depending on the tag):

union MyUnion {
  first: String,
  second: Long
}

which again, when generating TypeScript could produce:

type MyUnion =
  | { type: "first"
    , value: string
    }
  | { type: "second"
    , value: bigint
    }

TypeScript (and other languages) can also model a hybrid of these two types, where some members/constructors have associated data (as in unions), some don't (as in enumerations), and some have optional values:

// In TypeScript
type AnotherUnion =
  | { type: "first"
    , value: string
    }
  | { type: "second"
    , value: bigint
    }
  | { type: "third"
    }
  | { type: "fourth"
    , value: number | null
    }
-- In Haskell
data AnotherUnion
  = First String
  | Second Integer
  | Third
  | Fourth (Maybe Double)

Here third is a nullary constructor -- it doesn't have an associated value, but it is distinct from first, second and fourth. fourth has a value which can be null/not present. How might we model this in Smithy? We can't do something like:

union AnotherUnion {
  @required
  first: String,

  @required
  second: Integer,

  third: Empty,
  fourth: Double
}

(where Empty is some dummy/to-be-ignored type e.g. structure Empty {}) since @required is banned inside unions (since unions cannot have null/not present values). Another option might be to use a trait, such as:

@sum([
  { name: "first", value: String },
  { name: "second", value: Integer },
  { name: "third" },
  { name: "fourth", value: Double, nullable: true },
])
union AnotherUnion {}

but this seems a shame since the fact that AnotherUnion is essentially a union is somewhat obscured (also empty unions are forbidden). Does anyone have any thoughts or recommendations on how to approach this? Perhaps this is simply not the way to model this problem? All feedback and critique welcome!

@mtdowling
Copy link
Member

mtdowling commented Oct 8, 2021

Hi @lunaris. These are great questions, and hopefully my answers don't disappoint you :)

For the nullary type, I would use an empty structure:

union AnotherUnion {
    first: String,
    second: Integer,
    third: MyUnit,
    fourth: Double
}

structure MyUnit {}

As an example, the JSON protocols used in AWS would serialize this as:

{"third": {}}

Union variants are "members" in Smithy, and all members have to target a shape in the model. Since Smithy doesn't have a kind of unit type, you have to pick a shape to target. Here I've made up my own empty unit type. There's nothing special about it, and it gets code-generated as a normal structure.

For the optional Fourth type, we don't really have a solution... but I'd actually suggest that you turn Fourth into two members of the union to help your end users deal with this type. So instead of trying to model (Maybe Double), you could add a Fifth entry to the enum. It's one less thing for your end users to need to match on at least.

union AnotherUnion {
    first: String,
    second: Integer,
    third: MyUnit,
    fourth: MyUnit,
    fifth: Double
}

-- In Haskell
data AnotherUnion
  = First String
  | Second Integer
  | Third
  | Fourth
  | Fifth Double

edit: Also, if you really wanted a kind of Maybe Double, you could literally add a MaybeDouble union:

union AnotherUnion {
    first: String,
    second: Integer,
    third: MyUnit,
    fourth: MaybeDouble
}

union MaybeDouble {
    just: Double,
    nothing: MyUnit
}

it might not produce as nice of Haskell code that actually uses Maybe, but it's a similar idea.

@lunaris
Copy link
Contributor Author

lunaris commented Oct 15, 2021

Thanks for the reply and sorry for the delay in responding!

Yes, the "extra constructor" or nested union approaches are what we came up with/what I expect we will use; the challenge is mostly in legacy code we are looking to retrofit Smithy APIs on to. For those interested/coming here later, we are considering a trait-based approach to those APIs. Specifically, we are looking at defining a "unit" trait and an "optional" trait that a code generator can use to generate the Maybe-based code. E.g.:

@trait 
structure unit {}

@trait 
structure optional {}

@unit 
structure Unit {}

union AnotherUnion {
  first: String,
  second: Integer,
  third: Unit,

  @optional
  fourth: Double,
}

Thanks again for the detailed response and consideration! Happy to close this issue.

@mtdowling
Copy link
Member

I could see a @unit trait being added to Smithy. I think the only place it makes sense is tagged union members, right?

@mtdowling mtdowling added the feature-request A feature should be added or improved. label Nov 19, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature-request A feature should be added or improved.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants