Поділитися через


Types

A type value is a value that classifies other values. A value that is classified by a type is said to conform to that type. The M type system consists of the following kinds of types:

  • Primitive types, which classify primitive values (binary, date, datetime, datetimezone, duration, list, logical, null, number, record, text, time, type) and also include a number of abstract types (function, table, any, anynonnull and none)

  • Record types, which classify record values based on field names and value types

  • List types, which classify lists using a single item base type

  • Function types, which classify function values based on the types of their parameters and return values

  • Table types, which classify table values based on column names, column types, and keys

  • Nullable types, which classifies the value null in addition to all the values classified by a base type

  • Type types, which classify values that are types

The set of primitive types includes the types of primitive values, and a number of abstract types, which are types that do not uniquely classify any values: function, table, any, anynonnull and none. All function values conform to the abstract type function, all table values to the abstract type table, all values to the abstract type any, all non-null values to the abstract type anynonnull, and no values to the abstract type none. An expression of type none must raise an error or fail to terminate since no value could be produced that conforms to type none. Note that the primitive types function and table are abstract because no function or table is directly of those types, respectively. The primitive types record and list are non-abstract because they represent an open record with no defined fields and a list of type any, respectively.

All types that are not members of the closed set of primitive types plus their nullable counterparts are collectively referred to as custom types. Custom types can be written using a type-expression:

type-expression:
      primary-expression

      type primary-type
type:
      primary-expression
      primary-type
primary-type:
      primitive-type
      record-type
      list-type
      function-type
      table-type
      nullable-type
primitive-type:
one of
      any anynonnull binary date datetime datetimezone duration function list logical
      none null number record table text time type

The primitive-type names are contextual keywords recognized only in a type context. The use of parentheses in a type context moves the grammar back to a regular expression context, requiring the use of the type keyword to move back into a type context. For example, to invoke a function in a type context, parentheses can be used:

type nullable ( Type.ForList({type number}) )   
// type nullable {number}

Parentheses can also be used to access a variable whose name collides with a primitive-type name:

let  record = type [ A = any ]  in  type {(record)} 
// type {[ A = any ]}

The following example defines a type that classifies a list of numbers:

type { number }

Similarly, the following example defines a custom type that classifies records with mandatory fields named X and Y whose values are numbers:

type [ X = number, Y = number ]

The ascribed type of a value is obtained using the standard library function Value.Type, as shown in the following examples:

Value.Type( 2 )                 // type number 
Value.Type( {2} )               // type list 
Value.Type( [ X = 1, Y = 2 ] )  // type record

The is operator is used to determine whether a value's type is compatible with a given type, as shown in the following examples:

1 is number          // true 
1 is text            // false 
{2} is list          // true

The as operator checks if the value is compatible with the given type, and raises an error if it is not. Otherwise, it returns the original value.

Value.Type( 1 as number )   // type number 
{2} as text                 // error, type mismatch

Note that the is and as operators only accept nullable primitive types as their right operand. M does not provide means to check values for conformance to custom types.

A type X is compatible with a type Y if and only if all values that conform to X also conform to Y. All types are compatible with type any and no types (but none itself) are compatible with type none. The following graph shows the compatibility relation. (Type compatibility is reflexive and transitive. It forms a lattice with type any as the top and type none as the bottom value.) The names of abstract types are set in italics.

Type compatibility

The following operators are defined for type values:

Operator Result
x = y Equal
x <> y Not equal
x ?? y Coalesce

The native type of type values is the intrinsic type type.

Primitive Types

Types in the M language form a disjoint hierarchy rooted at type any, which is the type that classifies all values. Any M value conforms to exactly one primitive subtype of any. The closed set of primitive types deriving from type any are as follows:

  • type null, which classifies the null value.
  • type logical, which classifies the values true and false.
  • type number, which classifies number values.
  • type time, which classifies time values.
  • type date, which classifies date values.
  • type datetime, which classifies datetime values.
  • type datetimezone, which classifies datetimezone values.
  • type duration, which classifies duration values.
  • type text, which classifies text values.
  • type binary, which classifies binary values.
  • type type, which classifies type values.
  • type list, which classifies list values.
  • type record, which classifies record values.
  • type table, which classifies table values.
  • type function, which classifies function values.
  • type anynonnull, which classifies all values excluding null.
  • type none, which classifies no values.

Any Type

The type any is abstract, classifies all values in M, and all types in M are compatible with any. Variables of type any can be bound to all possible values. Since any is abstract, it cannot be ascribed to values—that is, no value is directly of type any.

List Types

Any value that is a list conforms to the intrinsic type list, which does not place any restrictions on the items within a list value.

list-type:
      { item-type }
item-type:
      type

The result of evaluating a list-type is a list type value whose base type is list.

The following examples illustrate the syntax for declaring homogeneous list types:

type { number }        // list of numbers type 
     { record }        // list of records type
     {{ text }}        // list of lists of text values

A value conforms to a list type if the value is a list and each item in that list value conforms to the list type's item type.

The item type of a list type indicates a bound: all items of a conforming list conform to the item type.

Record Types

Any value that is a record conforms to the intrinsic type record, which does not place any restrictions on the field names or values within a record value. A record-type value is used to restrict the set of valid names as well as the types of values that are permitted to be associated with those names.

record-type:
      [ open-record-marker ]
      [ field-specification-listopt ]
      [ field-specification-list , open-record-marker ]
field-specification-list:
      field-specification
      field-specification , field-specification-list
field-specification:

      optionalopt field-name field-type-specificationopt
field-type-specification:

      = field-type
field-type:
      type
open-record-marker:

      ...

The result of evaluating a record-type is a type value whose base type is record.

The following examples illustrate the syntax for declaring record types:

type [ X = number, Y = number] 
type [ Name = text, Age = number ]
type [ Title = text, optional Description = text ] 
type [ Name = text, ... ]

Record types are closed by default, meaning that additional fields not present in the fieldspecification-list are not allowed to be present in conforming values. Including the openrecord-marker in the record type declares the type to be open, which permits fields not present in the field specification list. The following two expressions are equivalent:

type record   // primitive type classifying all records 
type [ ... ]  // custom type classifying all records

A value conforms to a record type if the value is a record and each field specification in the record type is satisfied. A field specification is satisfied if any of the following are true:

  • A field name matching the specification's identifier exists in the record and the associated value conforms to the specification's type

  • The specification is marked as optional and no corresponding field name is found in the record

A conforming value may contain field names not listed in the field specification list if and only if the record type is open.

Function Types

Any function value conforms to the primitive type function, which does not place any restrictions on the types of the function's formal parameters or the function's return value. A custom function-type value is used to place type restrictions on the signatures of conformant function values.

function-type:
      function ( parameter-specification-listopt ) function-return-type
parameter-specification-list:
      required-parameter-specification-list
      required-parameter-specification-list
, optional-parameter-specification-list
      optional-parameter-specification-list
required-parameter-specification-list:
      required-parameter-specification
      required-parameter-specification
, required-parameter-specification-list
required-parameter-specification:
      parameter-specification
optional-parameter-specification-list:
      optional-parameter-specification
      optional-parameter-specification
, optional-parameter-specification-list
optional-parameter-specification:

      optional parameter-specification
parameter-specification:
      parameter-name parameter-type
function-return-type:
      assertion
assertion:

      as nullable-primitive-type

The result of evaluating a function-type is a type value whose base type is function.

The following examples illustrate the syntax for declaring function types:

type function (x as text) as number 
type function (y as number, optional z as text) as any

A function value conforms to a function type if the return type of the function value is compatible with the function type's return type and each parameter specification of the function type is compatible to the positionally corresponding formal parameter of the function. A parameter specification is compatible with a formal parameter if the specified parameter-type type is compatible with the type of the formal parameter and the parameter specification is optional if the formal parameter is optional.

Formal parameter names are ignored for the purposes of determining function type conformance.

Specifying a parameter as optional implicitly makes its type nullable. The following create identical function types:

type function (optional x as text) as any
type function (optional x as nullable text) as any

Table types

A table-type value is used to define the structure of a table value.

table-type:
      table row-type
row-type:

      [ field-specification-listopt ]

The result of evaluating a table-type is a type value whose base type is table.

The row type of a table specifies the column names and column types of the table as a closed record type. So that all table values conform to the type table, its row type is type record (the empty open record type). Thus, type table is abstract since no table value can have type table's row type (but all table values have a row type that is compatible with type table's row type). The following example shows the construction of a table type:

type table [A = text, B = number, C = binary] 
// a table type with three columns named A, B, and C 
// of column types text, number, and binary, respectively

A table-type value also carries the definition of a table value's keys. A key is a set of column names. At most one key can be designated as the table's primary key. (Within M, table keys have no semantic meaning. However, it is common for external data sources, such as databases or OData feeds, to define keys over tables. Power Query uses key information to improve performance of advanced functionality, such as cross-source join operations.)

The standard library functions Type.TableKeys, Type.AddTableKey, and Type.ReplaceTableKeys can be used to obtain the keys of a table type, add a key to a table type, and replace all keys of a table type, respectively.

Type.AddTableKey(tableType, {"A", "B"}, false) 
// add a non-primary key that combines values from columns A and B 
Type.ReplaceTableKeys(tableType, {}) 
// returns type value with all keys removed

Nullable types

For any type T, a nullable variant can be derived by using nullable-type:

nullable-type:
      nullable type

The result is an abstract type that allows values of type T or the value null.

42 is nullable number             // true null is
nullable number                   // true

Ascription of type nullable T reduces to ascription of type null or type T. (Recall that nullable types are abstract and no value can be directly of abstract type.)

Value.Type(42 as nullable number)       // type number
Value.Type(null as nullable number)     // type null

The standard library functions Type.IsNullable and Type.NonNullable can be used to test a type for nullability and to remove nullability from a type.

The following hold (for any type T):

  • type T is compatible with type nullable T
  • Type.NonNullable(type T) is compatible with type T

The following are pairwise equivalent (for any type T):

    type nullable any
    any

    Type.NonNullable(type any)
    type anynonnull

    type nullable none
    type null

    Type.NonNullable(type null)
    type none

    type nullable nullable T
    type nullable T

    Type.NonNullable(Type.NonNullable(type T))
    Type.NonNullable(type T)

    Type.NonNullable(type nullable T)
    Type.NonNullable(type T)

    type nullable (Type.NonNullable(type T))
    type nullable T

Ascribed type of a value

A value's ascribed type is the type to which a value is declared to conform.

A value may be ascribed a type using the library function Value.ReplaceType. This function either returns a new value with the type ascribed or raises an error if the new type is incompatible with the value.

When a value is ascribed a type, only a limited conformance check occurs:

  • The type being ascribed must be non-abstract, non-nullable, and compatible with the value's intrinsic (native) primitive-type.
  • When a custom type that defines structure is ascribed, it must match the structure of the value.
    • For records: The type must be closed, must define the same number of fields as the value, and must not contain any optional fields. (The type's field names and field types will replace those currently associated with the record. However, existing field values will not be checked against the new field types.)
    • For tables: The type must define the same number of columns as the value. (The type's column names and column types will replace those currently associated with the table. However, existing column values will not be checked against the new column types.)
    • For functions: The type must define the same number of required parameters, as well as the same number of optional parameters, as the value. (The type's parameter and return assertions, as well as its parameter names, will replace those associated with the function value's current type. However, the new assertions will have no effect on the actual behavior of the function.)
    • For lists: The value must be a list. (However, existing list items will not be checked against the new item type.)

Library functions may choose to compute and ascribe complex types to results based on the ascribed types of the input values.

The ascribed type of a value may be obtained using the library function Value.Type. For example:

Value.Type( Value.ReplaceType( {1}, type {number} ) 
// type {number}

Type equivalence and compatibility

Type equivalence is not defined in M. An M implementation may optionally choose to use its own rules to perform equality comparisons between type values. Comparing two type values for equality should evaluate to true if they are considered identical by the implementation, and false otherwise. In either case, the response returned must be consistent if the same two values are repeatedly compared. Note that within a given implementation, comparing some identical type values (such as (type text) = (type text)) may return true, while comparing others (such as (type [a = text]) = (type [a = text])) may not.

Compatibility between a given type and a nullable primitive type can be determined using the library function Type.Is, which accepts an arbitrary type value as its first and a nullable primitive type value as its second argument:

Type.Is(type text, type nullable text)  // true 
Type.Is(type nullable text, type text)  // false 
Type.Is(type number, type text)         // false 
Type.Is(type [a=any], type record)      // true 
Type.Is(type [a=any], type list)        // false

There is no support in M for determining compatibility of a given type with a custom type.

The standard library does include a collection of functions to extract the defining characteristics from a custom type, so specific compatibility tests can be implemented as M expressions. Below are some examples; consult the M library specification for full details.

Type.ListItem( type {number} ) 
  // type number 
Type.NonNullable( type nullable text ) 
  // type text 
Type.RecordFields( type [A=text, B=time] ) 
  // [ A = [Type = type text, Optional = false], 
  //   B = [Type = type time, Optional = false] ] 
Type.TableRow( type table [X=number, Y=date] ) 
  // type [X = number, Y = date] 
Type.FunctionParameters(
        type function (x as number, optional y as text) as number) 
  // [ x = type number, y = type nullable text ] 
Type.FunctionRequiredParameters(
        type function (x as number, optional y as text) as number) 
  // 1 
Type.FunctionReturn(
        type function (x as number, optional y as text) as number) 
  // type number