Alloy syntax currently does not support conditional expressions. Adding them would bring the language a step closer to being able to solve general-purpose problems and provide flexibility in the way that users write Alloy configuration files.
Some things to consider when choosing a design.
- Syntax familiarity for users
- Readability and terseness, both when reading and writing Alloy configurations
- Easiness to maintain as a language feature
Current
Option 1: Do nothing
One could argue that Alloy does not need conditional expressions, and that all conditional logic should be moved either to the configuration management layer, or to the component layer where the component would decide its behavior based on some external stimuli.
Option 2: Add Templating conditionals
An option would be to add conditionals that don’t resolve to a value, but rather define what the text of the final config should render to.
Ignoring the specific syntax, the idea is presented below
local.file "LABEL" {
filename = FILE_NAME
{{ if env(“POLL_FREQUENCY”) != “” }}
poll_frequency = env(“POLL_FREQUENCY”)
{{ else }}
poll_frequency = “1m”
{{ end }}
}
The rendering would then need to happen first, before the config file gets evaluated.
This option makes it easier to work when the conditions dictate what kind of blocks should be utilized, but is not the only way to go about it, if we make use of the enabled
reserved keyword.
Option 3: Add Value conditionals
The other option would be to add conditionals that resolve to a value and are handled at evaluation time.
Ignoring the specific syntax, the idea is presented below
local.file "LABEL" {
filename = FILE_NAME
poll_frequency = {{ if env(“POLL_FREQUENCY”) != “” }} env(“POLL_FREQUENCY”) {{ else }} “1m” {{ end }}.
}
This option could be enabled by various syntax implementations.
Option 3a: Ternary expressions
One option to implement conditionals is to do something similar to HCL’s implementation of ternary expressions, for either single-line or multiline statements.
attr = condition ? true_val : false_val
attr = (condition) ?
(some
multiline
statement) : (another
multiline)
We don’t currently use the ‘?’ or ‘:’ tokens in Alloy, so it might be relatively easy from an implementation standpoint to distinguish their use in conditionals.
Option 3b: Standard Library function
Another option is to implement conditionals, by adding a new function is Alloy’s stdlib. The function signature would look like:
if(condition, true_val, false_val)
Alloy already supports multi-line statements in function calls, so the implementation effort would be easier. It also provides a useful precedent of new features being available as stdlib functions, similar to how map/reduce/filter might be in the near future.
Option 3c: Multiline conditional statements
A third option is to implement conditionals in multiline statements, similar to bash’s if/fi blocks. We could use curly braces to denote where statements begin and end, but I’d discourage this as it might be confusing with regular Alloy blocks when scanning a configuration file, and if we go this route we should either depend on specific keywords or other symbols.
if (condition)
ret1
elseif (condition_2)
ret2
else
ret
endif
Option 3d: Switch-like statements
A fourth option would be to implement conditionals in pseudo-switch blocks by evaluating conditions in order and returning the statement defined in the first one that evaluates to true, similar to what Erlang does.
if
condition -> statement#1
condition2 -> statement grafana/agent#2
condition3 -> statement grafana/agent#3
true -> final_statement
end
Proposal
Regarding the three main approaches:
- Doing nothing: My personal opinion is that the dynamic nature of Alloy lends itself to conditional expressions; adding them will simplify configuration files, and give more flexibility to power users. This option might keep the Alloy syntax smaller as a language, but it moves the goalposts and makes it rely on an external configuration system.
- Template rendering: My personal opinion is that this is not an approach we should move towards, as the Alloy syntax is a programming language and not a templating language. Configuration errors would be harder to diagnose and would only manifest at evaluation time. And while this approach seems to make it easier to conditionally change the type of blocks to use (eg.
prometheus.*
vs otelcol.*
), I believe that it will make it harder to read and reason about.
- Returning values: My personal opinion is that this is the most flexible of our options. It makes it the easiest to reason about a configuration file, and can be reused in different contexts, from the selection of which blocks to activate, to general Alloy expressions that in the future might be able to perform relabeling rules, or modifying log lines.
My proposal is to go with Option 3, and implement a type of Value conditionals.
Syntax Proposal
After looking through the syntax options, they all come with pros and cons.
The HCL-like format might be familiar to users, but requires some work in the Alloy syntax parser.
The stdlib function would probably be the easiest to implement.
For both of them, writing complex nested conditions is harder.
The multiline conditionals and switch-like statements on the other hand, make it easier to evaluate multiple conditions, but does exactly fit the HCL-inspiredness of the language. On the other hand, this should not discourage us from going forward with what we think is best for Alloy itself.
I propose we go with Option 3d and the if/elseif/end
statements.
Example usage
Using the proposed solution will enable us to use the following configuration:
local.file "apikey" {
filename = "/var/data/my-api-key.txt"
is_secret = if env(“CLUSTER”) == “prod” then "true" else "false" end
}
Implementation
The implementation consists of updating the Alloy syntax parser to recognize these new tokens to build the expression. The conditions in each branch should be valid Alloy expressions that results in a boolean value. The result values may be of any type (either a Alloy builtin type or a capsule), but they must both be of the same type, so that Alloy understands what type the whole conditional expression will return.
Let me know how this sounds to you!