Power Query M 言語のデータ型

Power Query M 言語のデータ型

― 構造、分類、特性に基づく型の理解 ―

Copyright © 2025 LWP 山中 一弘 本資料は、出典を明記いただければ、商用・非商用を問わず、ご自由に複製・改変・再配布していただけます。なお、著作権表示は改変せず、そのまま記載してご利用くださいますようお願いいたします。

第1章 M言語におけるデータ型の全体像

1.1 データ型とは何か:値と構造を定義するもの

M言語における「データ型」とは、値の形式・構造・性質を定義するものであり、式を評価した結果として得られる値に付随する情報である。型は、演算の妥当性や関数の適用条件、UI表示の判断基準にもなっており、クエリ全体の動作を統制する重要な要素である。

すべてのM言語の値は必ず1つの型を持つ。明示的に型を指定しない場合でも、実行時に型が推論され、Value.Type 関数を使うことで取得できる。型は type number や type text のように記述でき、M言語の式上でも静的な型定義として扱える。また、ユーザー定義のスキーマ型(たとえば type record [Name = text, Age = number])のように複合構造の型も記述可能である。

型は、値の表面構造だけでなく、「どのような操作が可能か」「他の値との互換性があるか」といった意味論も含んでいる。たとえば 123 は type number であり、加算や乗算が可能だが、 "123" は type text であり、数値演算には適さない。

1.2 M言語における型の評価と階層構造

M言語では、型は静的な「構文」でもあり、動的な「評価結果」でもある。type というキーワードを用いて型を記述することができ、これは Value.Type で得られる実行時型と一致する構造を持つ。たとえば、type number や type list, type table [A=number] などの型式は、M式内で明示的に使用できる。

型は単純なリテラル(たとえば "abc", 42, true)の型だけでなく、関数・レコード・テーブル・クエリなど、複雑な構造や評価可能な式にも付随する。型の情報は Type.Is, Type.Equals, Value.ReplaceType などの関数によって評価・注釈・比較が可能であり、型安全性を担保するための仕組みが言語仕様に組み込まれている。

また、M言語では型に階層構造が存在するわけではないが、「複数の型が入れ子になった構造」(たとえば、テーブル → レコード → リスト → 値)という構造的ネストが型を通じて表現される。これによって、実務で扱う多階層のデータも正確に型定義できる。

1.3 スカラー型・コンテナ型・関数型の大分類

M言語のデータ型は、大きく以下の3カテゴリに分類される:

  • スカラー型(scalar type):文字列(text)、数値(number)、論理値(logical)、null、日付・時刻系(date, datetime, duration など)。いずれも単一の値として扱われる。これらは最小単位であり、比較や変換、演算の対象となる。

  • コンテナ型(structured/container type):リスト(list)、レコード(record)、テーブル(table)。複数の要素や名前付き要素を内包し、要素ごとに異なる型を持つことができる。コンテナ型は階層構造(ネスト)を作ることができ、複雑なデータモデルの構築に対応している。

  • 関数型(function type):評価可能な式を値として扱うための型であり、(x as number) as number => x + 1 のように、引数・戻り値の型を含んだ形式で表現される。関数は第一級オブジェクトであり、他の関数に引数として渡したり、返り値として返したりできる。関数もまた Value.Type によって型情報を取得でき、型注釈を与えることでより厳密な制御が可能になる。

この3分類は、M言語の型設計の中心軸であり、スカラーを構成要素とするコンテナ型、関数を受け取って実行する高階構造など、組み合わせによってあらゆるデータ構造を表現可能にしている。

さらに、クエリ(名前付き式)自体も「値」として扱われるため、他のクエリ内で参照される際には、そのクエリが評価された結果の型として認識される。これは、クエリがデータ型として振る舞うという特徴的な性質であり、M言語の「すべてが式である」という設計思想の一端を成している。型を持った式としてのクエリ名は、他クエリにとってスキーマ定義や変換対象として活用される。

[

demoScalarTypes = [

        sampleText = "ABC",
        sampleNumber = 123.45,

sampleLogical = true,

sampleNull = null,

        textType = Value.Type("ABC"),        // → type text
        numberType = Value.Type(123.45),     // → type number
        logicalType = Value.Type(true),      // → type logical
        nullType = Value.Type(null)          // → type null

],

demoListType = [

sampleList = {1, 2, 3},

        listType = Value.Type({1, 2, 3})      // → type list

],

demoRecordType = [

        sampleRecord = [Name = "Tokushima", Pop = 833],
        recordType = Value.Type([Name = "Tokushima", Pop = 833])  // → type [Name = text, Pop = number]

],

demoTableType = [

        sampleTable = #table({"Region", "Pop"}, {
            {"Tokushima", 833},
            {"Kagawa", 1023}
        }),
        tableType = Value.Type(#table({"Region", "Pop"}, {
            {"Tokushima", 833},
            {"Kagawa", 1023}
        }))

// → type table [Region = text, Pop = number]

],

demoFunctionType = [

        sampleFunc = (x as number) as number => x + 1,
        funcType = Value.Type((x as number) as number => x + 1)

// → type function

],

demoQueryReferenceType =

let

            demoSubQuery = [ID = 1, Name = "Sample"],
            refType = Value.Type(demoSubQuery)

in

refType

]

第2章 スカラー型(プリミティブ型)の体系

2.1 文字列型(text)

文字列型(text)は、M言語における基本的なスカラー型のひとつであり、Unicode文字列の並びを表す。文字列は二重引用符 " で囲んで記述され、空文字列 "" も有効な type text 値である。文字列値は、Text.Length, Text.Upper, Text.Combine などの関数によって加工・評価される。

文字列リテラルが指定された場合、M言語の評価エンジンは暗黙的に type text を割り当てる。別の型から文字列への変換には Text.From を使用するか、構文上 as text による明示的な変換が用いられる。これにより、他型の値に対する文字列操作を安全に適用できる。

2.2 数値型(number, currency, percentage)

数値型(number)は、M言語において加減乗除などの算術演算や比較を行うための標準型であり、内部的には IEEE 754 準拠の倍精度浮動小数点型として実装される。整数・小数の区別は型的には存在せず、すべて type number に分類される。

currency 型と percentage 型は、Power BI などのUI環境では独立した表示形式を持つが、M言語の評価系ではともに number 型として処理される。従って、スクリプトレベルでは type currency や type percentage という型区別は存在せず、実体としてはすべて Value.Type(x) が type number を返す。

2.3 論理型(logical)

論理型(logical)は、true または false の2値からなるブール型であり、条件分岐やフィルタ処理において中心的な役割を果たす。M言語において論理演算は型安全に処理され、true and false, not true などの演算はすべて型レベルで正確に定義されている。

Value.Type(true) は type logical を返し、type logical は true, false のいずれかしか許容しない。比較式や if ... then ... else における条件判定は、式全体が logical 型に評価される必要があり、それ以外の型(たとえば text, number)は暗黙変換されない。

2.4 null型と型未定義

M言語における null は、値が存在しないことを示す特殊な値であり、その型は type null である。null はスカラー型のひとつとして扱われ、値の代替・空補充・欠損データの表現に用いられる。

比較演算において、null = null は true を返し、null <> null は false となる。これは、M言語が null を通常の値として定義しているためであり、SQLやJavaScriptのように null = null が null になる仕様とは異なる。さらに、null = 値 や 値 = null は false を返す。

この挙動により、if null then ... は false と評価され、null は論理型ではなく、評価文脈において false 相当として扱われる。より明示的かつ安全な比較を行うには、value is null または not value is null を使用することが推奨される。

なお、M言語における error は null とは異なり、構文的に例外を発生させる評価エラーである。try ... otherwise によって補足される error は、型ではなく制御フローの一部であり、null のように値として扱うことはできない。

2.5 日付・時刻型(date, time, datetime, datetimezone, duration)

M言語では、時間情報を扱うために以下の5種のスカラー型が提供されており、それぞれは相互に互換性を持たず、演算・比較には明示的な型整合が求められる。

  • date:日付のみ(年・月・日)を保持する(例:#date(2025, 5, 21))

  • time:時刻のみ(時・分・秒)を保持する(例:#time(14, 30, 0))

  • datetime:日付+時刻の複合型(例:#datetime(2025, 5, 21, 14, 30, 0))

  • datetimezone:タイムゾーン付きの datetime(例:#datetimezone(2025, 5, 21, 14, 30, 0, 9, 0))

  • duration:時差・期間などの相対時間を表す(例:#duration(1, 2, 0, 0) → 1日2時間)

これらはすべて Value.Type() によって明確な型情報を取得でき、異なる時間型同士での演算には Date.From, DateTime.From, Duration.From などの型変換関数が必要である。たとえば、#date(...) + #time(...) のような式は直接的には許容されず、DateTime.From を通じた変換が必要となる。

[

demoTextType = [

        sample = "Hello, world!",
        empty = "",
        fromNumber = Text.From(123.45),
        typeCheck = Value.Type("Hello, world!") // → type text

],

demoNumberType = [

integer = 100,

        float = 3.14,

currencyLike = 1200.00, // 実際の型は number(UIでcurrencyに見える)

percentageLike = 0.85, // 同上

        typeCheck = Value.Type(3.14) // → type number

],

demoLogicalType = [

yes = true,

no = false,

comparison = 5 > 3, // → true

combined = true and false, // → false

        typeCheck = Value.Type(true)       // → type logical

],

demoNullType = [

nullValue = null,

nullEqualsNull = null = null, // → true

nullNotEqualsNull = null <> null, // → false

nullEqualsValue = null = 1, // → false

valueEqualsNull = 1 = null, // → false

        isNull = if null is null then "Yes" else "No", // → "Yes"
        typeCheck = Value.Type(null)       // → type null

],

demoDateTimeType = [

        date = #date(2025, 5, 21),
        time = #time(14, 30, 0),
        datetime = #datetime(2025, 5, 21, 14, 30, 0),
        datetimezone = #datetimezone(2025, 5, 21, 14, 30, 0, 9, 0),
        duration = #duration(1, 2, 30, 0),
        dateType = Value.Type(#date(2025, 5, 21)),
        timeType = Value.Type(#time(14, 30, 0)),
        datetimeType = Value.Type(#datetime(2025, 5, 21, 14, 30, 0)),
        durationType = Value.Type(#duration(1, 2, 0, 0))

]

]

第3章 リスト型:順序付き要素集合

3.1 リストの定義と型としての性質

M言語におけるリスト(list)は、順序付きの値の集まりを表現するコンテナ型である。リストは中括弧 {} を用いて定義され、各要素はコンマで区切られる。たとえば {1, 2, 3} は3つの数値からなるリストである。

リストは他のコンテナ型(レコード、テーブル)とは異なり、**添字アクセス(0-based indexing)**により要素を取得する。これは list{index} の形式で行われ、{} はリストのみに適用される構文である。要素数を取得するには List.Count(list)、先頭要素は list{0}、最後の要素は list{List.Count(list) - 1} のようにアクセスできる。

M言語の評価系では、リスト全体の型は type list として固定されるが、内部の要素の型については保持しない。したがって、リスト型自体は「構造型」ではあるが、「中身の型は自由(非束縛)」であるという特徴を持つ。これは後述の型非均質性にも関係する。

3.2 要素の型均質性と非均質性

リストはその性質上、型的に均質である必要がない。つまり、{1, "abc", true} のように、数値・文字列・論理値が混在した構造も正当な list として受け入れられる。これは、M言語がリストを「順序つきの評価可能値の列」として扱うためであり、型安全性より柔軟性を重視した設計思想の現れである。

ただし、実際の関数処理(例:List.Sum, List.Average, List.Transform) においては、関数側が要素型の一致を前提とすることが多く、異なる型の混在はランタイムエラーの原因となる。たとえば List.Sum({1, 2, "abc"}) は評価時にエラーを返す。

そのため、実務上はリストの要素を List.Transform(list, each Value.Type(_)) などで確認し、事前に型整形を行う設計が望まれる。特定の型のみを抽出するには List.Select(list, each Value.Is(_, type number)) のような書き方が有効である。

3.3 ネストされたリストの型評価

M言語ではリストを入れ子(ネスト)にすることが可能であり、たとえば {1, {2, 3}, {4, {5}}} のような多階層構造をとることができる。ネストされたリストも通常のリストとして評価され、内部のリストは単一要素として扱われる。

このため、ネスト構造を評価するには、再帰的な型確認やドリルダウンが必要となる。たとえば、リスト内の第2要素 {2, 3} にアクセスするには list{1} を使用し、それがさらにリストであるかどうかを判定するには Value.Is(list{1}, type list) のような書き方になる。

M言語は、リストの階層的構造に対して明示的なスキーマを持たないため、ネストの深さや構造の整合性を保証するには、評価前に List.Transform や List.Accumulate を組み合わせた明示的な検査が推奨される。

リストのネストは特に、レコードやテーブルの列として格納された複数値データや、JSON構造の解析処理において頻繁に使用される。これらのケースでは、ネストされたリスト構造をドリルダウン → フラット化 → 型変換する一連の処理設計が不可欠である。

[

demoListDefinition = [

numericList = {1, 2, 3, 4, 5},

        mixedList = {1, "abc", true, null},
        listType = Value.Type({1, 2, 3})         // → type list

],

demoElementAccess = [

        list = {"a", "b", "c"},
        first = {"a", "b", "c"}{0},              // → "a"
        last = {"a", "b", "c"}{2},
        count = List.Count({"a", "b", "c"})

],

demoHomogeneousVsHeterogeneous = [

homogeneous = {10, 20, 30},

        heterogeneous = {10, "twenty", true},
        sumAttemptValid = List.Sum({10, 20, 30}),    // → 60
        sumAttemptError = try List.Sum({10, "twenty", 30}) otherwise "Type Error",
        typesInList = List.Transform({10, "twenty", true}, each Value.Type(_))

],

demoListFilteringByType = [

        mixed = {1, "abc", null, 3.14, false},
        onlyNumbers = List.Select({1, "abc", null, 3.14, false}, each Value.Is(_, type number)),
        onlyText = List.Select({1, "abc", null, 3.14, false}, each Value.Is(_, type text))

],

demoNestedList = [

nested = {1, {2, 3}, {4, {5, 6}}},

level1 = {1, {2, 3}, {4, {5, 6}}}{1}, // → {2, 3}

        isList = Value.Is({1, {2, 3}, {4, {5, 6}}}{1}, type list),// → true

deepAccess = {1, {2, 3}, {4, {5, 6}}}{2}{1}{0} // → 5

]

]

第4章 レコード型:名前付きフィールドの集合

4.1 フィールド名と型の対応関係

M言語におけるレコード型は、名前付きフィールドの集合を表現する構造型のひとつである。レコードは1つ以上のフィールドを持ち、それぞれのフィールドには一意の名前と対応する値が含まれる。各値には独立した型があり、レコード全体の型は、個別のフィールド名とその型を対応づけたスキーマとして表現される。

レコードのフィールドには、固定された順序は存在せず、名前によってアクセスされる。たとえば Name = "Alice", Age = 30 のようなレコードでは、Name が文字列型、Age が数値型として扱われる。これらの型情報は Value.Type によって取得可能であり、型は type [Name = text, Age = number] のような形式で示される。

レコードへのアクセスには、角括弧構文(record[Name])または Record.Field(record, "Name") を使用する。前者は静的参照、後者は動的参照を可能にする。また、フィールド名に空白などが含まれる場合は #"Full Name" のように引用符形式で表現される。

4.2 レコード型のスキーマと部分一致性

レコード型は、構文的にも type record として明示でき、スキーマ(各フィールド名とその型のセット)を持つ。たとえば type [A = number, B = text] は、A列が数値、B列が文字列である構造を示す。

M言語の型システムでは、レコード型の評価において部分一致が許容される。つまり、レコード値にスキーマ定義されていないフィールドが存在していても、定義された型に一致する部分のみを見ることで整合が取れる。これは Type.Is(value, type [A = number]) が true を返す例などに現れる。

ただし、型注釈付きで as type [A = number, B = text] とした場合、評価時に余分なフィールドが存在すると Expression.Error になる。従って、厳密一致が必要な場面では Value.ReplaceType によってスキーマを明示的に注釈し、予期しないフィールドを排除することが推奨される。

4.3 任意フィールドを含む型の扱い

M言語では、レコード型の柔軟性を高めるために任意の追加フィールドを許容する型記述が可能である。この形式では、次のような構文が用いられる:type [A = number, B = text, ...]。この ... は、AとB以外の未定義フィールドが存在してもよいことを意味しており、部分一致よりもさらに緩やかなスキーマ制限となる。

ただし、これは型比較の内部処理や自動型推論の一部として認識されるものであり、as type のような明示的な注釈にこの ... を含めることはできない。そのため、実務においては、必要最小限のフィールドだけを型制約に含め、その他のフィールドを柔軟に許容するために Value.Type や Type.Is を活用する設計が現実的である。

このように、レコード型は「定義された最小限の構造を守りながら、将来的な拡張性を担保する」ための型設計が可能であり、M言語におけるデータ構造の記述力の中核を成している。

[

demoBasicRecord = [

        record = [Name = "Alice", Age = 30],
        valueByKey = [Name = "Alice", Age = 30][Name],
        valueByFunction = Record.Field([Name = "Alice", Age = 30], "Age"),
        recordType = Value.Type([Name = "Alice", Age = 30])  // → type [Name = text, Age = number]

],

demoPartialTypeMatch = [

        record = [ID = 1, Name = "Bob", Extra = "Optional"],
        matchesPartial = Type.Is([ID = 1, Name = "Bob", Extra = "Optional"], type [ID = number]),
        mismatchesStrict = try ([ID = 1, Name = "Bob", Extra = "Optional"] as type [ID = number, Name = text]) otherwise "Error" // → Error due to Extra

],

demoOptionalFields = [

        full = [ID = 1, Name = "Charlie", Tag = "A"],

partialType = type [ID = number, Name = text],

        acceptsExtra = Type.Is([ID = 1, Name = "Charlie", Tag = "A"], type [ID = number, Name = text]),
        stripped = Record.SelectFields([ID = 1, Name = "Charlie", Tag = "A"], {"ID", "Name"}),
        strictlyTyped = Value.ReplaceType(Record.SelectFields([ID = 1, Name = "Charlie", Tag = "A"], {"ID", "Name"}), type [ID = number, Name = text])

]

]

第5章 テーブル型:ラベル付き二次元構造

5.1 列名と列型のマッピング構造

M言語におけるテーブル型は、ラベル付きの列と、順序付きの行からなる二次元構造である。テーブルは行単位で構成され、各行は同一構造を持つレコードとして内部的に扱われる。列にはラベル(列名)が与えられ、それぞれの列は特定のデータ型と対応づけられる。この列名と列型のセットがテーブルのスキーマを構成する。

テーブルの列型は、テーブル全体の型情報の一部として保持されており、たとえば type table [Name = text, Age = number] のような形式で型として表現される。これは、列名が Name と Age であり、それぞれの列が文字列型と数値型であることを意味している。列のデータ型は Table.TransformColumnTypes 関数などによって明示的に設定できる。

5.2 スキーマ定義と動的列の型評価

M言語では、テーブルを定義する際に明示的にスキーマを与えることもできるし、既存のデータ構造から自動的にスキーマを推論することもできる。静的に型を定義する場合は、type table [列名 = 型, …] の形式でスキーマを明記する。一方、動的に生成されたテーブルに対しては、Value.Type を用いることで現在のスキーマ構造を取得することができる。

テーブルの列の型は厳密に定義されているわけではなく、すべての列に型注釈があるとは限らない。たとえば Excel ファイルなどから読み込まれたデータは、すべて type any として初期化されることが多く、その後の型整形処理で適切な型に変換する必要がある。列ごとの型は Table.Schema 関数を使用することで確認でき、型推論やデータ品質チェックに活用される。

また、列の追加や削除はテーブルの型構造そのものに影響を与える。列を追加した場合にはスキーマに新しい列が加わり、削除した場合には型情報からもその列が失われる。型注釈が必要な場合は、Value.ReplaceType によって明示的にテーブルに型を割り当てることで、安全な構造として管理することができる。

5.3 テーブル型とレコード型の相互関係

テーブル型とレコード型は、M言語において密接な関係を持っている。テーブルの各行は、列名をフィールド名とするレコードとして表現されるため、Table.First や Table.SelectRows などの関数を用いて取得した行はレコード型の値となる。逆に、レコードの集合は、構造が一致していれば Table.FromRecords によってテーブルに変換することができる。

テーブルの列は、リスト型としてアクセス可能であり、Table.Column によって列単位の値一覧を取得できる。このとき、各行の特定列の値がリストに抽出されるため、列はレコード集合に対する射影操作と見なすことができる。一方、テーブルから任意の行を指定して取り出すと、その行はレコード型として返され、フィールド名は列名と一致する。

このように、テーブルは「レコードの集合(行)」と「ラベル付きリスト(列)」の両方として解釈可能であり、リスト型・レコード型との変換や操作が自然に行える構造となっている。これにより、構造化データを論理的にモデル化し、高度な変換処理を組み立てるための中核的なデータ型として機能している。

[

demoTableBasic = [

        sampleTable = #table(
            {"Name", "Age"},

{

                {"Alice", 30},
                {"Bob", 28}

}

),

        tableType = Value.Type(#table({"Name", "Age"}, {{"Alice", 30}, {"Bob", 28}}))

// → type table [Name = text, Age = number](推論結果)

],

demoTableColumnTypeAssign = [

        raw = Table.FromRows({{"Alice", "30"}, {"Bob", "28"}}, {"Name", "Age"}),
        typed = Table.TransformColumnTypes(

Table.FromRows({{"Alice", "30"}, {"Bob", "28"}}, {"Name", "Age"}),

            {{"Name", type text}, {"Age", type number}}

),

        schemaCheck = Table.Schema(

Table.TransformColumnTypes(

Table.FromRows({{"Alice", "30"}, {"Bob", "28"}}, {"Name", "Age"}),

                {{"Name", type text}, {"Age", type number}}

)

)

],

demoTableRowAccess = [

        table = #table({"ID", "Score"}, {{1, 90}, {2, 85}}),
        firstRow = Table.First(#table({"ID", "Score"}, {{1, 90}, {2, 85}})),
        valueByField = Table.First(#table({"ID", "Score"}, {{1, 90}, {2, 85}}))[Score]

],

demoTableColumnAccess = [

        table = #table({"Product", "Price"}, {{"Apple", 100}, {"Banana", 80}}),
        columnList = Table.Column(#table({"Product", "Price"}, {{"Apple", 100}, {"Banana", 80}}), "Price")

],

demoTableFromRecords = [

records = {

[Region = "A", Pop = 1000],

[Region = "B", Pop = 800]

        },
        toTable = Table.FromRecords({

[Region = "A", Pop = 1000],

[Region = "B", Pop = 800]

        })

]

]

第6章 関数型:評価可能な式としての型

6.1 関数も「値の一種」であるという型の捉え方

M言語では、関数も通常の値と同様に扱われる。つまり、関数は first-class value(第一級の値)であり、リスト・レコード・変数などと同じように、他の関数に渡したり、データ構造の一部に含めたりすることができる。

関数は、評価可能な式でありながら、そのまま値として定義・保持・転送が可能である。たとえば関数式が変数に格納され、その変数を通じて後から関数を呼び出すこともできる。さらに、関数そのものに対して型情報を取得することが可能であり、Value.Type を用いれば type function の結果が得られる。これは、関数が「型を持った値」としてM言語の型システムに正式に位置付けられていることを示している。

6.2 引数・戻り値型の明示と省略

関数は、引数の型と戻り値の型を明示することができる。構文上、(x as number) as number => x + 1 のように記述することで、x は数値型であり、結果も数値型であることが保証される。この形式により、型整合が明示され、評価エラーの予防や意図の明確化につながる。

一方、型注釈を省略することも可能であり、(x) => x + 1 のような形式で定義された関数は、実行時に引数の型が決定される。省略時は暗黙の型推論に依存するため、柔軟性は高いが型安全性は下がる。このため、特に明確なスキーマや再利用を想定した関数には型注釈が推奨される。

関数の戻り値型も同様に省略可能であるが、型情報を明示しておくことで、関数の合成や高階関数の利用時により予測可能な動作が得られる。特に複数の関数を組み合わせる処理においては、戻り値の型明示が構文解析の一貫性を高める。

6.3 高階関数の型的性質

M言語における関数型は、高階関数を自然に扱える構造を持っている。関数が関数を引数に取ったり、戻り値として関数を返したりすることができるため、処理の抽象化や再利用性を高める設計が可能となる。

たとえば、List.Transform(list, each _ + 1) のように記述された構文では、リスト内の各要素に適用するための関数が、引数として関数の引数に渡されている。ここで使用される each 構文は、匿名関数を簡潔に記述する構文糖であり、内部的には (x) => x + 1 のような関数定義と等価である。

関数型としての定義は、Value.Type を使うことで type function の形で確認できる。さらに、Value.ReplaceType を用いれば、関数に対して明示的に型注釈を与えることも可能である。これにより、関数の型整合性を厳格に管理し、型安全な高階関数の構築が可能となる。

高階関数の利用は、List 系や Table 系の変換処理において非常に一般的であり、M言語におけるデータ変換の基礎として機能している。関数型を明確に定義し、型に基づいて設計された関数群は、クエリの再利用性と信頼性を高める重要な要素である。

[

demoFunctionAsValue = [

        addOne = (x) => x + 1,
        callAddOne = (x) => x + 1)(5),
        functionType = Value.Type((x) => x + 1)  // → type function

],

demoTypedFunction = [

        typedAdd = (x as number, y as number) as number => x + y,
        result = (x as number, y as number) as number => x + y)(3, 4),
        typedFunctionType = Value.Type((x as number, y as number) as number => x + y)

],

demoAnonymousFunctionEach = [

source = {1, 2, 3},

        transformed = List.Transform({1, 2, 3}, each _ * 10),
        equivalent = List.Transform({1, 2, 3}, (x) => x * 10)

],

demoHigherOrderFunction = [

        applyTwice = (f as function, x) => f(f(x)),
        double = (n as number) as number => n * 2,
        result = (f as function, x) => f(f(x)))((n) => n * 2, 5)  // → 20

],

demoFunctionTypeAnnotation = [

        original = (x) => x + 1,
        annotated = Value.ReplaceType(

(x) => x + 1,

type function (x as number) as number

),

        typeCheck = Value.Type(

Value.ReplaceType((x) => x + 1, type function (x as number) as number)

)

]

]

第7章 型のネストと合成構造

7.1 コンテナ型におけるネスト構造

M言語では、リスト・レコード・テーブルといったコンテナ型は、それ自体が他の値を内部に保持する構造であり、これらの中にさらにコンテナ型を格納することが可能である。これを型のネスト構造という。たとえば、リストの要素としてレコードを格納する、あるいはテーブルの列にリストを含めるなどの構成が可能である。

このような構造は、データが階層的である場合や、入れ子になったJSONやXMLの構造をそのまま保持したい場面で特に有効である。M言語では、すべての値が型を持つため、ネストされた各値にも明確な型が存在する。コンテナ型の中に別のコンテナ型を保持する場合、その型情報は構造ごとに独立して評価される。

7.2 入れ子型の評価と型整合性

M言語では、ネストされた型構造に対しても Value.Type を用いて個別に型を取得することが可能である。たとえば、リストの中にレコードが含まれている場合、その要素に対して Value.Type を適用すれば、レコード型として評価される。同様に、レコードのフィールドがテーブルである場合にも、テーブル型として認識される。

このような構造において重要となるのは、型整合性である。つまり、ネスト構造の中でどのような型の値が含まれているかを、評価前に正確に把握しておくことで、関数の適用や変換操作が安全に行える。たとえば、List.Transform をネストされたリストに適用する際には、変換対象がリストかどうかを事前に確認することでエラーを防ぐことができる。

さらに、テーブル内の列にレコードが格納されている場合、Table.ExpandRecordColumn などの関数を使用することで、入れ子構造をフラット化し、列ベースのスキーマに整形することができる。こうした変換処理では、入れ子になった各構造がどのような型を持っているかが、正確な処理設計の前提となる。

7.3 型の再帰構造と抽象化

M言語では、型自体を再帰的に構成することが可能である。たとえば、レコードのフィールドとして別のレコードを含めたり、リストの中にリストを含めるといった構成は、再帰的な型構造を生み出す。このような再帰構造は、複雑な階層データや柔軟なデータ設計に適しており、構文的にも Value.Type によって型情報をたどることができる。

再帰構造は、データの抽象化にも寄与する。たとえば、任意の数の項目を持つリストや、可変の構造を含むレコードを設計することで、動的なスキーマに対応した抽象的なデータモデルが構築できる。これは、固定スキーマの構造とは異なり、柔軟なデータ流通や将来的なスキーマ拡張に対応するための戦略として有効である。

ただし、抽象的な構造は明示的な型整合性の保証が難しくなるため、事前に型を確認し、必要に応じて Type.Is や Value.ReplaceType を使って制約を明示することで、安全性と柔軟性の両立を図ることが重要である。ネスト型と再帰構造を適切に活用することで、M言語における高度なデータ設計が実現可能となる。

[

demoNestedContainers = [

        listWithRecord = { [Name = "Alice", Age = 30], [Name = "Bob", Age = 28] },

recordWithList = [Scores = {90, 85, 88}, Passed = true],

        tableWithNestedList = #table(
            {"Student", "Scores"},

{

                {"Alice", {90, 85}},
                {"Bob", {80, 82}}

}

)

],

demoTypeEvaluation = [

nestedList = {1, {2, {3}}},

level1 = {1, {2, {3}}}{1}, // → {2, {3}}

        isList = Value.Is({1, {2, {3}}}{1}, type list), // → true
        innerType = Value.Type({1, {2, {3}}}{1}),       // → type list

nestedRecord = [A = [B = [C = 100]]],

        typeOfC = Value.Type([A = [B = [C = 100]]][A][B][C])  // → type number

],

demoRecursiveStructure = [

recursiveRecord = [

ID = 1,

Meta = [

                Tags = {"x", "y"},
                Sub = [
                    Status = "active"

]

]

],

        getSubStatus = [ID = 1, Meta = [Tags = {"x", "y"}, Sub = [Status = "active"]]][Meta][Sub][Status],

recursiveList = {1, {2, {3, {4}}}},

deepValue = {1, {2, {3, {4}}}}{1}{1}{1}{0} // → 4

],

demoAbstractTyping = [

        genericRecord = [A = 1, B = "text", C = {true, false}],
        typeInfo = Value.Type([A = 1, B = "text", C = {true, false}]),
        isCompatible = Type.Is([A = 1, B = "text", C = {true, false}], type [A = number, B = text])

]

]

第8章 型の取得・比較・注釈のための型的要素

8.1 Value.Type による実行時型取得

M言語では、すべての値に型が付与されており、その型は Value.Type 関数を用いることで動的に取得できる。たとえば数値リテラルに対して Value.Type(123) を適用すると、結果として type number が返される。これは、その値が評価された結果として number 型であることを意味する。

この関数は、リスト・レコード・テーブル・関数などすべての型に対して利用可能であり、複雑な構造を持つ値に対しても、階層的に適用することで個々の構成要素の型を特定することができる。たとえば、レコードの特定のフィールドに対して Value.Type(record[Name]) のように適用すれば、そのフィールドの型を個別に取得できる。

実行時型の取得は、動的に生成されたデータの構造を評価する際や、デバッグ時の構造確認に非常に有効である。また、後述の Type.Is との組み合わせにより、型に基づく分岐や制御構造の設計も可能となる。

8.2 type [...] による静的型記述

M言語では、型そのものを構文として明示的に記述することができる。たとえば type number や type list, type [Name = text, Age = number] のように記述された構造は、それ自体が型リテラルとして認識され、型の比較や注釈に使用することができる。

この静的型記述は、関数の引数や戻り値の型注釈としても用いることができ、型安全性と可読性を向上させる。また、Value.ReplaceType 関数を使えば、既存の値に対して型情報を後から適用することもできる。これにより、外部から取得した型不明なデータに対しても、明示的なスキーマを与えることで、以後の操作を安全に行うことが可能になる。

静的型記述は、あくまで構文的な型のラベルであり、型の定義ではなく参照に過ぎない。そのため、型記述自体は評価されず、あくまで比較・注釈・制御のための要素として機能する。

8.3 Type.Is, Type.Equals による型比較ロジック

M言語では、型の等価性や包含関係を判定するために Type.Is および Type.Equals の2つの関数が提供されている。Type.Is(value, type) は、ある値が指定した型に一致または部分一致するかどうかを論理値で返す。たとえば Type.Is(123, type number) は true を返す。

一方、Type.Equals(type1, type2) は、2つの型が完全に一致しているかどうかを評価する。これは、スキーマ型や関数型など、構造の一致が重要となる場面で使用される。たとえば type [A = number, B = text] と type [B = text, A = number] は、記述順が異なるが型的には等価と判定される。これはフィールド順が型の一致に影響しないM言語の仕様に基づいている。

このような型比較の機構は、条件分岐や型ベースのデータ整形、動的スキーマ検証などにおいて強力な手段となる。型が明示されていないデータに対して、部分的に型を確認した上で操作することで、予期しないエラーや構造不整合を未然に防ぐことができる。

[

demoValueType = [

numberValue = 123,

        numberType = Value.Type(123),                      // → type number

listValue = {1, 2, 3},

        listType = Value.Type({1, 2, 3}),                  // → type list
        recordValue = [Name = "Alice", Age = 30],
        recordType = Value.Type([Name = "Alice", Age = 30]) // → type [Name = text, Age = number]

],

demoStaticTypeExpression = [

explicitType = type [Name = text, Age = number],

        annotated = Value.ReplaceType([Name = "Bob", Age = 25], type [Name = text, Age = number]),
        typeOfAnnotated = Value.Type(Value.ReplaceType([Name = "Bob", Age = 25], type [Name = text, Age = number]))

],

demoTypeIs = [

input = [X = 1, Y = 2],

        partialCheck = Type.Is([X = 1, Y = 2], type [X = number]),       // → true
        fullCheck = Type.Is([X = 1, Y = 2], type [X = number, Y = number]), // → true
        failCheck = Type.Is([X = 1, Y = 2], type [X = number, Z = number])  // → false

],

demoTypeEquals = [

type1 = type [A = number, B = text],

type2 = type [B = text, A = number],

        areEqual = Type.Equals(type [A = number, B = text], type [B = text, A = number]), // → true
        notEqual = Type.Equals(type [A = number, B = text], type [A = number])            // → false

]

]

第9章 クエリ名の型的扱い

9.1 クエリは「型のついた名前付き式」である

Power Query におけるクエリとは、実際には「評価可能な式に名前をつけたもの」である。これは、定数や関数、テーブルやレコードといった任意の値を返す式であり、その評価結果には常に型が伴う。クエリはワークスペース内で名前によって識別され、他のクエリや関数から参照可能である。

たとえば、テーブルを返すクエリ Customers は、内部的には Customers = ... という定義であり、Customers を評価すればテーブル型の値が得られる。そのため、Value.Type(Customers) のようにクエリ名に対して型取得を行うことができる。M言語においては、すべてが式であるという原則に従い、クエリ名もまた式であり、型を持った値として動作する。

9.2 クエリを型表現として参照する構文的振る舞い

M言語では、クエリ名をそのまま式の一部として用いることで、暗黙的に「型付きの値」として扱うことができる。たとえば、あるクエリ Orders が type table [OrderID = number, Amount = number] のテーブルを返す場合、そのクエリを他のクエリや関数の中で参照すれば、型情報が引き継がれた状態で式評価が行われる。

この性質により、クエリは事実上「型付き定数」として振る舞い、スキーマの一貫性を維持する手段として活用できる。たとえば、Table.Combine({Orders, NewOrders}) のような操作では、Orders クエリに定義されたスキーマが操作対象として継承される。クエリ名そのものが型付きスキーマの抽象名として機能していることを意味する。

また、関数やマクロ的な処理でクエリを受け渡す場合にも、クエリの型が評価文脈に影響を与えるため、型推論の精度向上にも寄与する。これは、M言語が静的型注釈よりも動的型推論を重視する中で、型の伝播元としてクエリが果たす重要な役割である。

9.3 クエリの再利用と型安全性の設計戦略

クエリは、明示的に型付けされた名前付き式として定義されることにより、再利用性と安全性の両立が図られる。たとえば、あるデータセットの前処理結果を定義したクエリ CleanedData は、後続のクエリで何度でも参照でき、そのスキーマに基づいた操作が可能となる。

さらに、Value.ReplaceType を用いてクエリに型注釈を与えれば、たとえ読み込み元の構造が曖昧であっても、明示的なスキーマ制約によってその後の操作の型安全性を担保できる。これにより、動的に変化するデータ構造に対しても、クエリ単位での抽象的な型定義を保ちながら、エラーの少ない再利用が実現できる。

このように、クエリを「型のついたラベル」として設計することで、複数クエリ間のスキーマ整合性を保ちつつ、柔軟かつ堅牢なデータ変換プロセスを構築することが可能になる。M言語におけるクエリの型的扱いは、単なる表データの参照ではなく、再帰可能で予測可能なデータモデル設計の中心に位置付けられている。

CustomerTableクエリ

let

    Source = #table(
        {"CustomerID", "Name"},

{

            {1, "Alice"},
            {2, "Bob"}

}

)

in

Source

CustomerTable_Typedクエリ

let

    Annotated = Value.ReplaceType(

#table(

            {"CustomerID", "Name"},

{

                {1, "Alice"},
                {2, "Bob"}

}

),

type table [CustomerID = number, Name = text]

)

in

Annotated

CheckCustomerTableTypeクエリ

let

    T = Value.Type(CustomerTable_Typed)

in

T

IsCustomerTableValid

let

    result = Type.Is(

CustomerTable_Typed,

type table [CustomerID = number]

)

in

result

UseCustomerInMergeクエリ

let

    Other = #table(
        {"CustomerID", "Region"},

{

            {1, "East"},
            {2, "West"}

}

),

    Merged = Table.Join(

CustomerTable_Typed,

"CustomerID",

Other,

"CustomerID"

)

in

Merged