Dana makes a clear separation between data and behavior, with two distinct type hierarchies. As a result of this design, a common pattern in Dana programming is to declare a named required interface which contains a set of utility functions for operating over a given data type. A common example of this is data.StringUtil stringUtil which allows us to operate on char[]-based strings:
component provides App requires data.StringUtil stringUtil {
int App:main(AppParam params[])
{
char myString[] = "hello"
char myOtherString[] = stringUtil.uppercase(myString)
return 0
}
}
Due to how ubiquitous this design pattern is, Dana offers a general concept of associative functions. This effectively allows you to call a function "on" a data type (including primitive types) and makes a lot of code much more concise. If a function call is made on a data type, associative functions work by searching each function of all required interfaces with handle names (like stringUtil), and checking to see if any of those functions takes the given type as its first parameter. If so, the function can be called that it syntactically appears as if the given data type is an object.
Using this technique, the above program can therefore be alternatively expressed as:
component provides App requires data.StringUtil stringUtil {
int App:main(AppParam params[])
{
char myString[] = "hello"
char myOtherString[] = myString.uppercase()
return 0
}
}
Let's look in more detail at how this works. The data.StringUtil interface has the following functions, among others:
char[] trim(char string[])
char[] uppercase(char string[])
char[] lowercase(char string[])
When the compiler is presented with what appears to be an object-call made on a value of data or primitive type (or an array of such types), like this:
char myOtherString[] = myString.uppercase()The compiler knows that this must be an associative function call, since these types are not objects. The compiler scans over every named required interface in the component header, starting with stringUtil. It then checks for any function called uppercase() which has a first parameter of type char[]. If the compiler finds such a function, it converts the function call to instead be:
char myOtherString[] = stringUtil.uppercase(myString)Associative function calls can be chained, for example:
char myOtherString[] = myString.trim().uppercase()Let's see another example, using the data.query.Sort API.
component provides App requires data.StringUtil stringUtil, data.query.Sort sort {
int App:main(AppParam params[])
{
char myString[] = "hello"
char myOtherString[] = myString.uppercase()
String strings[] = getSomeStrings() // some other function
strings = strings.sort(String.[string], true)
return 0
}
}
Here the variable strings is an array of data instances, which again is not an object type. When encountering the .sort() function call, the compiler again knows that this must be an associative call. It begins by examining StringUtil for a function called sort(), which it doesn't find. It then examines the Sort interface and does find a function called sort(), which is defined as:
Data[] sort(Data list[], TypeField field, optional bool ascending)The first parameter of this function is a basetype Data array, which is compatible with String[]. The compiler therefore knows that this function can be used associatively, and is converted to the form:
strings = sort.sort(strings, String.[string], true)In this case the type of the first parameter wasn't an exact match, but it was compatible. The Dana compiler will score the type distance on every candidate associative function, and selects that which has the shortest distance between the formal type and the actual type. As seen in the above example, parameters that come after the first one are simply appended to the parameter list of the converted call.
We take this concept a step further to support the easy expansion of string expressions. In Dana you can place function calls or variables directly in strings prefixed with $ as follows:
component provides App requires data.IntUtil iu {
int App:main(AppParam params[])
{
int k = params[0].string.intFromString()
char str[] = "some string and the value of $k here"
return 0
}
}
When expanding string expressions, the Dana compiler assumes that the associative function makeString should be called on whatever type is in the string expression -- effectively converting the above expression to k.makeString(). Because the data.IntUtil interface has a function defined as char[] makeString(int v) this will expand the string expression, using IntUtil, with a function to convert the integer to a string - expanding the string expression to iu.makeString(k).
If an associative function has more than one parameter, these parameters are simply supplied by the programmer as part of the function call (ignoring the first parameter). While the full-form of function calling is often still used in Dana programs (where the required interface handle is used as the relative entity on which a call is make), this general concept can make code much shorter and simpler to read, as long as the programmer is aware of which functions are being associated from which interfaces.