Custom Operators

This tutorial will demonstrate how to implement custom operators using the operator Interface. This interface can be used to implement operators with custom behavior such as:

  • Number of parameters
  • Lazy and eager evaluation
  • Semantics
  • Associativity
  • Initial value
  • Pretty printing
  • Side-effects
Warning

This tutorial is not yet polished. This interface is incomplete and will be changed in v0.4.

Setup

Implementing an operator requires defining methods for that operator. To do so, their function names must be imported or prefixed by the Interface module. This module also exports several other required and useful functions.

julia> import PAndQ:
           Associativity, Evaluation, arity, dual, evaluate,
           initial_value, parenthesize, print_expression, symbol
julia> using PAndQ, .Interface

Nullary

This is a renamed tautology operator. First, define an Operator. If possible, this should be a constant whose name corresponds to the operator name.

julia> const truth = Operator{:truth}()
Error showing value of type Operator{:truth}:
ERROR: InterfaceError: implement `symbol` for `Operator{:truth}()`

If a required method is not implemented, a runtime error will display the function and operator that a method must be implemented for. The error says to implement symbol. This function is used to print an operator.

julia> symbol(::typeof(truth)) = "truth";
julia> truthtruth
julia> truth()ERROR: InterfaceError: implement `PAndQ.Interface.Evaluation` for `PAndQ.Interface.Operator{:truth}()`

The error says to implement Evaluation. This function is used to specify whether an operator lazily or eagerly evaluates its arguments.

julia> Evaluation(::typeof(truth)) = Lazy;
julia> truth()ERROR: InterfaceError: implement `arity` for `PAndQ.Interface.Operator{:truth}()`

The error says to implement arity. This function is used to construct a node in a syntax tree.

julia> arity(::typeof(truth)) = 0;

julia> truth()
Error showing value of type PAndQ.Tree{0}:
ERROR: InterfaceError: implement `print_expression` for `Operator{:truth}()` with `0` propositions

The error says to implement print_expression. This function is used to print a node of a syntax tree.

julia> print_expression(io, o::typeof(truth)) = show(io, "text/plain", o);
julia> print_table(truth())ERROR: InterfaceError: implement `evaluate` for `PAndQ.Interface.Operator{:truth}()` with `0` propositions

The error says to implement evaluate. This function is used to specify the semantics of an operator.

julia> evaluate(::typeof(truth)) = ⊤;
julia> print_table(truth())┌───────┐ │ truth │ ├───────┤ │ ⊤ │ └───────┘

Unary

This is an eagerly evaluated not operator.

julia> const negate = Operator{:negate}();
julia> symbol(::typeof(negate)) = "negate";
julia> negatenegate
julia> Evaluation(::typeof(negate)) = Eager;
julia> evaluate(::typeof(negate), p) = evaluate(¬, p);
julia> @atomize negate(¬p)p
julia> @atomize print_table(negate(p))┌───┬────┐ │ p ¬p │ ├───┼────┤ │ ⊤ │ ⊥ │ │ ⊥ │ ⊤ │ └───┴────┘

Binary

This is an imply operator represented by the --> symbol.

julia> const if_then = --> = Operator{:if_then}();
julia> symbol(::typeof(-->)) = "-->";
julia> -->-->
julia> Evaluation(::typeof(-->)) = Lazy;
julia> arity(::typeof(-->)) = 2;

If a node in a syntax tree is not the root node, it may be necessary to parenthesize it to avoid ambiguity. The parenthesize function is used to print parentheses around a node if it is not the root node. The print_proposition function is used to print the propositions in a node.

julia> print_expression(io, o::typeof(-->), p, q) = parenthesize(io) do
           print_proposition(io, p)
           print(io, " ")
           show(io, "text/plain", o)
           print(io, " ")
           print_proposition(io, q)
       end;
julia> @atomize p --> qp --> q
julia> evaluate(::typeof(-->), p, q) = p → q;
julia> @atomize print_table(p --> q)┌───┬───┬─────────┐ │ p q p --> q │ ├───┼───┼─────────┤ │ ⊤ │ ⊤ │ ⊤ │ │ ⊥ │ ⊤ │ ⊤ │ ├───┼───┼─────────┤ │ ⊤ │ ⊥ │ ⊥ │ │ ⊥ │ ⊥ │ ⊤ │ └───┴───┴─────────┘
julia> @atomize fold(𝒾, (-->) => ())ERROR: InterfaceError: implement `PAndQ.Interface.Associativity` for `PAndQ.Interface.Operator{:if_then}()`

This error says to implement Associativity. This function is used to determine which direction to fold.

julia> Associativity(::typeof(-->)) = Left;
julia> @atomize fold(𝒾, (-->) => ())ERROR: InterfaceError: implement `initial_value` for `PAndQ.Interface.Operator{:if_then}()`

This error says to implement initial_value. This function is used to determine the init parameter when folding.

julia> initial_value(::typeof(-->)) = Some(⊤);
julia> @atomize fold(𝒾, (-->) => ())
julia> @atomize fold(𝒾, (-->) => (p, q, r))(p --> q) --> r

Ternary

This is a lazily evaluated conditional operator.

julia> const conditional = Operator{:conditional}();
julia> symbol(::typeof(conditional)) = "?";
julia> conditional?
julia> Evaluation(::typeof(conditional)) = Lazy;
julia> arity(::typeof(conditional)) = 3;
julia> print_expression(io, o::typeof(conditional), p, q, r) = parenthesize(io) do print_proposition(io, p) print(io, " ? ") print_proposition(io, q) print(io, " : ") print_proposition(io, r) end;
julia> @atomize conditional(p, q, r)p ? q : r
julia> evaluate(::typeof(conditional), p, q, r) = (p → q) ∧ (p ∨ r);
julia> @atomize print_table(conditional(p, q, r))┌───┬───┬───┬───────────┐ │ p q r p ? q : r │ ├───┼───┼───┼───────────┤ │ ⊤ │ ⊤ │ ⊤ │ ⊤ │ │ ⊥ │ ⊤ │ ⊤ │ ⊤ │ ├───┼───┼───┼───────────┤ │ ⊤ │ ⊥ │ ⊤ │ ⊥ │ │ ⊥ │ ⊥ │ ⊤ │ ⊤ │ ├───┼───┼───┼───────────┤ │ ⊤ │ ⊤ │ ⊥ │ ⊤ │ │ ⊥ │ ⊤ │ ⊥ │ ⊥ │ ├───┼───┼───┼───────────┤ │ ⊤ │ ⊥ │ ⊥ │ ⊥ │ │ ⊥ │ ⊥ │ ⊥ │ ⊥ │ └───┴───┴───┴───────────┘