Working with Data Types

You can work with data types in the following ways:

Checking Whether an Expression Has a Specified Data Type

Summary:
Use the isa operator to check whether an expression has a specified data type.
To determine whether a specified expression has a specified data type, use the isa operator. The isa operator has the following syntax:
Syntax:expr isa type
where:
expris the expression whose data type you want to compare with type.
typeis the data type that you want to compare with the type of expr.
If you supply a literal value for expr, this operator returns true if the literal value can be stored in a variable of type type without changing its representation; otherwise it returns false. For example:

Example: Using isa with Literals
98 isa int... {value 98 isa int}

98 isa int8... {value 98 isa int8}

98 isa int32... {value 98 isa int32}

98 isa int64... {value 98 isa int64}

98 isa float... {value 98 isa float}

98 isa double... {value 98 isa double}

98 isa char... {value 98 isa char}
If expr is a variable of a primitive type, this operator returns true if the variable's data type is type or an alias for that type; otherwise, it returns false. For example:

Example: Using isa with Variables of Primitive Types
98 (an int) isa int...
{value
    let x:int = 98
    x isa int
}

98 (an int) isa int32...
{value
    let x:int = 98
    x isa int32
}

98 (an int8) isa int...
{value
    let x:int8 = 98
    x isa int
}
If expr is a variable of a class type, this operator returns true if the variable's data type is either type or a subclass of type; otherwise, it returns false. For example:

Example: Using isa with Variables of Class Types
{define-class Animal}
{define-class Cow {inherits Animal}}

{let harriet:Cow = {Cow}}

harriet isa Cow is ... {value harriet isa Cow}

harriet isa Animal is ... {value harriet isa Animal}

harriet isa double is ... {value harriet isa double}
For additional information, view the API Reference information for isa or see Relational Operators.
Note: When you want to check at run time whether an expression has one of several possible data types, instead of repeatedly testing it in isa expressions, it may be more efficient to use a single type-switch construct, as described in the next section.

The type-switch Expression

Summary:
  • Uses the data type of a value to determine which block of code to execute.
The type-switch expression uses the data type of a value to determine which block of code to execute. So, for example, you could execute different blocks of code depending on whether the runtime value of an any variable was an int, a double, a Distance, and so on.
The type-switch expression has the following syntax:
Syntax:{type-switch value
 [case identifier-1:type-1 do     code-block-1]
 [case identifier-2:type-2 do     code-block-2]
 ...
 [else code-block-n]
}
where:
valueis the value whose data type determines which block of code to execute.
type-1, type-2, ...are the data types with which you want to compare value for each case clause.
identifier-1, identifier-2, ...are variable names for each of the possible values of value. These variable names may be useful in the code block that accompanies the case clause.
code-block-1, code-block-2, ...are the code blocks that you want to execute for each case clause.
When the Curl® runtime encounters a type-switch expression, it compares the data type of value with each of type-1, type-2, and so on. It uses the isa operator to perform the comparison. When the runtime encounters the first comparison that returns true, it executes the corresponding code (the code that follows the accompanying do expression) and then moves to the end of the type-switch expression. If none of the comparisons return true and there is an else clause, the runtime executes the code that follows the else clause. If there is no else clause, the runtime moves to the expression immediately following the type-switch expression.
If the data type of one of the case clauses matches the data type of value, a type-switch expression returns the value of the last expression in the code block that is executed. Otherwise, a type-switch expression does not return a value.
For example, the following type-switch expression uses the data type of the return value of a call to the some-function procedure to determine which code to execute. Each of the case clauses presents a possible data type for the return value. The runtime executes the code for the first case clause that matches the type of the return value of the procedure call. Note that the identifier in each case clause contains the value being returned. This might be useful in the code that accompanies the case clause. Also note that, in this case, the else clause throws an error.
{type-switch {some-function}
    case i:int do
        {output i, " is an int"}
    case f:float do
        {output f, " is a float"}
    case d:Distance do
        {output d, " is a Distance"}
    case str:String do
        {output str, " is a String"}
    else
        {error "unexpected return value"}
}

Getting the Data Type of an Expression

Summary:
  • Use the type-of expression to determine the runtime data type of an expression.
To determine the data type of a specified expression, use the type-of expression. The type-of expression has the following syntax:
Syntax:{type-of expr}
where:
expris a value, variable, or expression.
The type-of expression returns the runtime data type of an expression. Note that the runtime data type of an expression might be different from the compile-time data type of the expression. For example, if you use the any data type to declare a variable, it has a compile-time data type of any. However, the runtime data type of the variable depends on the value that is assigned to the variable. Similarly, if you declare a variable using a class and then assign an instance of a subclass to the variable, the runtime data type will be the data type of the subclass.
The following example shows some sample type-of expressions for primitive data types:

Example: Using type-of with Variables of Primitive Types
{let i:int = 3}
The data type of i is... {type-of i}

{let c:char = 'a'}
The data type of c is... {type-of c}
The following example shows some sample type-of expressions for the any data type. Remember that the type-of expression returns the runtime data type of the variables.

Example: Using type-of with Variables of type any
{let a:any = 3}
The data type of a is... {type-of a}

{let b:any = 'a'}
The data type of b is... {type-of b}
The following example uses type-of with variables of class types. Remember that the type-of expression returns the runtime data type of the expression.

Example: Using type-of with Variables of Class Types
|| Declare a variable "c" to be of type "VBox".
 || Initialize "c" with a "VBox" object.
 {let c:VBox = {VBox}}
 The data type of c is... {type-of c}

 || Declare a variable "d" to be of type "Box".
 || Initialize "d" with a "VBox" object.
 || Note that "Box" is a superclass of "VBox".
 {let d:Box = {VBox}}
 The data type of d is... {type-of d}
To get the compile-time version of a variable, as opposed to its runtime type, use the compile-time-type-of operator. This is an advanced feature.
You can also use type-of to query the data type of arbitrary expressions. Here are some simple examples:

Example: Using type-of in Expressions
The data type of 3 is ... {type-of 3}

The data type of 3 * 7.0 is ... {type-of (3 * 7.0)}

The data type of 'a' is ... {type-of 'a'}

The data type of "xyz" is ... {type-of "xyz"}

The data type of {StringBuf "xyz"} is ... {type-of {StringBuf "xyz"}}
Note: For the special default object value null, {type-of null} returns Null.

Checking for Subtype Relationships

Summary:
  • Subtype values may be stored in supertype variables.
  • Use the Type.subtype-of? method to determine if a type is a subtype of another.
A data type is a subtype of a second data type if its values can be stored in variables declared with the second type without a change in representation. A data type is a supertype of a second data type if the second data type is a subtype of the first. Specifically:
To determine if a data type is a subtype of a second data type, use the subtype-of? method of the Type class.

Example: Testing for Subtypes Using the subtype-of? Method
String subtype of String: {(String asa Type).subtype-of? String}

String subtype of #String: {(String asa Type).subtype-of? #String}

#String subtype of String: {(#String asa Type).subtype-of? String}

String subtype of Object:  {(String asa Type).subtype-of? Object}

String subtype of any:  {(String asa Type).subtype-of? any}

int subtype of int64: {int.subtype-of? int64}

Converting Data Types

Summary:
  • In some cases, the runtime supports the implicit conversion of values.
  • You can use the asa operator to explicitly convert values.
  • You can convert any value to a class that supports a matching implicit constructor or factory.
  • You can usually convert a value with a primitive type only to another primitive type.
  • You can convert a value with a class type only to another class type.
In some cases, you can convert a value to another data type. If you assign a value with one data type to a variable with a different data type, the runtime attempts to place the value in the variable. For example, if you assign a character to an integer variable, the runtime assigns the numeric value of the character to the variable. Such conversions are called implicit conversions because you do not specify an expression to convert the value. The runtime performs these conversions automatically. Another instance of an implicit conversion occurs when a string is converted to an enumerated type element with the same name in an assignment. (See the Variables of Enumerated Types section.)
Implicit conversion typically occurs in assignment statements and when passing values to functions. If you assign a value with one data type to a variable with a different data type, the runtime attempts to perform an implicit conversion. Similarly, if you pass a value with one data type to an argument expecting a different data type, the runtime attempts to perform an implicit conversion. However, the runtime will not perform an implicit conversion if the conversion might result in a loss of data. For example, if you convert an int to an int8, you might lose data because the value of the int might be outside of the valid range of values for an int8. If you attempt to implicitly convert a value to a data type that might result in a loss of data, the Curl® compiler throws a syntax error.
You can use the asa operator to perform an explicit conversion of a value from one data type to another. The asa operator has the following syntax:
Syntax:expr asa type
where:
  • expr is an expression whose type you want to convert.
  • type is the data type to which you want to convert expr.
For example:

Example: Using asa
{value
    || Declare a variable "d" with data type double and
    || initialize the variable with the value 37.7 as
    || a double-precision floating-point number.
    let d:double = 37.7

    || Display the value of the variable "d" converted
    || to an int.
    d asa int
}
Conversion errors detected at compile time will result in a SyntaxError, while those detected later at run time will result in a CastException. When deciding whether to perform an implicit or explicit conversion, you should consider that it is desirable to perform type checking at compile time rather than at run time. If you convert values explicitly, it is more likely that the Curl® compiler will catch an illegal conversion.
The rules for converting primitive data types and class types are different. In general, you cannot convert a value with a primitive data type to a value with a class type, and vice versa. However, any value can be converted to a class that supports an implicit constructor or factory that can be invoked with the value. For details on this feature see the documentation for the implicit keyword.

Converting Primitive Data Types

Summary:
  • A widening conversion involves converting to a data type that can hold more information.
  • A narrowing conversion involves converting to a data type that can hold less information.
  • The runtime supports other conversions, such as converting to and from a char.
  • You can convert between all numeric types, although quantity types must be compatible; for example, casting int to double is allowed, but casting Distance to Time is not.
A widening conversion involves converting a value to a data type that holds more information. That is, the conversion does not result in loss of data. An example of a widening conversion is the conversion of an int8 to an int16. You can perform widening conversions either implicitly or explicitly.
Figure: Widening Conversion
A narrowing conversion involves the conversion of data from a type to a data type that can hold less information. That is, the conversion might result in loss of data. An example of a narrowing conversion is the conversion of an int16 to an int8. It is possible to lose data if the value of the int16 is outside the range of possible values for an int8. You must explicitly convert the value to perform a narrowing conversion.
Figure: Narrowing Conversion
You can perform both widening conversions and narrowing conversions on primitive data types. However, there are exceptions; you cannot convert to a bool data type and you cannot convert to or from a quantity. You may explicitly convert a bool to any integer type with an explicit cast (this ability was added in 6.0), which converts true and false to 1 and 0 respectively of the specified integer type. There are other types of conversions that do not fit into the widening and narrowing categories, such as:
When converting from a numeric data type to a char, the data is first automatically converted to an integer. The integer value must be a legal value for char, or an error will result. The legal values for char are the following ranges:
You must take care when converting between integer and floating-point data types because such conversions can result in the loss of data. During conversions from floating-point numbers to integers, the decimal portion of the number is truncated. For example, if the value 37.7 of type double is converted to the type int, its value changes to 37.

Example: Converting Between Integer and Floating-Point Data Types
{value
    || Declare a variable "i" with the data type int.
    let i:int
    || Assign the value 37.7 to the variable "i".
    || The value 37.7 is explicitly converted to an
    || integer value.
    set i = 37.7 asa int
    || Display the value of "i".
    i
}
If you assign a large integer value to a variable with the floating-point data type, you might also experience a loss of information because of the way that your computer stores floating-point numbers. For example:

Example: Loss of Information During Conversions
{value
    || Declare a variable "f" with the data type float.
    let f:float
    || Assign the value 2147483525 to the variable "a".
    || The value 2147483525 is implicitly converted to
    || a single-precision floating-point value.
    set f = 2147483525
    || Display the value of "f" as an int.
    f asa int
}
The only conversions that you cannot perform among primitive data types are conversions to and from bool or quantity data types. That is, you cannot convert a bool value to another primitive data type and vice versa. And similarly, you cannot convert a quantity such as 4m to another primitive data type, such as an int, or to an incompatible quantity type, such as Time, and vice versa.
You must also take care when converting between signed integer and unsigned integer data types because such conversions might also result in the loss of data. During a conversion from a signed integer to an unsigned integer, a negative number might not convert as you expected. This is because of the way in which unsigned integers are internally represented.
Figure: Internal representation of signed (int8) and unsigned (uint8) data
Figure: Internal mapping between signed and unsigned data
The following interactive example shows some conversions from an int8 to a uint8:

Example: Performing a Narrowing Conversion
{value -128 asa uint8}
{value -64 asa uint8}
{value 0 asa uint8}
{value 64 asa uint8}
{value 127 asa uint8}
Of course, these principles can also be applied in reverse to conversions from a uint8 to an int8. And similar principles apply to all conversions between signed and unsigned data types.
For tables describing all of the conversions between primitive data types, see Primitive Data Type Conversions.

Converting Class Types

Summary:
  • Upcasting converts an object to the type of a superclass.
  • You can implicitly or explicitly upcast objects.
  • After upcasting, you can access only the members of the superclass.
  • Information is not lost when converting objects.
  • Downcasting converts an object to the type of a subclass.
  • You can only explicitly downcast objects.
You can convert an object to the class type of a superclass or a subclass. However, you cannot convert an object to a class type that is not in the class inheritance tree for that object.
Converting an object to the class type of a superclass is called upcasting. That is, upcasting travels up the class inheritance tree toward a superclass (the term up is used because the root of a class inheritance tree is typically at the top, with the leaves below.)
Figure: Upcasting
You can perform an upcast conversion either implicitly or explicitly. An example of upcasting is the conversion of a CommandButton to a Graphic, where CommandButton is a subclass of Graphic.
let foo:Graphic = {CommandButton}
A class inherits all of the class members of its superclass. It also, typically, has its own class members. When you upcast an object to the type of a superclass, you can access only the members of the superclass. That is, you cannot access the members that are defined only in the original class.
Figure: An Upcast Class Can Access Only the Superclass's Members
However, it is important to realize that the information in the members that are defined only in the original class is not lost. Even though this information is inaccessible while the object has the class type of the superclass, it is still in memory. To access the information, convert the object back to the type of the original class. For example:
foo asa CommandButton
Converting an object to the type of a subclass is called downcasting. That is, a downcasting travels down the class inheritance tree towards subclasses. Typically, you will want to downcast because upcasting has previously occurred. The Curl language does not implicitly downcast an object; you must use the asa operator to explicitly convert the object.
Figure: Downcasting
To reinforce these concepts, consider the following example where TextField is a subclass of BaseFrame. In the code, the assignment statement implicitly converts the TextField to a BaseFrame:

Example: Implicit Upcasting from TextField to BaseFrame
|| Declare a variable "b" of type BaseFrame.
{let b:BaseFrame = {BaseFrame}}

|| Declare a variable "t" of type TextField, a subclass of BaseFrame.
{let t:TextField = {TextField value = "hello"}}

|| Assign a TextField value to a BaseFrame variable.
|| The value of t is upcast to the BaseFrame type.
{set b = t}

|| Display the object that "b" now refers to
{value b}

|| Check the runtime and compile-time types of "b"
b's runtime type is ... {type-of b},
but its compile-time type is ... {compile-time-type-of b}
The TextField class includes a value accessor. However, the BaseFrame class does not have a value property. Therefore, if you try to get the value property for the object f in the example above, the compiler will throw a syntax error:
|| Display the value of the object.
b.value


--> Syntax Error!
To use the value getter, you must explicitly convert the object to a TextField. For example:

Example: Explicit Downcasting from BaseFrame to TextField
{value
    || BaseFrame is a superclass of TextField
    let b:BaseFrame = {BaseFrame}
    let t:TextField = {TextField value = "hello"}

    || A TextField value is implicitly upcast to the BaseFrame type.
    set b = t

    || Explicit downcasting back to a TextField makes this operation possible.
    (b asa TextField).value
}