Stream: cql
Topic: Proposed New Feature - Named Types
Frank Adrian (Sep 14 2021 at 07:30):
Occasionally, when writing algorithms in CQL, one must use types such as (for example):
Tuple { dt Date, val Decimal }
List<Tuple { component DomainResource, id String, clinicalDate Date}>.
When passing these types as parameters to functions or using them in return clauses, one must currently replicate these type definitions, leading to lengthier and less understandable code. To alleviate this difficulty in using these types, we propose to add a new linguistic construct - the named type. A named type creates an alias for a type definition and can be used anywhere a type specifier is needed. For example, we examine the following code which is written in the current version of CQL:
using FHIR version "4.0.0"
...
define function makeHbAicValueDescriptor( dt Date, val Decimal )
returns Tuple { effectiveDate Date, representativeValue Decimal }:
Tuple{ effectiveDate: dt, representativeValue: val }
define "HbA1c Procedure Values"
returns List<Tuple { effectiveDate Date, representativeValue Decimal }">:
[Procedure: HbA1c] P
return makeHbAicValueDescriptor(date from P.performed."end", getRepresentativeValue(P))
define getRepresentativeValue(p Procedure) returns Decimal:
...
define "HbA1c Observation Values"
returns List<Tuple { effectiveDate Date, representativeValue Decimal }>:
[Observation: HbA1c] O
return makeHbAicValueDescriptor(date from O.effective, O.value.value)
define "All HbA1c Values"
returns List<Tuple { effectiveDate Date, representativeValue Decimal }>:
("HbA1c Procedure Values" union "HbA1c Observation Values") "HbA1c Values"
sort by effectiveDate
This code, in addition to having many repetitions of the Tuple definition, is also less clear because it is hard to tell if the various Tuple types are equivalent. In a version of CQL allowing named types, the code would become:
using FHIR version "4.0.0"
type "HbA1c Value Descriptor": Tuple { effectiveDate Date, representativeValue Decimal }
...
define function makeHbAicValueDescriptor( dt Date, val Decimal )
returns "HbAic Value Descriptor":
Tuple{ effectiveDate: dt, representativeValue: val }
define "HbA1c Procedure Values" returns List<"HbA1c Value Descriptor">:
[Procedure: HbA1c] P
return makeHbAicValueDescriptor(date from P.performed."end", getRepresentativeValue(P))
define getRepresentativeValue(p Procedure) returns Decimal:
...
define "HbA1c Observation Values" returns List<"HbAic Value Descriptor">:
[Observation: HbA1c] O
return makeHbAicValueDescriptor(date from O.effective, O.value.value)
define "All HbA1c Values" returns List<"HbAic Value Descriptor">:
("HbA1c Procedure Values" union "HbA1c Observation Values") "HbA1c Values"
sort by effectiveDate
This code is not only more succinct, but also makes it clear that the various type specifiers used in the code are equivalent.
Named types can be added to the CQL grammar in the library production following using clauses (after which all primitive types are defined) and before include clauses:
library
:
libraryDefinition?
usingDefinition*
namedTypeDefinition*
includeDefinition*
codesystemDefinition*
valuesetDefinition*
codeDefinition*
conceptDefinition*
parameterDefinition*
statement*
EOF
;
namedTypeDefinition
: accessModifier? 'type' typeIdentifier ':' typeSpecifier
;
typeIdentifier
: identifier
;
A simple implementation of the named type concept collects the definitions of the named types and then transitively expands the named type specifiers until they are defined in terms of the CQL primitive types and types taken from the models specified in the using clauses. After this the named types in parameters and returns clauses are replaced by their expansions to be processed into ELM. Self reference of named types would be forbidden. What do folks think about this?
Bryn Rhodes (Sep 14 2021 at 22:10):
I love this, thank you for putting it together! A couple points for discussion:
- I would think that type equivalence would still be based on the definition of the type for tuple types, I think that's an important aspect of the flexibility of using Tuples
- Should this construct be allowed to rename existing types? i.e. is
type A : FHIR.Patient
allowed?
Frank Adrian (Sep 14 2021 at 23:58):
- Agreed. Tuples are a great feature and ultimately basing type equivalence on them is probably a good thing.
- Is doing this good practice? Probably not - I don't see a way to use this without making the renamed types less clear. As such, I'm not sure it's necessary, but I've always thought that it's easier to add a feature completely rather than cluttering the code with special case checks. So unless there's a good reason for not allowing renaming existing types, I'd probably allow it. It shouldn't interfere with type equivalence checking, since one's expanding the types to primitives for checking anyway. But like most edge cases in languages, it should be used sparingly, if at all.
Michael Riley (Sep 16 2021 at 14:11):
I'm writing CQL with a lot of tuples with it right now, and I added a "type" metadata field for exactly this reason, so I could know exactly what context the tuple is trying to represent.
Michael Riley (Sep 16 2021 at 15:40):
In the authors guide on tuples (section 4.3), there's a section that references "Named tuple types" but I've never seen how to define a named tuple type in a script before. Maybe someone can jump in here on how to do it? image.png
Frank Adrian (Sep 21 2021 at 18:50):
Although this is conjecture on my part, I would assume that these are restricted to tuple types defined in the models that are specified in "using" statements. In the examples in the CQL specification, these are the only ones I've ever seen used in this manner. And, to be honest, I'm not certain whether or not this feature is even present in the reference implementation.
This, of course, leads to the next question - what is the form of the models specified in the using statements? Are they simply .xsd files? If so, one could (assuming the feature you have described works) simply define a .xsd file containing the named types the user would want to define and use this model in the CQL code.
JP (Sep 21 2021 at 18:59):
Hi Frank. You are correct that the types allowed in that tuple construct are the CQL system-types and the types in whatever data model you're using
. Internally, the CQL translator represents the type information for a given model as a ModelInfo
. Although ModelInfo
is considered an implementation detail of the cql-translator, the schema for that is documented here: https://cql.hl7.org/elm.html
Yes, the cql-translator supports plugging in new data models. That's how the "profile-informed authoring" that was demoed at the last connectathon works. The set of built-in ModelInfos
is here:
There's tooling available for generating ModelInfos
from both .xsd files and from FHIR structure definitions:
Michael Riley (Sep 21 2021 at 20:30):
Can you go into a little more detail here? How could I define a custom modelinfo file, but if I didn't want to change my dataprovider what could I do here? It's nice that we can have some .xsd translation but what if I have some simple data needs that could be easily mapped from something like a FHIR model?
JP (Sep 22 2021 at 15:39):
Are you referring to the cql-engine dataproviders? The cql-translator supports emitting the ELM for models derived from FHIR, such as QICore, in terms of plain ole FHIR. The cql-engine actually runs the FHIR-based ELM, so it's not aware of QICore at all and no changes to the data providers are required. If you're attempting to define a FHIR-based model, you might be able to do something similar. Longer term, the thinking is that there will be some mechanism for the cql-translator to be IG-aware such that you could define whatever profile you wanted in the IG and author CQL against it. If you're using a non-FHIR-derived datamodel you'll need to create new dataprovider to support it in the cql-engine.
Bryn Rhodes (Sep 24 2021 at 21:52):
FYI, I added a tracker to add this capability to the language: https://jira.hl7.org/browse/FHIR-34014
Last updated: Apr 12 2022 at 19:14 UTC