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

Add user-facing docs for Variant #38712

Closed
wants to merge 5 commits into from

Conversation

annelo-msft
Copy link
Member

@annelo-msft annelo-msft commented Sep 14, 2023

Addresses #32978

This PR adds samples for Variant in the Azure.Core.Experimental/samples directory, with the intention that these would move to Azure.Core/samples when Variant GA's in Azure.Core.

One of the purposes of this PR is to ask the questions:

  • Is this how we want to think about this type?
  • Are these the right APIs for what we want this type to do?
  • What's missing/needed to make the dev UX for this type great?

@azure-sdk
Copy link
Collaborator

API change check

API changes are not detected in this pull request.

}
```

If you do know the type the `Variant` is holding, you can use the `As<T>` method to get its value as that type.
Copy link
Member

Choose a reason for hiding this comment

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

As a potential caller, I'm not clear on whether the explicit cast operation, As<T>, or both will properly downcast or upcast e.g., Variant holds an int but would either approach upcast to a long? Or maybe it holds a long but small enough to fit into an int: will that be supported? What happens on overflow? I'd love to see these details explained, even if briefly, to know how to better use Variant.

Copy link
Member

Choose a reason for hiding this comment

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

Oh. You do explain that As<T> doesn't cast just below. But, still, my question stands about the explicit cast operator. And should we support that if not supported already? I see a lot of advantage in some scenarios rather than a potentially long switch statement like you show above.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good questions - let me clarify this!


`Variant` handles nullable primitives without boxing them for value types on the supported list above. It also supports holding a reference type with a `null` value. In either of these cases, the variant's `Type` property will return `null`.

Working with nulls with Variant can be a little tricky for the following reasons. First, since a Variant instance holding `null` has a value (i.e., it's a Variant holding a `null`), any comparison to `null` will return false. Second, assigning a `Variant` to an instance that holds `null` could require you to call the `Variant` constructor and cast the `null` some type to avoid ambiguous method resolution errors. Because of this, there are some APIs that make working with `null` and `Variant` a little easier.
Copy link
Member

Choose a reason for hiding this comment

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

Grammar hound time 😄 : should you put "(i.e., it's a Variant holding a null)" in parens if you already preface it with "i.e.,"? I would think it'd just be:

Suggested change
Working with nulls with Variant can be a little tricky for the following reasons. First, since a Variant instance holding `null` has a value (i.e., it's a Variant holding a `null`), any comparison to `null` will return false. Second, assigning a `Variant` to an instance that holds `null` could require you to call the `Variant` constructor and cast the `null` some type to avoid ambiguous method resolution errors. Because of this, there are some APIs that make working with `null` and `Variant` a little easier.
Working with nulls with Variant can be a little tricky for the following reasons. First, since a Variant instance holding `null` has a value i.e., it's a Variant holding a `null`, any comparison to `null` will return false. Second, assigning a `Variant` to an instance that holds `null` could require you to call the `Variant` constructor and cast the `null` some type to avoid ambiguous method resolution errors. Because of this, there are some APIs that make working with `null` and `Variant` a little easier.

Alternatively, remove "i.e.," and just have it in parens. More of a question, though. I've never seen both used this way.

Copy link
Member Author

Choose a reason for hiding this comment

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

Happy to fix - I always have to fix up my grammar over multiple drafts 🙂

Comment on lines +131 to +134
To assign the value a Variant holds to `null`, you can set it to `Variant.Null`:

```csharp
Variant v = Variant.IsNull;
Copy link
Member

Choose a reason for hiding this comment

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

You document Variant.Null but show Variant.IsNull. Not sure which is right given I haven't looked at the code in a while.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch! Will fix - thanks!


## Handling nulls

`Variant` handles nullable primitives without boxing them for value types on the supported list above. It also supports holding a reference type with a `null` value. In either of these cases, the variant's `Type` property will return `null`.
Copy link
Member

Choose a reason for hiding this comment

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

The way I read this, any nullable value type will have a Type property value of null. Is that correct, or is it only when the value of the nullable value type is currently null?

Copy link
Member Author

@annelo-msft annelo-msft Sep 15, 2023

Choose a reason for hiding this comment

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

Ah, good. Let me clarify this. IIRC, the Type property is only null for a nullable value type when the value of the Nullable is null.

Before I checked, I had originally written that for value types we would return the actual type from the Type property (e.g. System.Nullable<System.Int32>), but for reference types we don't. I think this would be easy to implement but we don't do it today. We aren't able to store the type for reference types because it's either entirely unknown or would exceed our 128 bit storage limit.

What do you think? Would that be a better API experience?

Copy link
Member

Choose a reason for hiding this comment

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

I do think it would be much better to return the actual nullable type, if possible. The alternatives both seem worse (e.g. Type's value can change depending on the value, or all nullable value types return null)

Copy link
Member

Choose a reason for hiding this comment

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

Thinking to COM's VARIANT, though, this is analogous to VT_NULL. Sure, it's an int?, but because it's null does it even have a type? And what can any caller do with it besides treat it as null? Or do you feel there's sufficient coverage with the IsNull property?

Copy link
Member Author

@annelo-msft annelo-msft Sep 15, 2023

Choose a reason for hiding this comment

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

I do think it would be much better to return the actual nullable type, if possible.

I agree with this. Let me look at a) how hard it is to implement and b) what the implications are for type switching. I think in cases where we come in through a specific constructor (e.g. int? i = null; Variant v= i;), we can be clear about the type, and even though the value is null, if someone tried to do v = new MemoryStream() we could yell at them for type switching. It feels like there is a user intent here that we could support through the API. I'm not deep on VARIANT, but I know that people say it causes them nausea, so I don't think we have to preserve those rules here if we think there are better APIs to follow in a typesafe world.

I'm happy to look at examples where this gets interesting though, so we can think about whether a design choice like this might paint us into a corner. What are the pro/con cases here?

@@ -0,0 +1,135 @@
# Azure.Variant

`Azure.Variant` is an implementation of a [tagged union](https://en.wikipedia.org/wiki/Tagged_union). It can hold both reference and value types and can be used to avoid boxing .NET primitives. The list of value types that Variant can hold without boxing is below.
Copy link
Member

Choose a reason for hiding this comment

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

Omit the locale from the URL. Also, avoid using directional terms like "below".

Suggested change
`Azure.Variant` is an implementation of a [tagged union](https://en.wikipedia.org/wiki/Tagged_union). It can hold both reference and value types and can be used to avoid boxing .NET primitives. The list of value types that Variant can hold without boxing is below.
`Azure.Variant` is an implementation of a [tagged union](https://wikipedia.org/wiki/Tagged_union). It can hold both reference and value types and can be used to avoid boxing .NET primitives. The following list contains value types that `Variant` can hold without boxing.

int i = (int)v;
```

If you don't know the type of the value that a `Variant` is holding, you can read that from the `Variant.Type` property.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
If you don't know the type of the value that a `Variant` is holding, you can read that from the `Variant.Type` property.
If you don't know the type of the value that a `Variant` is holding, you can read that from the `Variant.Type` property:

MemoryStream streamValue = v.As<MemoryStream>();
```

If you call `Variant.As<T>` and the `Variant` isn't holding the type that you ask for, it will throw an `InvalidCastException`. To try to retrieve the value without risking an exception being thrown, you can use the `TryGetValue` method instead.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
If you call `Variant.As<T>` and the `Variant` isn't holding the type that you ask for, it will throw an `InvalidCastException`. To try to retrieve the value without risking an exception being thrown, you can use the `TryGetValue` method instead.
If you call `Variant.As<T>` and the `Variant` isn't holding the type that you ask for, it will throw an `InvalidCastException`. To try to retrieve the value without risking an exception being thrown, you can use the `TryGetValue` method instead:

}
```

If you do know the type the `Variant` is holding, you can use the `As<T>` method to get its value as that type.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
If you do know the type the `Variant` is holding, you can use the `As<T>` method to get its value as that type.
If you do know the type the `Variant` is holding, you can use the `As<T>` method to get its value as that type:

If you do know the type the `Variant` is holding, you can use the `As<T>` method to get its value as that type.

```csharp
MemoryStream s = new();
Copy link
Member

Choose a reason for hiding this comment

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

Observation: Our team is very inconsistent with how we instantiate in our code samples. Here, the target-typed new expression is used. In other locations, this same line of code may have been written in one of the following forms:

  • MemoryStream c = new MemoryStream();
  • var s = new MemoryStream();

This would be a good topic for the next .NET team sync. /cc: @jsquire

Copy link
Member

Choose a reason for hiding this comment

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

Actually, it was already codified a long time ago in our root .editorconfig and is that var is only usable when the rvalue is obvious - most often instantiation.

Though, we never have discussed the new() pattern.

Copy link
Member Author

@annelo-msft annelo-msft Sep 15, 2023

Choose a reason for hiding this comment

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

My personal preference is for not using var because so often I don't know what type a method is returning and I can't mentally parse the code until I replace those, which I can't do on a GH page. I'm entirely agnostic on new() vs. new TypeName(), and happy to vote with the majority on this. In this example, I used new() just to reduce the width of the sample.

Since you can't have an implicit cast operator from `object`, for reference types, you must create a new instance of `Variant` and pass the value to the `Variant` constructor.

```csharp
MemoryStream s = new();
Copy link
Member

Choose a reason for hiding this comment

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

Do we want to include a using statement in this sample?

Suggested change
MemoryStream s = new();
using MemoryStream s = new();

Copy link
Member Author

Choose a reason for hiding this comment

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

Good callout. I don't want to add complexity to the samples, so I'll pick a type for the examples that isn't disposable to sidestep the issue. Thanks, @scottaddie!

}
```

## Handling nulls
Copy link
Member

Choose a reason for hiding this comment

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

Avoid gerunds in headings.

Suggested change
## Handling nulls
## Handle nulls

Copy link
Member Author

@annelo-msft annelo-msft Sep 15, 2023

Choose a reason for hiding this comment

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

What's the thinking behind this rule?

Copy link
Member

Choose a reason for hiding this comment

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

I won't die on this hill, but there are 3 reasons:

  1. It's more difficult for non-English speakers to parse gerunds, since many of their languages don't have this word form. This factor matters if/when the content is localized. For example, if we decide to publish this content to learn.microsoft.com down the road.
  2. Gerunds are longer.
  3. The indicative verb form is the most direct (create a database vs. creating a database).

Copy link

Hi @annelo-msft. Thank you for your interest in helping to improve the Azure SDK experience and for your contribution. We've noticed that there hasn't been recent engagement on this pull request. If this is still an active work stream, please let us know by pushing some changes or leaving a comment. Otherwise, we'll close this out in 7 days.

@github-actions github-actions bot added the no-recent-activity There has been no recent activity on this issue. label Nov 17, 2023
Copy link

Hi @annelo-msft. Thank you for your contribution. Since there hasn't been recent engagement, we're going to close this out. Feel free to respond with a comment containing /reopen if you'd like to continue working on these changes. Please be sure to use the command to reopen or remove the no-recent-activity label; otherwise, this is likely to be closed again with the next cleanup pass.

@github-actions github-actions bot closed this Nov 24, 2023
@annelo-msft
Copy link
Member Author

/reopen

@annelo-msft annelo-msft reopened this Nov 27, 2023
@github-actions github-actions bot removed the no-recent-activity There has been no recent activity on this issue. label Nov 27, 2023
Copy link

github-actions bot commented Feb 2, 2024

Hi @annelo-msft. Thank you for your interest in helping to improve the Azure SDK experience and for your contribution. We've noticed that there hasn't been recent engagement on this pull request. If this is still an active work stream, please let us know by pushing some changes or leaving a comment. Otherwise, we'll close this out in 7 days.

@github-actions github-actions bot added the no-recent-activity There has been no recent activity on this issue. label Feb 2, 2024
@annelo-msft
Copy link
Member Author

/reopen

@annelo-msft annelo-msft removed the no-recent-activity There has been no recent activity on this issue. label Feb 2, 2024
Copy link

github-actions bot commented Apr 5, 2024

Hi @annelo-msft. Thank you for your interest in helping to improve the Azure SDK experience and for your contribution. We've noticed that there hasn't been recent engagement on this pull request. If this is still an active work stream, please let us know by pushing some changes or leaving a comment. Otherwise, we'll close this out in 7 days.

@github-actions github-actions bot added the no-recent-activity There has been no recent activity on this issue. label Apr 5, 2024
Copy link

Hi @annelo-msft. Thank you for your contribution. Since there hasn't been recent engagement, we're going to close this out. Feel free to respond with a comment containing /reopen if you'd like to continue working on these changes. Please be sure to use the command to reopen or remove the no-recent-activity label; otherwise, this is likely to be closed again with the next cleanup pass.

@github-actions github-actions bot closed this Apr 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Azure.Core no-recent-activity There has been no recent activity on this issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants