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

User-defined types -How to define default value #9636

Open
slavizh opened this issue Jan 25, 2023 · 17 comments
Open

User-defined types -How to define default value #9636

slavizh opened this issue Jan 25, 2023 · 17 comments
Labels
enhancement New feature or request Needs: Upvote This issue requires more votes to be considered type system

Comments

@slavizh
Copy link
Contributor

slavizh commented Jan 25, 2023

Is your feature request related to a problem? Please describe.
We can define the following type:

type test = {
  foo: true
}

But when you use intellisense on the foo property you get:
image
Instead what I want to get is that foo is boolean and default value is true.

What if I also want to define default value that uses Bicep function like:

type test = {
  foo: subscription().subscriptionId
}

Describe the solution you'd like
See above.

@slavizh slavizh added the enhancement New feature or request label Jan 25, 2023
@ghost ghost added the Needs: Triage 🔍 label Jan 25, 2023
@jeskew
Copy link
Member

jeskew commented Jan 25, 2023

Default values on types came up at a previous team discussion, and the consensus at the time was that modifying a parameter or output based on its declared type wasn't something that should be done lightly, and the rules around which default value to apply and where defaults would be permitted got tricky very quickly.

This is some strawman syntax from that discussion:

type barString 'bar'|'baz' = 'bar'

@sealed()
type myObject {
  @minLength(3)
  foo: string = 'foo'
  bar: barString         // <-- uses default value from type
  baz: barString = 'baz' // <-- uses explicit default
  recur?: myObject // <-- recursive, so blocks a default for `myObject`
} // '= {}' would be a compiler error either:
    // because 'myObject' is directly recursive (if we apply property defaults to the object default), or
    // because required properties 'foo', 'bar', and 'baz' are missing

We looked at a few schema definitions languages that support some form of default value, and found that most schema definition languages either treat default values as purely informational or had some rules that made usage confusing:

  • JSON Schema's default is "not used to fill in missing values during the validation process" per JSON schema's documentation.
  • Smithy's @default trait is also meant as a hint for clients and documentation, but has some strict placement and value requirements to prevent infinite recursion in clients.
  • Terraform's default is permitted on variable declarations, not within type syntax. This is equivalent to how Bicep permits default values for param declarations but not on type aliases.
  • Avro's default property is permitted on most schema node, but the value supplied must validate against the schema with no further modification. This is probably not what people would expect if they supply a default value for an object that is missing a property for which there is a default value defined.

As an alternative, a template author can always supply a default when they read a value. #9454 should help with this, as nullable types will be usable with the ?? (coalesce) operator.

@jeskew jeskew added story: type system vNext Needs: Upvote This issue requires more votes to be considered and removed Needs: Triage 🔍 labels Jan 25, 2023
@jeskew jeskew added this to the Not as fast as you would like milestone Jan 25, 2023
@jeskew jeskew added this to Bicep Jan 25, 2023
@github-project-automation github-project-automation bot moved this to Todo in Bicep Jan 25, 2023
@slavizh
Copy link
Contributor Author

slavizh commented Jan 26, 2023

@jeskew Probably I should have explained it better. My main goal is not to define a default value in type so that default value to be used later in the Bicep deployment. I already have a mechanism in Bicep code where we are setting default values for certain properties if they are not defined. My main goal is that when folks use intellisense to build parameters file or use the module to see foo is a boolean and has default value of true. As you see above foo is reported as Type: true. I want to define foo in a way that it shows Type: bool with default value of true. With the example of type barString seems this can be achieved but only for enums.
I have tried this:

type defaultTrue = bool

type test = {
  foo: defaultTrue = true
}

but it gives error on the equal sign to true but I guess this is something you are planning to implement. That looks ok to me.

@jeskew
Copy link
Member

jeskew commented Jan 26, 2023

I should clarify that the strawman syntax from my previous comment is not planned to be implemented, though that could be revisited based on community feedback.

For the use case you describe where you are setting the default value but wish to communicate this to users, could you use a @description trait for that? type foo = true is not what you want; that is a literal type and means that the only accepted value for foo is true (similar to how type fizz = 'buzz' | 'pop' will cause any value other than 'buzz' or 'pop' to be rejected).

@slavizh
Copy link
Contributor Author

slavizh commented Jan 26, 2023

@jeskew yes, description was my workaround but it is always good intellisense to show this separately from the description and also to be visible in code so when you make changes it is clear what needs to be changed. For example one version of the template foo can have default true but in another to be false.

@jeskew jeskew removed this from the Not as fast as you would like milestone Feb 6, 2023
@pie-r
Copy link

pie-r commented May 15, 2023

@jeskew
In the example below, is it possible to set a default value to sizeGbType?

type sizeGbType = int

type storageProfileType = {
  @description('Optional. The storage size of the server.')
  sizeGB: sizeGbType
---
other keys
---
}

What I'm trying to achieve, is to move this:

@description('Optional. The storage size of the server.')
param storageSizeGB int = 32

inside a user-data type that contains all the storageProfile fields.

@jeskew
Copy link
Member

jeskew commented May 15, 2023

@pie-r While you can't define a default value for a user-defined type at this time, you can make the property optional and apply a default value when reading it:

type sizeGbType = int

type storageProfileType = {
  @description('Optional. The storage size of the server.')
  sizeGB: int?
  ...
}

param storageProfile storageProfileType

var defaultSizeGB = 32

resource disk 'Microsoft.Compute/disks@2022-07-02' = {
  ...
  properties: {
    diskSizeGB = storageProfile.?sizeGB ?? defaultSizeGB
    ...
  }
}

@pie-r
Copy link

pie-r commented May 15, 2023

In this specific case, I think that writing 4 different statement, instead of a single row with the single scalar is not worth it.

param diskSizeGB int = 32 

But I see use cases where your solution is the proper approach. Thanks for sharing!

@pie-r
Copy link

pie-r commented May 15, 2023

My syntax suggestion to enable default values in the context of user-defined types is to have the ability to set them in the param. I don't think exist a programming language that allows you to put the default in a data type.

That means define a type as:

type storageProfileType = {
  key1: string
  sizeGB: int
  key2: string
}

A param like:

param storageProfile storageProfileType = {
key1: ='xxx'
sizeGB:  = 32 # default, use this value if not overridden from inputparam
key2: ='yyy'
}

And now given an input

storageProfile = {
key1:  'fooo'
}

At build time bicep convert it with:

param storageProfile storageProfileType = {
key1: 'fooo'
sizeGB:   32    --> use default
key2: 'yyy'      --> use default
}

OR given an input

storageProfile = {
key1:  'fooo'
sizeGB: 16
}

At build time bicep convert it with:

param storageProfile storageProfileType = {
key1: 'fooo'
sizeGB:   16  
key2: 'yyy'      --> use default
}

@jeskew
Copy link
Member

jeskew commented Dec 11, 2023

Porting some syntax suggestions for this from #12661: (original author: @mattias-fjellstrom)

I would like to be able to provide default values to optional fields in a user-defined type parameter.

I see two options, the first is to be able to provide the default value in the type definition:

param myParameter {
  field1: string
  field2: string? = 'default value'
}

The second option is to allow partial default value like the following, the default values should be merged with the values passed as a value for the parameter (with the passed value taking precedence):

param myParameter {
  field1: string
  field2: string?
} = {
  field2: 'default value'
}

@jeskew
Copy link
Member

jeskew commented Dec 11, 2023

One other option would be to add some form of deepMerge function. To take @mattias-fjellstrom's example above, this might look like:

param myParameter {
  field1: string
  field2: string?
}

var myParameterDefaults = {
  field2: 'default value'
}

var withDefaults = deepMerge(myParameterDefaults, myParameter)

@slavizh
Copy link
Contributor Author

slavizh commented Dec 12, 2023

This one seems the most easy one to use to me:

param myParameter {
  field1: string
  field2: string? = 'default value'
}

Sometimes the default value is something that can only be calculated at deployment time. For example the subscription ID so being able to just type the words: 'current subscription' instead the actual GUID as that one can only known at deployment time is good for us. So basically being relaxed as much as possible syntax. And of course when you use intellisense that default value to be listed along the type and the description with some text like: Default value: current subscription.

@jeskew
Copy link
Member

jeskew commented Dec 19, 2023

One other option would be to add some form of deepMerge function. To take @mattias-fjellstrom's example above, this might look like:

param myParameter {
  field1: string
  field2: string?
}

var myParameterDefaults = {
  field2: 'default value'
}

var withDefaults = deepMerge(myParameterDefaults, myParameter)

It turns out that the union function will already perform a deep merge, so you can use the following as a workaround today:

param myParameter {
  field1: string
  field2: string?
}

var myParameterDefaults = {
  field2: 'default value'
}

var withDefaults = union(myParameterDefaults, myParameter)

@mattias-fjellstrom
Copy link

@jeskew That's a good workaround, I had not thought of trying that 👍🏻

@slavizh
Copy link
Contributor Author

slavizh commented Dec 20, 2023

Be careful with union(). It does not merge arrays within object and null is never merged (if you have default value 'str' and you provide null, the end value will be 'str'.

@ChristopherGLewis
Copy link
Contributor

ChristopherGLewis commented Mar 11, 2024

@pie-r While you can't define a default value for a user-defined type at this time, you can make the property optional and apply a default value when reading it:

type sizeGbType = int

type storageProfileType = {
  @description('Optional. The storage size of the server.')
  sizeGB: int?
  ...
}

param storageProfile storageProfileType

var defaultSizeGB = 32

resource disk 'Microsoft.Compute/disks@2022-07-02' = {
  ...
  properties: {
    diskSizeGB = storageProfile.?sizeGB ?? defaultSizeGB
    ...
  }
}

I'd rather have my type be smarter rather than my code...

This is primarily to match what some resource providers do currently. What happens when we get to import types from resource providers and they have default values?

@dharnil
Copy link

dharnil commented May 30, 2024

Is there any progress of defining defaults?

@gaorlov
Copy link

gaorlov commented Oct 21, 2024

This feels like it would need to introduce strongly typed variable and param creation. Otherwise how would the compiler know what set of defaults to apply?

Given 2 types:

type KvConfig = {
  name: string
  foo: string = 'foo'
}

type StorageConfig = {
  name: string
  bar: string = 'bar'
}

How would the compiler assign defaults to the following:

var kvconf = { name: 'kv' }       // <-- user probably wants to resolve to KvConfig
var saconf = { name: 'storage' }  // <-- user probably wants to resolve to StorageConfig
var nameconf = { name: 'myname' } // <-- user maybe wants just an object

What we can do today is define a "constructor" for the type that we can then call

// myObject.bicep
type MyObject = {
  @minLength(3)
  foo: string?
  bar: string?
  baz: string?
  recur: myObject?
}

func new(userObj object) MyObject => union({
  foo: 'foo'
  bar: 'bar'
  baz: 'baz'
}, userObj)

// main.bicep
import * as MyObject from './myObject.bicep'

var myObj = MyObject.new({foo: 'foo'}) // => returns a MyObject.MyObject

but is fragile:

  • only one new per file to avoid name collisions
  • have to manually create new function for every type
  • no guarantees that everyone else will do the same and name it the same thing

Proposal

  • Add @default(any) decorator to type members, which has the same restrictions as func (no var references, no resource references, etc)
  • Add a constructor to type that would instantiate the defaults if values aren't present.

To use the examples above it would look like

// configs.bicep

type KV = {
  name: string
  @default('foo')
  foo: string
}

type Storage = {
  name: string
  @default('bar')
  bar: string
}

// main.bicep
import * as Configs from './configs.bicep'

var kvconf1 = Config.KV.new(name: 'kv', foo: 'baz')  // => { name: 'kv', foo: 'baz'}
var kvconf2 = Config.KV.new(name: 'kv')              // => { name: 'kv', foo: 'foo'}
var saconf = Config.Storage.new(name: 'storage')     // => { name: 'storage', bar: 'bar'}
var nameconf = { name: 'myname' }                   // => { name: 'myname' }

I'm not sure how much impact this would have on the underlying ARM, but this would alleviate some of the ask above, without needing to introduce persistent type associations to objects.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request Needs: Upvote This issue requires more votes to be considered type system
Projects
Status: Todo
Development

No branches or pull requests

8 participants