Agda-2.3.2.2: notes/fixity-declarations
Suggestion for how operator fixity should be specified
------------------------------------------------------
Nils Anders Danielsson
(By fixity I mean associativity and precedence.)
The current scheme is a mess. With Unicode symbols and mixfix
operators users (such as myself) tend to define more operators than in
Haskell, and then the Haskell fixity handling is too limited. It is
very hard to get an overview over a total ordering which specifies how
tight every operator binds in comparison to every other.
This note describes a way to avoid these problems. The solution is not
perfect--some limitations are discussed towards the end--but it is
quite lightweight, so it should be relatively easy to implement and
try out.
New approach
------------
Associativity can be specified just as before. An operator is either
left associative, right associative, or nonassociative. (Note that
only infix operators can be left or right associative; pre- and
postfix operators are always nonassociative.)
The basic idea of the new approach to precedence handling is to
abandon the current total order and instead have a partial order of
precedences. This is an old idea, which is easy to understand. The
basic difference compared to the current scheme is that two operators
of noncomparable precedence cannot be used next to each other without
inserting parentheses. The only crux is to find a good way of
specifying the precedences.
I believe that it is a good idea if the precedence of an operator can
be understood locally, so I suggest that one should only be allowed to
specify precedences at the binding site (in the defining module) of an
operator, conservatively extended when new operators are defined.
(This rules out having "first class precedences", where the
precedences of an operator are free-standing entities which can be
exported and imported separately from the operators themselves.)
Precedences are defined for an operator • by relating it to
previously defined operators. This can be done in three ways:
* infix[lr ] • binds as ∘
This means that • (which is left, right or nonassociative) binds in
exactly the same way as ∘.
* infix[lr ] • binds tighter-than (op₁…) looser-than (op₂…)
This means that • binds strictly tighter than op₁… and strictly
looser than op₂…. The two parts tighter-than (…) and looser-than (…)
can be given in any order, and one of them can be omitted.
This declaration is only valid if it does not change the relations
of any previously declared operators, i.e., if the precedence
relation before this declaration is denoted by ⊰ and the one after
this declaration by ⊰′, then the following property must hold:
∀ op₁ ≠ •. ∀ op₂ ≠ •. op₁ ⊰ op₂ ⇔ op₁ ⊰′ op₂.
This property ensures some degree of locality for the precedences:
To see if/how two operators are related it is enough to inspect the
fixity declarations of these two operators, plus those of the
operators referred to in these declarations (transitively). It is
impossible for an unrelated fixity declaration to change this
relation.
* infix[lr ] •
With this declaration • becomes unrelated to all other operators.
* No fixity declaration for • is the same as specifying "infix •",
i.e. • becomes nonassociative and unrelated to all other operators.
It should also be possible to combine the fixity declarations of
several operators, for instance as follows:
infixl _op₁_ _op₂_ _op₃ binds looser-than (_+_)
This is equivalent to the following three declarations:
infixl _op₁_ binds looser-than (_+_)
infixl _op₂_ binds as _op₁_
infixl _op₃ binds as _op₁_
(Note the use of _op₁_ in the last two declarations.)
Some minor details
------------------
Some minor details (as compared to the current fixity handling in
Agda):
* Non-operator (function) symbols should still bind tighter than
everything else.
* Fixity declarations should of course be scope checked, and an error
given if a fixity declaration is given for an operator which is not
in scope.
* It should be possible to give fixity declarations to record fields,
for instance as follows:
infix Setoid._≈_ binds as _≡_
infixl Ring._-_ binds as Ring._+_
* I do not like the fact that, for operators of the same precedence,
the following sub-order of precedence is used:
* postfix
* prefix
* infix right associative
* infix left associative
* infix nonassociative
As an example, take the following fixity declarations:
infixr 6 _∷_
infixl 6 _+_
infix 6 -_ _!
Currently they result in 5 + 6 ∷ [], - 5 + 6 and - 2 ! parsing as (5
+ 6) ∷ [], (- 5) + 6 and - (2 !), even though these expressions
should not parse at all.
Limitations
-----------
The scheme outlined above has a limitation, demonstrated by the
following example:
Let us say that two libraries, one for sets and one for arithmetic,
are developed independently. It is probably unreasonable (if one
wants to keep unrelated code separate) for one of these libraries to
depend on the other. Hence expressions such as the following won't
parse: a + b ∈ c. Parentheses will have to be used: (a + b) ∈ c.
To me this example is not very convincing, though. If the two
libraries are really separate, then there should not be _any_
connection between them. If, on the other hand, it is a requirement
that _+_ should really bind tighter than _∈_, then the libraries are
not unrelated, but one should import the other.
Another possible problem with the scheme outlined above is its
implementation. Currently mixfix operator parsing is implemented in
Agda (more or less) as follows:
* Expressions are first parsed as if every operator was a function
symbol. This yields parse trees (rose trees) which need to be
post-processed.
* Operator parsing is then done as part of scope-checking. For every
symbol sequence (list in the rose tree) in a parsed expression a
dedicated parser is generated based on which operator parts are
present in the sequence. Scope information is needed for this step,
since the relative precedences of the operators and also their
associativity are used to construct the parser. The symbol sequence
is then parsed using this dedicated parser.
It is currently unclear whether this implementation method can be made
efficient for the fixity handling scheme outlined above.
Conclusion
----------
If the implementation can be made efficient, then I believe that the
scheme outlined above is strictly better than the one we have. It is
also easy to understand. In other words, I will start thinking about
the implementation.
------------------------------------------------------------------------
Improved syntax
------------------------------------------------------------------------
infix [ε|left|right] <operators>
[ε|binds [as <operator>
|looser than <operators>
|tighter than <operators>
|looser than <operators> tighter than <operators>
|tighter than <operators> looser than <operators>
]]
------------------------------------------------------------------------
Refinement
------------------------------------------------------------------------
Ulf commented that the scheme above is too inflexible. If (the already
existing) library A defines _+_, and library B defines _&_ (which is
unrelated to _+_), then it is impossible to define _==_ in library C
in such a way that _+_ binds tighter than _==_, which in turn binds
tighter than _&_. In order to accommodate this, let us drop
transitivity.
Details (based on discussions with Ulf):
Precedence relations are DAGs, whose nodes are annotated with sets
of operators. Let node(•) be the node of operator • (if any), and
let n₁ ⊰ n₂ mean that there is an edge from node n₁ to node n₂.
Fixity declarations get the following meanings:
• binds as ∘:
• is added to node(∘).
• binds looser than ∘₁ tighter than ∘₂:
A new node annotated with {•} is added,
plus an edge from node(∘₁) and an edge from node n for all n with
n ⊰ node(∘₁),
plus an edge to node(∘₂) and an edge to node n for all n with
node(∘₂) ⊰ n.
Note that this does not create any new dependencies between ∘₁ and
∘₂, but • inherits earlier dependencies.
A precedence relation now gives rise to a context free grammar in
the following way:
* The top-level production is as follows:
expr ∷= <atom> | ⋁ {n | n is a node in the graph}
* For every node n the following productions are added:
n ∷= prefix-op⁺ n↑
| n↑ postfix-op⁺
| n↑ infix-non-assoc-op n↑
| (n↑ infix-left-assoc-op)⁺ n↑
| n↑ (infix-right-assoc-op n↑)⁺
n↑ ∷= <atom> | ⋁ {n' | n ⊰ n'}
x-op ∷= ⋁ {op-prod | op is an "x" operator annotating n}
op-prod ∷= op₁ expr op₂ expr op₃ … op_n
(where op_i are the name parts of the mixfix operator op)
Note that if all operator name parts are unique, and <atom>s don't
introduce any ambiguity, then the grammar is unambiguous. However,
we don't want to require all operator name parts to be unique, since
this can be rather inflexible. (Consider a DSEL containing both
if_then_ and if_then_else_, for instance. Or the two operators ⟦_⟧_
and ⟦_⟧'_.) All ambiguous parses will be rejected, in many cases
with an error message listing all possible parses:
Ambiguous parse. Could mean any of the following:
if x then (if y then a) else b
if x then (if y then a else b)
We expect there to be rather few cases of ambiguity. A large number
of potentially ambiguous operators will make it harder to write
syntactically correct programs, and programmers will presumably be
reluctant to subject themselves to this situation.
------------------------------------------------------------------------
Sections
------------------------------------------------------------------------
We can also support sections. Some examples will outline how this can
be accomplished:
If we have
_+_ : ...
then 5 +_ and _+ 3 are sections. They stand for
\x -> 5 + x
and
\x -> x + 3,
respectively. Note that +_ becomes a postfix operator, and _+ a
prefix operator. Note also that _+_ can be viewed as a section, and
does not need to be treated as a special case. (The qualified
variant M._+_ still needs special treatment, though.)
All mixfix operators can be sectioned. For instance, if we have
if_then_else_ : ...
then if_then x else y stands for
\b -> if b then x else y.
Parsing of sections is accomplished by letting the lexer distinguish
different uses of '_':
* As a wildcard.
* At the beginning of an operator.
* In the middle of an operator.
* At the end of an operator.
The different uses can be distinguished by examining surrounding
white space.
------------------------------------------------------------------------
Open questions
------------------------------------------------------------------------
* What is the sub-class of DAGs that the declarations introduced above
can give rise to? Not all DAGs can be constructed in this way. Take
•⟶•⟶•⟶•, for instance. Could this be overly limiting?
* Does the order of the declarations matter? If it does, then the
scheme should be changed, since otherwise we would have a
non-declarative language for specifying fixities. (It would not be
very nice if the relative precedence of two operators depended on in
which order two modules were imported, for instance.)
Order does not matter for this simple example:
infix _≡_
infix left _+_ binds tighter than _≡_
infix ¬_ binds looser than _≡_
The declarations give rise to the following precedence graph:
╭─────╮
│ _+_ ├⟵╮
╰──┬──╯ │
↑ │
╭──┴──╮ │
│ _≡_ │ │
╰──┬──╯ │
↑ │
╭──┴──╮ │
│ ¬_ ├─╯
╰─────╯
If the order of the declarations is changed to
infix _≡_
infix ¬_ binds looser than _≡_
infix left _+_ binds tighter than _≡_
we still get the same graph. Is this generally true?
------------------------------------------------------------------------
Summary of important "correctness" criteria
------------------------------------------------------------------------
• Adding a new declaration should not change the relations between
previously declared operators.
• If declarations can be reordered, then the semantics must be
independent of their order.
------------------------------------------------------------------------
A possible problem with the scheme above
------------------------------------------------------------------------
Consider the following (Agda-like) modules:
module A where
infix _*_
module B where
import A
infix _^_ binds tighter-than (_*_)
module C where
import A
infix _+_ binds looser-than (_*_)
module D where
import B; import C
In D, do we have node(_+_) ⊰ node(_^_)? If not, then order of
declarations does (in some sense) matter, since putting the two
declarations in the same module would lead to a different result.
However, if we do have node(_+_) ⊰ node(_^_), then the relationship
between the two operators is not fixed until they are brought into the
same scope. Neither scenario feels appealing.
------------------------------------------------------------------------
Refinement of the refinement
------------------------------------------------------------------------
I am compelled to remove the transitivity emulation from fixity
declarations. It is too hard to understand. To start with we can
require the user to specify every relationship explicitly. If this
should turn out to require too much work, then the following
extensions seem promising:
• One could invent some notation for specifying the fixity of several
operator groups at once, for instance:
infix (_+_ _-_) < (_*_ _/_) < (_^_)
The different groups in this kind of declaration would be
transitively related.
• One could specify a /module/ in an operator list; this would stand
for all the operators exported from the module (top-level, plus
perhaps records). Note that this may be a bit coarse. If Agda's open
public was more like Haskell's re-exports, then it would be easy to
use the module system to package operators for inclusion in fixity
declarations, though.