scp-fs2open / scp-fs2open/fs2open.github.com
RFC: Keyword parameters
- Dominant language
- C++
- Stars
- 487
- Forks
- 184
- PR merge metrics
- PR metrics pending
Description
FSO's event system is very powerful in many respects, but in others, it shows its age. More modern languages typically support keyword parameters as a first-class construct, as in Kotlin and C#; using a wrapper object, as in Java, Javascript, and Lua; or using a wrapper object with syntactic sugar, as in Python. While FSO supports optional parameters, they are all positional; there is simply no concept of a keyword parameter. This lack has three main consequences. The first is a fairly mild quality of life concern: for any K > 1, if an operator accepts K optional parameters, for any N ∈ (1, K], it is impossible to specify the Nth optional parameter without also specifying the N-1th. The second is more significant: many operators can accept an arbitrarily large number of arguments, and these operators cannot, in general, be extended with new functionality. Lastly, it is often non-obvious what an optional parameter means; `( is-event-true-delay "some event" 0 ( true ) )` comes to mind.
The second limitation can be seen in operators as mundane as `is-destroyed-delay`. Its cousin `is-event-true-delay` has long ago been enhanced with an optional parameter which controls whether it influences directives. This was possible since `is-event-true-delay` originally had a fixed arity of 2. No such relief is possible for `is-destroyed-delay`, nor is it possible to control whether `is-destroyed-delay` provides a directive value. Keyword parameters would provide a solution.
# Proposed implementation
Keyword arguments would be represented using the pseudo-operator `kwargs`. When `kwargs` immediately follows an operator with keyword parameter, it is not a traditional argument, but instead a map of keyword arguments; it would not be evaluated as an expression, but instead interpreted specially. To preserve compatibility, `kwargs` is never required; that is, for some operator `foo` which supports `kwargs`, `( foo ... )` is strictly equivaent to `( foo ( kwargs ) ... )`, where `...` is any list of zero or more expressions. Internally, this would be implemented by a new function in `sexp.cpp` which detects the presence of `kwargs` and skips it if it is present.
Suppose `is-destroyed-delay` were enhanced with a keyword parameter, `directive-controller`, which indicates whether it influences the display of a directive. If a mission wishes to present a directive "Enforce node blockade" which becomes true when certain ships are destroyed and is present from mission start, it could use the following notation to do so:
```lisp
( is-destroyed-delay
( kwargs
"directive-controller" ( false )
)
3
"Antagonist"
"Villain"
"Enemy"
"Bad Guy"
"Humphry"
)
```
The existing form, `( is-destroyed-delay 3 "Antagonist" "Villain" "Enemy" "Bad Guy" "Humphry" )`, would be treated as though `( kwargs )` were present directly after `is-destroyed-delay`.
`sexp.cpp` would need to be enhanced with functions for interpreting keyword parameters. This functionality is entirely opt-in: an operator only supports `kwargs` if it needs to, and other operators do not need to change their implementation. That is, there is no general expectation that `kwargs` be handled; if it is present where it is not expected, it is simply reported as an error.
LuaSEXP operator definitions would be able to declare keyword parameters. Such an operator's action function would be expected to receive a table mapping keyword parameter names to values at the head of its arguments list, e.g. `mn.LuaSEXPs['my-cool-operator'].Action = function(kwargs, foo, bar) ... end`.
# Risks
The proposed implementation has been designed to minimize risk as much as possible: no new parsing forms are required, and `kwargs` can be processed using extant mechanisms.
In theory, `kwargs` may be the name of a LuaSEXP operator. This is always a risk when introducing new operators, and the name has been chosen to minimize the chance of a collision.
Upgraded operators must be tested extensively in both their original and `kwargs` forms.
# Alternatives
New operators could be introduced, e.g. `is-destroyed-delay-silent`.
Instead of the `kwargs` pseudo-operator, a new parse form, such as `keyword-name = ( expression )`, could be introduced.
Many of the use cases for keyword parameters involve boolean values, and dynamic values will likely be rare. A simple list of flags could be used instead; this would greatly simplify the implementation at the expense of some expressive power:
```lisp
( is-destroyed-delay
( flags "dont-influence-directive" )
3
"Antagonist"
"Villain"
"Enemy"
"Bad Guy"
"Humphry"
)
```
# Non-goals
Converting existing optional parameters into keyword parameters is inherently unsafe. While `is-event-true-delay`'s optional parameter motivated this example, it must remain an optional positional parameter; while it could, in theory, be deprecated in favor of an equivalent keyword parameter, doing so would become extremely awkward should the need arise to add further optional parameters. `send-message`'s priority parameter is even more of a non-starter.
It is also not a goal to provide sweeping upgrades to existing operators. Instead, one or two high-value upgrades will be chosen and carefully validated. Subsequent upgrades will be delivered as follow-up work.
Lastly, mandatory keyword parameters are not a goal, since for any existing operator, they would break compatibility. They may be implemented if and when a new operator will benefit from them.
# Questions
Should variables and `` be supported as the name of a keyword argument? (Leaning towards no, unless it simplifies the implementation.)
Should `kwargs` be permitted as a LuaSEXP operator name? (Strongly leaning towards no.)
Should an unexpected keyword be a warning, error, or neither? (Leaning towards a warning.)
Which operators should be upgraded?
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.