operator declarations

supports unary!
$ ./main.py --parse "piracetam -> + ACh"

each operator has a sybmol and one or more definitions

an operator definition has the:
meaning
arity
spacing
operands
reducer

e.g. operator declaration

    + :
     - means: plus
       arity: n-ary
       spacing: 0,
       operands:
        - '[0-9]*?[\.\,]?[0-9]+?'
       reduce: join

     - means: more
       arity: unary
       reduce: prefix

"arity" is only used by Op to pick the subclass
(each Op-subclass is a different arity)

operator precedence is by the operator symbol

Op : Hashable
`Op.id` : immutable
`Op.id` is used for hashing and equality
what uniquely identifies an operator (for parsing)?
 * symbol (e.g. "-")
 * arity (e.g. binary)
 * spaces (e.g. 0)
which is what `Op.id` has

Op : Copyable

`repr(Op)`
shows kwargs, hides defaults

Op methods
move from subclass to superclass

HACK:
in `Op.__init__`, `self.__dict__['means']` instead of `self.means` because tuples are immutable

the config code must be independent

Op.spacing : Range

use `Op.default()` for cleaner output
nodes!=nouns edges!=verbs

dispatch an `@edge` by `Op.means`
an `@edge` can yield zero-one-or-more `Edge`s per `(operator, operand)`

e.g.

    $ ./main.py --parse "piracetam -> + ACh"

    nodes  | ACh
           | + ACh
           | piracetam
           |
    edges  | ('more', 'ACh', '+ ACh')
           | ('causes', 'piracetam', '+ ACh')

    "+ ACh" makes a edge between "acetylcholine" and
    "more acetylcholine" because they are obviously related.
    (and "piracetam" still causes "more acetylcholine")

@typecheck ignores strings (as annotation for the human)