Option preference (first or last)?

This is a followup for Minor: "prolog flag option lists" do not follow option list semantics · Issue #756 · SWI-Prolog/swipl-devel · GitHub

It appears ISO talks about duplicate/conflicting options in a few places. Where it does, it says the rightmost (last) wins. That is also what SWI-Prolog implements in PL_scan_options() which is used by pretty much any predicate defined in C(++).

Prolog predicates tended to use member/2, memberchk/2 and later SWI-Prolog’s option/2 and friends. The latter deals with options written Key(Value) as well as Key = Value. It also deals with lists as well as dicts. For lists, this takes the leftmost as winner.

This discrepancy is surely not desirable. The most obvious solution is to make library(options) compatible with PL_scan_options(). However, at last quite a bit of my code uses constructs like this:

p(..., Options) :-
    q(..., [key(Value)|Options]).

Note that library(options) provides merge_options/3, so one could/should write

p(..., Options) :-
    merge_options([key(Value)], Options, Options1),
    q(..., Options1).

Now, Options1 will have key(Value), regardless of whether or not there is a key(OtherValue) in Options or not.

How should this be resolved? Related, is using [key(Value)|Options] a template that is in wide use?

I use it in quite a bit of code; unfortunately I can’t think of a good solution that doesn’t break everything.

Worse, I had Claude code do a quick scan on the whole SWI-Prolog source tree. It uses [key(Value)|Options] in 21 places (probably missed some). All seem to expect to overrule a possibly existing option, but a couple call a C predicate and thus behave differently (read: not as intended) :frowning:

Claude proposes a Prolog flag option_duplicates with values first|last|warning|error. That makes some sense, but you would like to have it scoped so you can say “this file assumes left (or right) wins”, but this should affect the file where you manipulate the option list, while the processing happens elsewhere :frowning: Multiple users of some library may have different intends :frowning:

Once I was young, not spotting any dangers :slight_smile: I fear “to be continued” … Ideas are welcome!

Languages with (stablished) dictionary syntax use record-update syntax for that (e.g., { ...opts, key: val }). That does a shallow copy of the whole dictionary, which is equivalent to merge_options/3. I’d prefer if option lists with duplicated keys are left as ‘implementation defined’ or ‘undefined behavior’ such that programmers cannot assume what will happen and program defensively here (do not duplicate keys, it is confusing… key/values should be order independent and with rightmost or leftmost win, it is no longer true).

Disagree. It’s fairly common in code that I’ve seen (and I think that this is how most Unix commands work) to set up an initial set of defaults and then process the user’s list, to override the defaults.

Both options(last) and options(first) derive their semantics from the simplest implementation (for options(last), Prolog would require an additional call to append/3; for Unix commands, one simply loops through the options and does a switch on them).

One advantage of the Unix way is that it easily works with shell scripts. For example, I can do something like:
my_cmd -a1 -b2 "$@"
(my shell-fu is old, so I might have this wrong).
Of course, it would be just as easy to do my_cmd "$@" -a1 -b2 but that ship has sailed.

Anyway, one thought about how to accommodate both options(first) and options(last) is to change predicates that take a single Options argument but two arguments: Options,DefaultOptions and have the code do merge_options/3. Alternatively, Options,OptionPrecedence, where OptionPrecedence is one of first, last, warning(Pred), error(Pred).

(Options(warning) is best left to a separate discussion, I think.)

PS: I’ve never really liked options(first), probably because I’ve become used to options(last) from how most software (that I’ve used) works.

I think we all agree that was a mistake. What is at stake here is whether there is a sensible route to get this right in the end or not? I think the best way out is to migrate to dicts, which are a lot faster and have clear semantics. Now you overrule options using Old.put(#{k:v, ...}) or you add new defaults using
#{k:v, ...}.put(Old). But, this moves too far away from de-facto standard Prolog :frowning:

I doubt this solves the problem (e.g., how do you chain this?). There might be an option in allowing for terms that combine options lists though. For example, allow for a term merge(Opts1, Opts2), that will combine the option lists in a well defined way. Now, the name merge is bad as it does not tell which one wins (I always forget the argument order of merge_options/3), but the idea stands. Options need to be processed by a library anyway as member/2, etc. take the wrong leftmost wins.

Note that this could also lead to e.g. (bad names) first(List) or last(List), so you can overrule an option using first([k(v)|Opts)]).

Could something based on these ideas, together with (incomplete) static analysis and warnings/errors in the option processing library be a way out?

[k(v)|Opts] in the current design would become Opts,[k(v)] with my Options,DefaultOptions suggestion.

This would work (also with last, warning, error, handle; where hook provides a predicate to predicate to handle errors)

static analysis is something I’ve wanted to work on (and also type inferencing) but keep on getting sidetracked. What are the best static analysis tools these days?

Dicitionaries would make the code harder for the average population.
I was recently looking at the pack installer, which I think uses dicts, and it was much harder to figure out what was going on.

pack(options) takes the leftmost but it is flexible in allowing options that can appear multiple times.

I did n’t do extensive research when coding pack(options). My intuition probably was that
optionised predicates most times/options have defaults. As lists are normally added to at the head then it seemed natural that invocation options will be added to the left.
Leftmost on a list is also most efficient when looking at lists to pick up the first matching term.

Nicos Angelopoulos

pack(options):
"options" pack for SWI-Prolog
options: Options handling.

I don’t know. This is mostly about data flow analysis such that you can figure out what is eventually passed to a predicate processing options. library(predicate_options) provides a small start. At the moment, I’m using Claude code to

  • Get rid of hand crafted option processing. This implies that Prolog code should use library(option), C code PL_scan_options(). This should have no or minimal impact. It mostly provides consistency (where C and Prolog code behave different, but that is what we are trying to resolve).
  • Rewrite predicates that allow for repeated options to use a single option with a list value. This provides a compatibility and deprecated warning. For example archive_open/4 allowed for multiple filter(+Filter) options. It now implements filters(List) and a small wrapper that maps this convention and emits a deprecated warning.
  • Complete :- predicate_options/3 declarations that allow check/0 (delegating to check_predicate_options/0) and the highlight library to flag unknown options or wrong values. It also tells us which built-ins and library predicates process options on what argument.
  • Extend linter checking in library(predicate_options) to find patterns where options are combined and thus ordering can matter. Eventually, I want to get rid of direct list manipulation on option lists and provide the tooling that allows user code to do the same with little effort.

That should give a fair picture on the issues as well as improve static checking and improve the consistency of the system (dicts should work anywhere once this is complete).

The pack installer use library(option) exclusively. It does represent its knowledge about packs as dicts, i.e., knowledge about already installed packs, available packs and installation plans. I don’t see any representation that would have made this more readable. That said, the code has been developed bottom up for a long time and got more and more complicated :frowning: Possibly it should be reconsidered … Especially when foreign code is involved though, packaging is complicated as I experienced trying to create the Janus pack for Python …