SIMPOL Documentation

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.