Documentation |
Data type of data types
DOM_DOMAIN is the data type of datatypes.
Each MuPAD^{®} object has a unique data type. Since a data type is a MuPAD object, too, it must itself have a data type; the data type comprising all data types (including itself) is DOM_DOMAIN.
There are two kinds of elements of DOM_DOMAIN: data types of the kernel, and data types defined in the library or by the user (domains). Objects that have a data type of the latter kind are called domain elements.
A data type has the same internal structure as a table; its entries are called slots. One particular slot is the key; no two different data types can have the same key. Most of the other slots determine how arguments of that data type are handled by functions.
Once a user-defined domain has been constructed, it cannot be destroyed.
Our first example stems from ethnology: some languages in Polynesia do not have words for numbers greater than three; every integer greater than three is denoted by the word "many". Hence two plus two does not equal four but "many". We are going to implement a domain for this kind of integers; in other words, we are going to implement a data type for the finite set {1, 2, 3, many}.
S := newDomain("Polynesian integer")
At this point, we have defined a new data type: a MuPAD object can be a Polynesian integer now. No operations are available yet; the domain consists of its key only:
op(S)
Even though there are no methods for input and output of domain elements yet, Polynesian integers can be entered and displayed right now. You have to use the function new for defining domain elements:
x := new(S, 5)
Now, x is a Polynesian integer:
type(x)
Of course, MuPAD cannot know what meaning a Polynesian integer has and what its internal structure should be. The arguments of the call to the function new are just stored as the zeroth, first, etc. operand of the domain element, without checking them. You may call new with as many arguments as you want:
new(S, 1, 2, 3, 4); op(%)
new cannot know that Polynesian integers should have exactly one operand and that we want 5 to be replaced by many. To achieve this, we implement our own method "new"; this also allows us to check the argument. We have one more problem: domain methods should refer to the domain; but they should not depend on the fact that the domain is currently stored in S. For this purpose, MuPAD has a special local variable dom that always refers to the domain a procedure belongs to:
S::new := proc(i : Type::PosInt) begin if args(0) <> 1 then error("There must be exactly one argument") end_if; if i > 3 then new(dom, hold(many)) else new(dom, i) end_if end_proc:
A function call to the domain such as S(5) now implicitly calls the "new" method:
S(5)
S("nonsense")
Error: The object '"nonsense"' is incorrect. The type of argument number 1 must be 'Type::PosInt'. Evaluating: S::new
In the next step, we define our own output method. A Polynesian integer i, say, shall not be printed as new(Polynesian integer, i), only its internal value 1, 2, 3, or many shall appear on the screen. Note that this value is the first operand of the data structure:
S::print := proc(x) begin op(x, 1) end_proc: S(1), S(2), S(3), S(4), S(5)
By now, the input and output of elements of S have been defined. It remains to define how the functions and operators of MuPAD should react to Polynesian integers. This is done by overloading them. However, it is not necessary to overload each of the thousands of functions of MuPAD; for some of them, the default behavior is acceptable. For example, expression manipulation functions leave domain elements unaltered:
x := S(5): expand(x), simplify(x), combine(x); delete x:
Arithmetical operations handle domain elements like identifiers; they automatically apply the associative and commutative law for addition and multiplication:
(S(3) + S(2)) + S(4)
In our case, this is not what we want. So we have to overload the operator +. Operators are overloaded by overloading the corresponding "underline-functions"; hence, we have to write a method "_plus":
S::_plus := proc() local argv; begin argv := map([args()], op, 1); if has(argv, hold(many)) then new(dom, hold(many)) else dom(_plus(op(argv))) end_if end_proc:
Now, the sum of Polynesian integers calls this slot:
S(1) + S(2), S(2) + S(3) + S(7)
Deleting the identifier S does not destroy our domain. It can still be reconstructed using newDomain.
delete S: op(newDomain("Polynesian integer"))
We could now give a similar example for more advanced Polynesian mathematics with numbers up to ten, say. This leads to the question whether it is necessary to enter all the code again and again whenever we decide to count a bit farther. It is not; this is one of the advantages of domain constructors. A domain constructor may be regarded as a function that returns a domain depending on some input parameters. It has several additional features. Firstly, the additional keywords category and axiom are available for specifying the mathematical structure of the domain; in our case, we have the structure of a commutative semigroup where different domain elements have different mathematical meanings (we call this a domain with a canonical representation). Secondly, an initialization part may be defined that is executed exactly once for every domain returned by the constructor; it should at least check the parameters passed to the constructor. Each domain created in such a way may inherit methods from other domains, and it must at least inherit the methods of Dom::BaseDomain.
domain CountingUpTo(n : Type::PosInt) inherits Dom::BaseDomain; category Cat::AbelianSemiGroup; axiom Ax::canonicalRep; new := proc(x : Type::PosInt) begin if args(0) <> 1 then error("There must be exactly one argument") end_if; if x > n then new(dom, hold(many)) else new(dom, x) end_if end_proc; print := proc(x) begin op(x, 1) end_proc; _plus := proc() local argv; begin argv:= map([args()], op, 1); if has(argv, hold(many)) then new(dom, hold(many)) else dom(_plus(op(argv))) end_if end_proc; // initialization part begin if args(0) <> 1 then error("Wrong number of arguments") end_if; end:
Now, CountingUpTo is a domain constructor:
type(CountingUpTo)
We have defined the domain constructor CountingUpTo, but we have not created a domain yet. This is done by calling the constructor:
CountingUpToNine := CountingUpTo(9); CountingUpToTen := CountingUpTo(10)
We are now able to create, output, and manipulate domain elements as in the previous example:
x := CountingUpToNine(3): y := CountingUpToNine(7): x, x + x, y, x + y, y + y
x := CountingUpToTen(3): y := CountingUpToTen(7): x, x + x, y, x + y, y + y
delete CountingUpToNine, CountingUpToTen, CountingUpTo, x, y:
No domain constructor with the same name may be used again during the same session.
Suppose that your domain does not really depend on a parameter, but that you need some of the other features of domain constructors. Then you may define a domain constructor dc, say, that is called without parameters. From such a domain constructor, you can construct exactly one domain dc(). Instead of defining the constructor via domain dc() ... end first and then using d := dc() to construct the domain d, say, you may directly enter domain d ... end, thereby saving some work.
Continuing the previous examples, suppose that we want to count up to three, knowing that we never want to count farther. However, we want to declare our domain to be an Abelian semigroup with a canonical representation of the elements. This is not possible with a construction of the domain using newDomain as in Example 1: we have to use the keyword domain. You will notice at once that the following source code is almost identical to the one in the previous example—we just removed the dependence on the parameter n.
domain CountingUpToThree inherits Dom::BaseDomain; category Cat::AbelianSemiGroup; axiom Ax::canonicalRep; new := proc(x : Type::PosInt) begin if args(0) <> 1 then error("There must be exactly one argument") end_if; if x > 3 then new(dom, hold(many)) else new(dom, x) end_if end_proc; print := proc(x) begin op(x, 1) end_proc; _plus := proc() local argv; begin argv:= map([args()], op, 1); if has(argv, hold(many)) then new(dom, hold(many)) else dom(_plus(op(argv))) end_if end_proc; end:
Now, CountingUpToThree is a domain and not a domain contructor:
type(CountingUpToThree)
You may use this domain in the same way as CountingUpTo(3) in Example 2.
When called as a function, the data type creates a new object of this data type out of the arguments of the call. E.g., the call DOM_LIST(1, 2, x) generates the list [1, 2, x] of domain type DOM_LIST (although, in this case, you probably prefer to type in [1, 2, x] directly which results in the same object). It depends on the particular type which arguments are admitted here.
In the case of a domain, the "new" method of that domain is called.
You can obtain the slots of a domain using slot. The function slot can also be used on the left hand side of an assignment to define new slots, or to re-define existing slots. Use delete to delete slots.
A data type consists of an arbitrary number of equations (objects of type "equal"). If a = b is among these equations, we say that the slota of the data type equals b. By convention, a is usually a string. Each domain has at least one slot indexed by "key".
The names of the data types provided by the MuPAD kernel are of the form DOM_XXX, such us DOM_ARRAY, DOM_HFARRAY, DOM_IDENT, DOM_INT, DOM_LIST, DOM_TABLE etc.
You can create further data types using the function newDomain (cf. Example 1) or via the keyword domain (cf. Example 3).
You can also create new data types by calling a domain constructor. Various pre-defined domain constructors can be found in the library Dom. You can also define your own domain constructors using the keyword domain. Cf. Example 2.
The domain type (data type) of any MuPAD object can be queried by the function domtype.