DOM_DOMAIN
-- the data type
of data typesEach MuPAD object is of 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.
The names of the data types provided by the MuPAD kernel are
of the form DOM_XXX
, such us DOM_ARRAY
, 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
.
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.
Evaluating an object of type DOM_DOMAIN
returns
itself.
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.
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 slot a
of the data
type equals b
. By convention, a
is usually a
string. Each domain has at least one slot indexed by
"key"
.
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")
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)
"key" = "Polynesian integer"
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)
new(Polynesian integer, 5)
Now, x
is a Polynesian integer:
>> type(x)
Polynesian integer
Of course, MuPAD cannot know what a Polynesian
integer stands for 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(Polynesian integer, 1, 2, 3, 4) 1, 2, 3, 4
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)
new(Polynesian integer, many)
>> S("nonsense")
Error: Wrong type of 1. argument (type 'Type::PosInt' expected\ , got argument '"nonsense"'); during evaluation of '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)
1, 2, 3, many, many
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:
many, many, many
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)
many + 2 + 3
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)
3, many
Deleting the identifier S
does not destroy
our domain. It can still be reconstructed using newDomain
.
>> delete S: op(newDomain("Polynesian integer"))
"_plus" = proc S::_plus() ... end, "print" = proc S::print(x) ... end, "new" = proc S::new(i) ... end, "key" = "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
. You find more
detailed information in the domains
reference.
>> 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)
DomainConstructor
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)
CountingUpTo(9) 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
3, 6, 7, many, many
>> x := CountingUpToTen(3): y := CountingUpToTen(7): x, x + x, y, x + y, y + y
3, 6, 7, 10, many
>> 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)
DOM_DOMAIN
You may use this domain in the same way as
CountingUpTo(3)
in example 2.
S
and T
, say, assigning or deleting
a slot slot(S, a)
implicitly also changes slot(T,
a)
(reference effect). This also holds if a =
"key"
.
You get no warning even if T
is
protected.