-
Notifications
You must be signed in to change notification settings - Fork 759
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
Sharing compile-time constant values across templates #10121
Comments
Some notes from today's team discussion:
|
I'm noticing that Bicep is more-and-more starting to look like an actual programming language, I'm wondering if at some point it shouldn't be reconsidered to simply co-opt a subset of an existing programming language syntax instead of mix-and-matching from different languages or coming up with Bicep specific terms. It would improve clarity imo as Bicep is becoming more and more complex as a language. This will start to cause confusion during development, especially if you're 'full stack'. This would also remove some of the need for discussion. For example if it was decided to more or less follow javascript syntax and keywords import might have never been chosen for provider extensibility and would be available for import/export. |
Some additional thoughts:
|
I left that section a little hazy because UDFs in Bicep are still a work in progress, and there are some decisions that haven't been made that would have a big impact on how I think an iterative approach for this proposal would be best, since there are at least three areas that need further discussion:
If this proposal were implemented for
My initial thought (based on no prototyping, so subject to change!) was that So in that case, the
I think the current draft of the UDF proposal doesn't allow variable or parameter references, but if that were to change, we would likely need to create a special kind of closure scope to keep functions sharable. The key goal with
That's true, but whether the expression or its evaluated type is longer is really dependent on the expression.
+1, |
Some more novel alternatives for fun:
I like the idea of |
👍🏻 on |
Would the idea of being able to expose compile-time foldable constants include those values which are the output of loops? |
I think it should. There may be some work that needs to be done on loop type inference to make that possible. |
You could borrow from shell languages and use |
I don't have an opinion on the terms chosen, but my one nitpick is I think the "from" piece of the declartion should go before the "include". This is how the Python style for imports is done, with the optional 'from' keyword before the 'import'. When including many imports, there is not a clear way to sort the imports. If the imports are ordered by the first imported item, this could lead to the imports being shuffled around as more things are added by a user. PythonPython offers a variety of ways to define imports. Generally, it's advised to avoid using "*", which leads to a decrease in performance, but it is still a valid option. import os
import shutil
import pandas as pd
from subprocess import *
from pathlib import Path as PathLibPath
from tempfile import mkdtemp, rmdtemp as rmdir BicepUsing that as inspiration, here are some examples of how it would look in Bicep. Import specific items directly.from './mod.bicep' import foo, buzz, baz Import specific items with alais.from './mod.bicep' import {foo as fizz, bar as buzz, baz as pop}
param fooParam fizz
param barParam buzz
param bazParam pop Import all itemsIn python, import './mod.bicep' as mod
param input mod.foo Comparing to proposal syntaxWhen including many imports, there is not a clear way to organize the imports. If the imports are ordered by the first item, this could lead to the imports being shuffled around as more things are added by a user. import {foo as fizz, bar as buzz, baz as pop} from 'mod.bicep'
import {name as ComputeName, sku as ComputeSku, location as ComputeLocation} from 'typesCompute.bicep'
import {name as StorageName, sku as StorageSku, location as StorageLocation} from 'typesStorage.bicep'
param fooParam fizz
param barParam buzz
param bazParam pop
param computeName ComputeName
... Starting with the from clause, we can order all the imports alphabetically by the file names. Now, even if we add more imports to typesStorage like 'aDefaultConfig', we do not have to reorder the statements. from 'mod.bicep' import {foo as fizz, bar as buzz, baz as pop}
from 'typesCompute.bicep' import {name as ComputeName, sku as ComputeSku, location as ComputeLocation}
from 'typesStorage.bicep' import {name as StorageName, sku as StorageSku, location as StorageLocation}
param fooParam fizz
param barParam buzz
param bazParam pop
param computeName ComputeName Other langaugesThis is pretty similar to other languages like Java and Scala. Though, they do not use the "from" keyword. I do think the 'from' is a bit easier to read, once you are used to it. import users.* // import everything from the users package
import users.given // import all given from the users package
import users.User // import the class User
import users.{User, UserPreferences} // Only imports selected members
import users.UserPreferences as UPrefs // import and rename for convenience |
Will we be able to use registry-based modules in the For example, I would like to be able to create a collection of my common types in our registry, and reuse them in my templates. from 'br/public:types/storage-acount:0.0.1' import types as StorageTypes
param location StorageTypes.location
param name StorageTypes.name
param newOrExisting StorageTypes.newOrExisting
param isZoneRedundant StorageTypes.isZoneRedundant
module storageAccount 'br/public:storage/storage-acount:0.0.1' = {
name: 'mystorageaccount'
params: {
location: location
name: name
newOrExisting: newOrExisting
isZoneRedundant: isZoneRedundant
}
}
output storageProperties StorageTypes.storageProperties = storageAccount.outputs.storageProperties This could be hard, because I don't think the user-defined If it is particularly difficult to get the type information from a registry module, at least being able use a URL with from 'https://raw.githubusercontent.com/Azure/bicep-registry-modules/main/modules/storage/storage-account/main.bicep' import types as StorageTypes
or from 'aka.ms/bicep-storage-types' import types as StorageTypes
|
One last thought, there could be cases where we do not want to make everything in a bicep file importable, and instead we may want to make something 'private'. Here, I would also suggest looking at how this has been in done in Python. Basically, the language has no 'private' keyword. By notation, a method or variable name that is prefixed with "_" is considered private (ex: def _my_private_method(): return "Something"). Programmers are freely able to use private methods if they want to, but static analysis tools will add a warning. I really like how this 'psuedo-private' is done in Python. Unlike languages like C#, or Java, which have very strict 'private' keywords, in Python it is super easy to test private methods. Having 'private' be part of the static analysis, instead of as part of the compilation rules, gives users the most flexibility. In the bicepconfig, users can choose if using private items causes a error or just a warning. |
@dciborow I'll bring the placement of @rouke-broersma I missed your comment earlier, but you may want to look at Farmer. It's an F# library that lets you write ARM templates as F# scripts. We're very committed to making a Turing-incomplete DSL that is approachable to an audience composed of both developers and systems administrators, but I acknowledge that no solution will be the best choice for every team. We try to keep an up-to-date listing of alternatives to Bicep that target the ARM engine and ecosystem here. |
Based on team discussions, I'm including two alternative proposals below, one that incorporates the semantic described above into Sticking the discussion label back on the issue so that we can talk these over at an upcoming team meeting. |
This content was moved to be the first alternative proposal in the issue description. One hurdle this proposal must address is that Bicep already has an How are
|
This content was moved to be the second alternative proposal in the issue description. Bicep already has a mechanism for referencing another template: as a module that will be executed as a nested deployment. How are
|
I'm imagining types as used in Bicep as being quite similar to interfaces in C# in that they identify the shape of the implementing object and can be passed around like contracts - one then develops more against the interfaces and cares a lot less about whatever it is that ultimately implements it. To that end, my preference at a high level is that we use keywords over conventions where possible. Within a module, everything remains private to that module by default (as it is today) unless the symbol is prefixed with a The
import * from './foo.bicep' as bar //Everything that is exported from foo is accessible from the 'bar' namespace
import { customType } from './foo.bicep' //Only the 'customType' is available in the current module from foo
import { customType as bar } from './foo.bicep' //Only the 'customType' is available from foo and it's called 'bar' when used in the file
import 'br:foo/bar@1.2.2' as bar //Everything that's exported from the module at foo/bar is accessible from the `bar` namespace
import 'kubernetes@1.0.0' //Standard named provider import
import { customType as foo } from './foo.bicep' as bar //Invalid syntax as the type already has an alias and the whole of the file isn't being used here
using * from './foo.bicep' as bar //Everything that is exported from foo is accessible from the 'bar' namespace
using { customType } from './foo.bicep' //Only the 'customType' is available in the current module from foo
using { customType as bar } from './foo.bicep' //Only the 'customType' is available from foo and it's called 'bar' when used in the file
using { myConstant as c } from './foo.bicep' //The referenced symbol is a constant, so allow it to be used exclusively as a constant in this file
using { customType as foo } from './foo.bicep' as bar //Invalid syntax as the type already has an alias and the whole of the file isn't being used here The downside to the using import { customType as bar } from 'br:foo/bar@1.2.2' //Remote source, but using the type I expect that in my own deployments, I'll have whole files (modules?) that aren't intended to be deployed, but are just entirely filled with shared types. Rather than having to import all or none of them, I'd rather not pollute Intellisense all the itme and instead have the option to either import all via a wildcard or import select types that can be optionally aliased. Regarding the module discussion, today, Bicep largely treats every file as a separate module that simply needs to be referenced in another file to access the outputs. I would again propose that the types be largely considered as just a compile-time feature (like in TypeScript) where, like interfaces in C#, they provide a shape that can be developed against for strongly-typed support, but where at runtime, it doesn't matter what ultimately fills the shoes. I propose that constants be treated as fixed value types, to be made available like any other types via |
I don't think this is accurate. The
The ambiguity that I'm concerned about is that any OCI artifact ID (like
Extensibility is still in preview, so we have some flexibility there. It's possible extensibility providers could use another keyword (like One bit of feedback we got from Anders Hejlsberg was that we should avoid cases where the same Bicep syntax generates different ARM templates based on the values supplied to said syntax. If we can't tell what
The proposals are separate alternatives. We can introduce a new keyword (like As an example, assume you have the following @minLength(3)
@maxLength(24)
type shortName = string Assuming you have a include: include {shortName} from 'types.bicep'
param name shortName import: import 'types.bicep'
param name types.shortName or from 'types.bicep' import shortName
param name shortName module: constants module types 'types.bicep'
param name types.shortName I think we could probably make the The |
Just chiming in to say that due to the natural proclivity to refer to these as "shared" values (as demonstrated above on this thread)
I also wonder how much real value there is in supporting aliasing given variable renaming is pretty easy in vs code? Still I guess it's a nice in principle.. |
The greatest value in aliasing for me would be for those modules that I didn't write and don't own myself but still want to consume despite there being a name conflict. |
I believe so, |
With the current proposal, an extensibility provider is not able to define constants and user-defined types This limitation is unfortunate since an extensibility provider is a great candidate for their use. For example, I can imagine a potential GitHub extensibility provider wanting to define the structure of the GitHub REST API objects and also define some constants/enums for particular purpose too... After all these are also modeling the provider domain in a way similar to resources. If an ext. provider can define resource types, it follows that it should also define compositions of built-in types |
I think two things are being conflated here. The provider can define the shape of resource PUT requests it expects, and the Bicep compiler will provide validation of those domain objects, but resource body validation is always issued as non-blocking warnings. This is very different from how parameter/output validations (including decorators like This difference in validation levels exists because provider resource shapes are a snapshot in time of an API contract that will be enforced at some point in the future by a system outside of ARM's control, whereas types defined in the template have deterministic behavior at deploy time that can always be known at compile time. With resource body validation, there is potential for drift between when a provider publishes its manifest and when the validation will be performed (even assuming the provider types were 100% at the time of publication, which is not always the case). I would think we would discourage extensibility providers from supplying "user-defined types" (which exist solely to provide blocking validation of template parameters and outputs) and instead encourage them to validate the bodies of resource PUT requests.
If an extensibility provider did want to define some template authoring helpers in the form of vars or UDFs, I don't think it's too much to ask them to publish two artifacts (or an extra layer within the artifact that defines the extensibility provider). We could end up in a situation where a user needs to
I don't see how that follows. You're proposing that Bicep introduce a single abstraction ("types") that covers both ARM parameter validation and ARM resource provider routing, but I would argue that any single abstraction will need to be leaky and will thus cause more confusion than just keeping the concepts distinct. A "resource type" in ARM is just HTTP routing information: the fact that a resource has a type of "Microsoft.Storage/storageAccounts@2023-12-31" tells ARM where to send the PUT request to deploy a resource of that kind (as well as what value to use for the I think one other issue that's factoring into this discussion is that the current |
@asilverman Unfortunately, the output of a Bicep build includes all the custom type information (e.g. it's not purely a development-time artifact like an interface in TypeScript), which means that all this custom type data would ultimately have to be bundled alongside any of the modules in one way or another anyway. The creation of a special file type just to contain them would still necessitate treating it as some sort of module and then handling import from it like any other file, so I'm not sure what the benefit would be. @jeskew I think your idea of changing the keyword for providers to |
I updated the issue description to reflect recent discussions and have moved the alternative proposals from comments to sections of the main description. |
I just had a thought over night I wanted to share. I think we could potentially allow user-defined types declaration in |
If you think of an ARM-JSON as an intermediate language that will eventually be read only by the machine, then having the behavior be as you describe is a non-issue. In fact, I think it reinforces the claim that the definitions should be handled as a separate construct. Consider how importing them from a
The benefits as I see them are:
There may be more benefits as well that are not immediately apparent:
That said, its just my personal experience and opinion and some of these benefits so I am trying to keep an open mind about it |
@asilverman Could you open a separate issue for the |
Can we link to the other 2 (or 3?) proposals from here? |
@brwilkinson The issue description has three proposals (1 main proposal and two alternatives); these aren't written up anywhere else. There are some references in the discussion to the user-defined functions (UDFs) proposal (#9239) and the Bicep extensibility proposal (#3565). |
The Bicep intermediate representation does not currently include type expressions, with the final step of the compilation process going straight from Bicep syntax to ARM JSON. This PR augments the IR to include type expressions and updates the TemplateWriter to generate ARM JSON from IR expressions rather than Bicep syntax. This change is meant to make work on #10121 easier, since that feature will involve moving type declarations across semantic models. The bulk of the changes in this PR are to baseline files, so the output of the baseline regeneration script is in a separate commit. ###### Microsoft Reviewers: codeflow:open?pullrequest=#11229
Partially addresses #10121 This PR adds compile-time imports for user-defined types. This feature is only available under a flag and must be enabled with a bicepconfig.json file that looks like this: ```json { "experimentalFeaturesEnabled": { "compileTimeImports": true, "userDefinedTypes": true } } ``` With the feature enabled, the `import` keyword can be used to move user-defined types across templates. For example, given the following template saved as `mod.bicep`: ```bicep @export() type foo = string @export() type bar = int ``` The `foo` and `bar` type symbols can be imported into another template either individually by name using "symbols list" syntax: ```bicep import {foo, bar} from 'mod.bicep' ``` or with "wildcard" syntax (which is less efficient but more fun to say): ```bicep import * as myImports from 'mod.bicep' ``` With the "symbols" list syntax, you can import a subset of the target template's exported symbols and rename/alias them at will: ```bicep import {foo} from 'mod.bicep' // <-- Omits bar from the compiled template import {bar} from 'mod.bicep' // <-- Omits foo from the compiled template import { foo as fizz bar as buzz } from 'mod.bicep' // <-- Aliases both foo and bar ``` You can also mix and match syntaxes. Symbols will be imported at most once: ```bicep import {foo} from 'mod.bicep' import * as baz from 'mod.bicep' ``` Imported types can be used anywhere a user-defined type might be (i.e., within the type clauses of `type`, `param`, and `output` statements). Only types that bear the `@export()` decorator can be imported. As of this PR, this decorator can only be used on `type` statements. ###### Microsoft Reviewers: codeflow:open?pullrequest=#11298
Partially addresses #10121 This PR updates the `compileTimeImports` experimental feature to support variables in addition to type declarations. `var` statements can be the target of an `@export()` decorator (provided that they only contain references to other variables and not to resources, modules, or parameters), and an imported variable can be used just like one declared in the importing template. Some aspects of this feature that should get special scrutiny: * Variables in an ARM template don't have anywhere to put metadata, so I've added a template-level metadata property that contains the names (and descriptions, if any) of any exported variables. * Imported variables are analyzed twice (once to determine their type, and then separately when they are migrated to the importing template in `TemplateWriter`), so there is room to improve efficiency * The translation from a `"copy"` property to a `for` loop expression is not perfectly roundtrippable: * `copyIndex(<var name>, 1)` will be transformed to `add(copyIndex(<var name>), 1)` * The `count` copy property will be transformed to `length(range(0, X))`, where `X` is the original `count` ARM expression. ###### Microsoft Reviewers: [Open in CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/Azure/bicep/pull/11657)
Issue summary
When users want to share or standardize values across multiple templates, one pattern that many turn to is the "shared constants module." Similar to how applications will often define symbols representing invariant values in a single place in code (such as
Bicep.Core.LanguageConstants
in the Bicep compiler), a template author may define some known invariants as outputs in a dedicated module:constants.bicep
This is extremely convenient for users. Since modules support parameterization, this pattern can also be used to follow standardized naming conventions:
names.bicep
From ARM's perspective, however, using modules to share constants is undesirable. Modules become nested deployments, which impose a runtime cost in terms of orchestration and persistence. As we look into how to share additional values across templates (including user-defined types and functions), Bicep should offer a way to share values at compile time rather than relying on ARM to share values at run time.
Proposed remediation
Bicep should add two new keywords for working with sharable symbols:
import
andexport
. To avoid confusion with Bicep extensibility control statements, the provider registration statement should be changed fromimport 'kubernetes@1.0.0'
toprovider 'kubernetes@1.0.0'
.What makes a value sharable?
In order to be sharable, a value MUST:
The last stipulation is added because an
import
statement should not silently add to a template's effects or to its public contract. This precludes the sharing ofmodule
andresource
symbols (which add a side effect (a deployment) to a template), as well as ofparam
andoutput
symbols (which add to the input and output data of a template). Outputs, modules, and resources can be "shared" via resource references. Some variables will be sharable, as will all user-defined types. Though still under development, user-defined functions are expected to be sharable as well. (This proposal assumes that functions will be declared with thefunc
keyword, though that is not final).param
output
module
module
is an action (a deployment), not a value.resource
resource
is an action (a deployment), not a value. Evenexisting
resources have a representation in the ARM deployment graph.var
type
func
import
The
import
keyword will be used in a template that wishes to consume a shared value.Example usage
Assuming a file
mod.bicep
with the following content:To import the
foo
,bar
, andbaz
symbols, a template would include animport
statement:This would cause the compiled template to include the definitions of
foo
,bar
, andbaz
as if they had been originally defined within the consuming Bicep template. The compiled template might look like the following:The above is the same template that would be compiled from the following Bicep:
There would be no runtime representation of the
mod.bicep
that had been imported; only of the included symbols.Aliasing imported symbols
import
will support aliasing via theas
keyword:import
could also support importing all includable symbols under a provided namespace using syntax borrowed from recent versions of ECMAScript:export
The
export
keyword will be used in templates that produce shared values.export
must precede a statement that declares named symbol with a compile-time constant value.Examples
The following examples would all be permitted:
The following usages would all be prohibited:
Should values be includable by default?
An alternative to the
export
keyword would be to make all symbols in a template consumable by default. I believe we should avoid doing so for two reasons:var
symbol would become a backwards incompatible change.export
keyword when the symbol is not a compile-time constant is a better user experience than reporting an error wherever the symbol isimport
ed.Alternative keywords to consider
import
andexport
will likely be familiar to many Bicep users because it is used in the same way in JavaScript modules, but users familiar with JS might not realize thatimport
will actually be copying the statements declaring the imported symbols into their template. (The mechanism used will be a little bit more sophisticated than simple copy/paste, but under the hood,import
in Bicep will function more likeinclude
/require
in PHP or#include
in C than likeimport
in JS or Python) Alternative keywords to consider:include
/public
include
-ing template, but doesn't have the panache ofimport
/export
.include
is a verb andpublic
is an adjective) instead of using two verbs; this is reflective of the underlying effect of the statements in the compiled JSON template, so not necessarily a bad thing.use
/share
use
/share
are a pair (unlikeimport
/export
, which are pretty obviously two sides of the same coin)Even if we opt to use
use
/share
,include
/public
, or another keyword pair, we should still change the keyword used for provider registration fromimport
toprovider
. Having bothimport
andinclude
as keywords but having them do very different things is likely to be a source of confusion, especially for new users.Alternative approaches
Rather than introducing two new keywords, it may be possible to repurpose an existing statement type (though doing so may still require introducing at least two new keywords).
Decide at compile time whether an
import
is an extensibility control statement or a macroOne hurdle this proposal must address is that Bicep already has an
import
statement, though it is used for registering extensibility providers that can be called during a deployment.How are extensibility provider registration and compile-time constant sharing the same?
The Bicep team frequently uses the word 'types' to describe how
import
modifies the compilation environment (i.e.,import
allows the template author to deploy new kinds of resources), and a major goal of the current proposal is to support the sharing of 'types.' From a template author's perspective, it would be confusing to say that some 'types' and functions must beimport
ed while others would require aprovider
statement.How are extensibility provider registration and compile-time constant sharing different?
When we talk about the 'types' that are introduced via an extensibility provider registration, we are refering to the varieties of resources that can be deployed in a template. Crucially, extensibility provider registration creates no new symbols for such 'types' but does allow more flexibility in the creation of resource symbols. For example, the
endpoint
symbol in the following template can only be created because the kubernetes provider has been registered in the first line:'core/Endpoints@v1'
is not a symbol and cannot be used as a parameter or output type, but we often call it a 'type' because it would be used to populate thetype
andapiVersion
properties of the resource in the compiled JSON template.Compile-time constant sharing, however, is meant to be executed at compile time (like a C macro) and leave no reference to the target of the
import
statement in the compiled artifact. The 'types' that would be pulled in are used exclusively for input and output validation but introduce no new deployment capabilities. The ARM runtime uses user-defined types for deployment pre- and post-condition verification, whereas ARM has no knowledge whatsoever of resource types. This distinction is muddied somewhat in Bicep, as the compiler will provide advisory validation of resource body data and resource property access, but in the ARM engine, such validation is left entirely up to the resource (or extensibility) provider.What would using the same syntax for both extensibility provider registration and compile-time constant sharing look like?
The existing
import
statement would be unchanged. Currently, the string identifying what is imported must refer to well-known, named provider or to an OCI artifact containing a provider manifest:These statements create three namespace symbols (
kubernetes
,privateProvider
, andanotherPrivateProvider
, respectively), each of which may be renamed using theas
keyword:The Bicep compiler could treat an
import
statement pointing at an ARM or Bicep template as a different kind of statement, one that could pull in user-defined types, exported variables, or user-defined functions. The targeted template could exist either in a local file or in a Bicep module registry:Each of these statements would create a local namespace symbol (
foo
,aModule
, andanotherModule
, respectively) containingexport
ed types, variables, and functions as top-level members of the namespace. The symbol associated with the namespace could be chosen with theas
keyword, and thewith
clause (used to provide configuration to extensibility providers) would not be permitted.Only symbols introduced with the
export
keyword would be added to theimport
ed namespace, and such symbols could be dereferenced either with or without the namespace, with the namespacing prefix ofbar.
only being required for disambiguation.Disadvantages
Syntactic ambiguity
Repurposing the
import
keyword in this way would make static analysis and compilation more difficult, as the effect of animport
statement would be entirely dependent on the content of what was imported. The strong distinction betweenimport
ing an extensibility provider andimport
ing constant values from another template would still exist in ARM JSON templates: the template emitter would, depending on whether the imported target was an extensibility provider or another template, either add to theimports
block of the compiled template (in the case of a provider import) or copy the targets exported values into the compiled template (in the case of a template import).There is unlikely to be any artifact that is ambiguously either an extensibility provider or a template, although this is possible with an OCI artifact. To disambiguate, we can require an extra keyword for constant value imports, e.g.
import static 'foo.bicep'
(to follow Java's example) orimport constants 'foo.bicep'
(taking inspiration from TypeScript's type-onlyimport
s). Indeed, we could require thestatic
secondary keyword for every usage ofimport
meant to cause compile-time constant sharing, which would allow the Bicep grammar to more precisely specify when thewith
clause of animport
statement is and is not allowed. This would also alleviate any concerns about introducing syntax whose representation in the intermediate language is indeterminate, sinceimport
andimport static
would effectively be different keywords.Semantic ambiguity
Assume a file
mod.bicep
that contains some exported constants alongside resource statements.import 'mod.bicep'
will not execute the resource deployments described inmod.bicep
, but will this be clear to users? The behavior difference betweenimport 'mod.bicep'
andmodule mod 'mod.bicep' = ...
will need to be made clear via documentation, and I expect this distinction to be somewhat confusing to newcomers to Bicep. We could introduce a new file extension & artifact type that only allowstype
,func
, andvar
statements and only permit those artifacts (and not standard Bicep templates) to be imported, but this feels like a lot of ceremony to impose in order to avoid a small amount of ambiguity. (If we do adopt this approach, we should strongly consider abandoning Biceptype
syntax in favor of an externally maintained standard like JSON schema; there's no point in forcing users to learn a single-purpose syntax if it doesn't need to integrate with existing Bicep templates.)The added ambiguity when comparing
import 'mod.bicep'
toimport 'kubernetes@1.0.0'
muddies the water further, since these two statements would have the same syntax but wildly different semantics, with the only distinguishing characteristic being theimport
statement's target. Experienced Bicep users would learn to distinguish the two statement types because the targets look different at a glance, though this is of course not the case forimport
statements targeting an OCI artifact ID. We may of course decide that the semantic difference betweenimport 'mod.bicep'
andimport 'kubernetes@1.0.0'
is an implementation detail only of concern to the ARM runtime. However, we will likely need to introduce some way for "power users" to disambiguate if they are concerned about template size limits and/or the runtime effects of extensibility provider registration (i.e., via animport static
-like statement), especially if we expect OCI artifacts that are ambiguously either Bicep modules or extensibility providers to proliferate.Less control over tree-shaking and aliasing
The
import
statement proposed in the first part of this document requires the template author to pick which symbols will be imported from the target and provides an opportunity to alias each one individually. The extensibility provider registration syntax, however, creates a single symbol for the imported namespace. Bicep only requires users to "fully qualify" a symbol (specify its namespace via the<namespace>.<symbol>
syntax) if there is a locally declared symbol of the same name or if multiple imported namespaces declare a symbol of the same name. Theimport
statement lets template authors alias the namespace symbol, but not any symbols contained in the namespace. This may lead to more verbose templates in some cases.The proposed
import {foo} from 'bar.bicep'
syntax furthermore allows the compiler to aggressively tree-shake the symbol table ofbar.bicep
so that only the declarations necessary forfoo
to be a valid symbol are copied into the host template. A naive implementation of the alternateimport 'bar.bicep'
syntax would copy all exportable symbols frombar.bicep
into the host template, although the compiler could be enhanced to perform tree-shaking based on which symbols frombar.bicep
are dereferenced in the host template. This could potentially be seen as an advantage of theimport 'bar.bicep'
syntax, asimport {foo} from 'bar.bicep'
forces the template author to identify which symbols will be used in the host template, even though that's something the compiler can trivially determine given theimport 'bar.bicep'
syntax.Share compile-time constants via
const module
statementsBicep already has a mechanism for referencing another template: as a module that will be executed as a nested deployment.
How are
module
andimport
the same?Both target another template and allow the parent template (the one in which the
module
orimport
statement occurs) to dereference symbols of the targeted template (outputs in the case ofmodule
; constants in the case ofimport
). As noted above, it is not uncommon to use modules today to share values across templates.How are
module
andimport
different?A
module
statement creates aMicrosoft.Resources/deployments
resource that will be deployed. Like all other resources, amodule
must have aname
property, which is used to create an identifier associated with the resource, allowing the resource to be dereferenced by ID rather than by symbol. This has a runtime cost in the ARM engine. The expectation withimport
is that it will have no runtime cost and will be resolved entirely at compile time.import
also has no declaration body, as it has no properties.What would importing constant values via
module
statements look like?Because
module
statements already target templates, users would need to disambiguate between nested deploymentmodule
s and constant-value-importmodule
s. This could be accomplished with an extra keyword preceding themodule
keyword, e.g.,Any alternative to
const
(such asinline
,static
,constonly
,constants
, etc.) could be used instead, so long as it does not conflict with any existing Bicep keyword.I'll refer to the existing
module
statement as a "deployed module" and the strawman syntax above as a "const module". A "const module" statement would introduce a new namespace symbol (mod
in this case), andexport
ed symbols in the const module could be dereferenced either with or without the namespace, with the namespacing prefix ofmod.
only being required for disambiguation:One advantage of this approach is that it would make sense to expose
export
ed values from "deployed modules," too, since I expect many users would want to access a deployed module'sexport
ed types in addition to its outputs. One common case might be if a template author wants to accept a parameter composed of the parameter types of a module:With the
import
syntax proposed in the first part of this document, users would need to have both animport
and amodule
statement to achieve this:(Bicep could opt to support pulling in types via either an
import
or amodule
statement, with the distinction being that the latter also deploys the module. It would probably be worth gathering user feedback on whether users actually want this feature and on whether supporting constant sharing via two separate mechanisms, each with its own side effects, is confusing.)Disadvantages
Could this overcomplicate the
module
statement?There has also been some discussion of adding an
inline
prefix keyword tomodule
statements as a way to share code without creating nested deployment resources. If bothinline
andconst
keywords were added, they would be incompatible, and themodule
statement overall would become a fairly complex beast.Less control over tree-shaking and aliasing
The
import
statement proposed in the first part of this document requires the template author to pick which symbols will be imported from the target and provides an opportunity to alias each one individually. The "const module" syntax, however, creates a single symbol for the imported module. This change will require Bicep to treat module symbols as a form of namespace. Bicep only requires users to "fully qualify" a symbol (specify its namespace via the<namespace>.<symbol>
syntax) if there is a locally declared symbol of the same name or if multiple imported namespaces declare a symbol of the same name. Theconst module
statement lets template authors alias the module symbol, but not any symbols contained in the namespace. This may lead to more verbose templates in some cases.The proposed
import {foo} from 'bar.bicep'
syntax furthermore allows the compiler to aggressively tree-shake the symbol table ofbar.bicep
so that only the declarations necessary forfoo
to be a valid symbol are copied into the host template. A naive implementation of the alternateconst module mod 'bar.bicep'
syntax would copy all exportable symbols frombar.bicep
into the host template, although the compiler could be enhanced to perform tree-shaking based on which symbols frombar.bicep
are dereferenced in the host template. This could potentially be seen as an advantage of the "const module" syntax, asimport {foo} from 'bar.bicep'
forces the template author to identify which symbols will be used in the host template, even though that's something the compiler can trivially determine given the "const module" syntax.The text was updated successfully, but these errors were encountered: