User-Defined Types
User-defined types are a significant advantage for any serious programmer, and can even be useful to less experienced programmers. At their simplest, user-defined types may consist of little more than a combination of value types that can be used together as a single unit. A perfect example of this might be a structure containing locale information. This type of structure would need to be passed to any function that is going to format a number, a date, or a time for presentation to the user and also to convert such data from the string representation provided by the user to an appropriate value or object type in the program. Here is what such a structure might look like:
type tLocaleInfo string sDecimalSep embed string sThousandsSep embed string sListSep embed
As can be seen from this example, it would be quite a bit more convenient passing around a single piece of information that contains all of the things that are important with respect to the locale than to have to pass each of these pieces of information around separately and to address them and store them separately as well. The example shown is a very simplistic implementation and does not include information about formatting dates or times, since these are also considerably more complicated than mere numeric formatting.
Type definitions must be located outside of any function, but they do not need to precede the function. The type definitions could be located in an include file and just be added on to the end of the program. Below is a gradual introduction to using user-defined types. Follow it through step-by-step and it should be failry clear at the end. The examples that include functions can even be tried out.
type mytype string m1 end type
In the type mytype, the string parameter contains a reference to a string object, it does not contain an actual string object.
type mytype1 string m1 embed end type
The mytype1 type contains an actual string object, not a reference.
To use the two types above see the following code:
function main() mytype m string s m =@ mytype.new() s = "hello" m.m1 =@ s s = "foo" end function m.m1
This will return foo, since
m.m1
contains a reference to
s
.
function main() mytype1 m m =@ mytype1.new() m.m1 = "hello" end function m.m1
This will return hello, since
m.m1
is an embedded string.
function main() mytype m m =@ mytype.new() m.m1 = "hello" end function m.m1
This will result in an error, since hello
is not an
object, it is a value and this type can only hold a reference to an object.
type mytype2 mytype mm1 embed mytype mm2 end type function main() mytype2 m string s m =@ mytype2.new() s = "hello" m.mm1 =@ mytype.new() m.mm1.m1 =@ s end function m.mm1.m1
This will result in an error 52, since the type mytype has not been defined as embeddable. The error not embeddable will be generated.
type mytype embed string m1 end type type mytype2 mytype mm1 embed mytype mm2 end type function main() mytype2 m string s m =@ mytype2.new() s = "hello" m.mm1.m1 =@ s end function m.mm1.m1
The above should now work as expected.
type mytype embed string m1 embed end type type mytype2 embed mytype mm1 mytype mm2 end type function main() mytype2 m m =@ mytype2.new() m.mm1.m1 = "hello" m.mm2.m1 = m.mm1.m1 m.mm2.m1 = .lstr(m.mm2.m1, 2) end function m.mm1.m2
This should also work and produce an output of
he
.
type mytype string m1 mytype next end type function main() string s mytype m,mfirst s = "hello" m =@ mytype.new() mfirst =@ m m.next =@ mytype.new() m =@ m.next m.next =@ mytype.new() m =@ m.next m.next =@ mytype.new() m =@ m.next m.next =@ mytype.new() m =@ m.next m.m1 =@ s m =@ mfirst mfirst =@ .nul end function m.next.next.next.next.m1
This is an example of a singly-linked list which should return hello. The ability to include references to the same type as that being defined makes it possible to create complex data structures in memory, such as lists and trees.
A final note about embedded objects. Some objects cannot be embedded, such as fsfileinputstream or fsfileoutputstream or any of the ppcstype1 objects, mainly because they cannot be initialized by calling their new function. However, references to any object type can be part of a type definition.
The previous types consisted only of values and references to values but did not
include methods. A more powerful kind of user-defined type is one that includes
methods. Any user-defined type can also have a user-defined
new()
method that allows the programmer to do initialization
of the newly created object when it is created. To create the methods, the functions
must be defined in the same module (compilation object) as that where the type is
defined and they must follow the type definition in the code file. It is defined by
using the type name followed by the dot operator followed by the function name.
The first argument to the function must be the type itself
and in the case of the new()
method it must return the object
of the type that was passed in, otherwise the assignment to the variable will fail.
See the example that follows:
type tCustInfo export string sCustID embed string sFirstname embed string sLastname embed datetime dtCreated embed string sCreatedBy embed function copy end type function tCustInfo.new(tCustInfo me, string sCreatedBy) me.dtCreated.setnow() me.sCreatedBy = sCreatedBy end function me function tCustInfo.copy(tCustInfo me) tCustInfo copy copy =@ tCustInfo.new(me.sCreatedBy) copy.sCustID = me.sCustID copy.sFirstname = me.sFirstname copy.sLastname = me.sLastname copy.dtCreated = me.dtCreated end function copy
The preceding example shows a user-defined type that implements a
new()
method and a copy()
method. The copy()
method is implemented so
that it produces an exact copy rather than a copy with a potentially
new creator ID and new creation datetime. Typically such types will be
defined and implemented in a single code file and then compiled as a
SIMPOL pre-compiled module file that can be added to a project either
at compilation or at runtime. That is the purpose of the export keyword
in the type definition, to ensure that the type is visible outside the module.
The functions do not require the export keyword since they are made
available within the type.
If the new()
method is listed inside the type definition
then it can be called again to reinitialize the type. It is not necessary to
list it, however, unless it should be possible to call at some point other than
during the initial call to create the object.
Another important issue is the proper use of the keywords embed
,
reference
, and resolve
. By default,
properties added to a type definition are references to items of a specified type
(or any type using type(*)
, any value type by using
type(=)
, or any matching tagged type when using
type(<tagname>)
). To make a property embedded, the
embed
keyword can be added to the end of the statement. To
switch the default from by reference to embedded, the embed
keyword can be placed inside the type definition on a line of its own. To switch
back, place the reference
keyword on a line by itself. These
switches only apply within a type definition. The change of the default resets
to "by reference" after exiting a type definition. The resolve
keyword is used for a very special situation. Normally properties that are not
embedded are not examined when trying to resolve the name of a property or method,
but if the resolve
keyword is added to the end of the property
definition, then at runtime that property will be included when searching for a
property or method that is not listed at the first level of the type definition.
Let's look at a small example of this:
type myform form1 f string sFormname embed end type type myapplication myform mf resolve embed string sUsername datetime dtStart end type function main() myapplication app app =@ myapplication.new() ⋮ app.addcontrol(…) ⋮ end function
Normally it would not be possible to call a method of the form1 object
without directly referencing the f, but the use of the
resolve
keyword allows this. However, if the form1
object has not yet been initialized this will result in a runtime error number
21, "Object not found". Using the
resolve
keyword can help in creating powerful and easy-to-use
types, but it is important that the types are designed in such a way as to minimize
the likelyhood that those portions marked with resolve
will
cause an error because they are uninitialized. That might mean that the type's
new()
method takes parameters that allow the correct
initialization.