先般、PBIJP Power Query 秘密特訓「虎の穴」炎の復活編 というイベントをやりまして、そこで話した内容など振り返りつつまとめておこうかなと。
拗らせるに至った軌跡のひとつから
Power Query の特徴を理解できそうかなぁというテーマ的なものをつぶやいていて、このあたりのお話がよいかなぁと。
そのままなんだけど、 テーブル に Text.NewGuid - PowerQuery M | Microsoft Docs を使って、GUID 列を追加したときどのような挙動になるかとーく観察するとよいよって話。で、行ごとでユニークな値になるよう考えていくと Power Query の根っこの話が理解できそうかな?と思ったのである。
Table.AddColumn( SourceTable, "GUID", each Text.NewGuid() )
というコードを記述したとき、得られる結果はすべての行で GUID はすべて同じになるのである。
で、すべての行でユニークなGUID列を作りにはどうしたらよいかな?の過程から Power Query のスペックを整理しちゃおうぜって話。
Power Query エディタに表示される値はすべてプレビューである
Power Querry エディタでの表示と得られたクエリ結果が異なることがあるし、
Power Query エディタでも表示される結果が異なることもある。
これらは、Powe Query エディタでの表示はすべてプレビューであるから。
エディタ既定の状態で先頭 1000 行を表示しているわけだし、[適用するステップ]ごと通りに処理が実施されるわけではないので、Power Query エディタで表示される通りの結果が得られることは保証されていないのだ。一致することがほとんどというだけなので、信用できるかどうかではなく、プレビューを参考にするという感覚での操作が望ましいかなと。
ボタンポチポチだけで済ませてしまうことは否定しないけれども、そのボタンポチひとつで出力される Power Query のコードはプレビューの範囲を対象としたサンプリング結果から生成されているのだから、考えなしでいると失敗を防ぐことができないか、失敗に気づくことがないのだ。
すべての行で同じ値になるのはなぜなんだ
評価戦略によるもの
すでに評価されている値は再評価しないという戦略だから。
ちょっと記述を変えてみたり、拗らせてみても結果は変わらない。
let
NewGUID = Text.NewGuid(),
AddedGuid = Table.AddColumn(
SourceTable,
"GUID",
each
NewGUID,
Text.Type
)
in
AddedGuid
Table.AddColumn(
SourceTable,
"GUID",
each
Expression.Evaluate( "Text.NewGuid()", #shared ),
Text.Type
)
なぜ?どうしたら?で理解しようとする
Power Query の仕様を理解しておけばよいだけなんだけど、わかりにくいわりに説明があっさりなので。
Evaluation model - PowerQuery M | Microsoft Docs
ポイントはここ。
Lazy and eager evaluation
List, Record, and Table member expressions, as well as let expressions (See Expressions, values, and let expression), are evaluated using lazy evaluation: they are evaluated when needed. All other expressions are evaluated using eager evaluation: they are evaluated immediately, when encountered during the evaluation process. A good way to think about this is to remember that evaluating a list or record expression will return a list or record value that knows how its list items or record fields need to computed, when requested (by lookup or index operators).
遅延評価
書いてあることとにらめっこしててもわからないままだから、ななめ読みであっても確かめながらでよいはずだ。 list, record, table は遅延評価されるよと書いてあるなと。で、ほんとなのか?と。
Table.AddColumn(
SourceTable,
"GUID",
each { Text.NewGuid() }{ 0 }
)
list で試してみるが結果は変わらず、すべての行で GUID 列はすべて同じ値になる。なぜか。
"{ Text.NewGuid() }" という List.Type の値が遅延評価の対象とはなっておらず、行ごとの処理(Table.AddColumn)で再評価されないから。
Table.AddColumn(
SourceTable,
"GUID",
each { 0, Text.NewGuid() }{ 1 }
)
では、行ごとの処理(Table.AddColumn)で再評価が行われるようにするにはどうしたらよいか。カレント行を参照すればよいのだ。
Table.AddColumn(
SourceTable,
"GUID",
each { Text.NewGuid(), _ }{ 0 }
)
行ごとの処理(Table.AddColumn) では "{ Text.NewGuid(), _ }" のリストアイテム "_" はカレント行を表す Record.Type の値になるけれども、行ごとで個別に評価されなければならないから、"{ Text.NewGuid(), _ }" は評価済みの値になりえず、リストに対しアイテムアクセスや集計が行われるまで評価は遅延となる。 "{ 0 }"(アイテムアクセス)で Text.NewGuid() が初めて評価されることになり、結果として異なる GUIDを返す。リストに含まれるアイテム "_" は参照されることはないから遅延評価という戦略で評価されていない。
Table.AddColumn(
SourceTable,
"GUID",
each let current = _ in Text.NewGuid(),
Text.Type
)
Table.AddColumn(
SourceTable,
"GUID",
each [ Guid = Text.NewGuid(), Current = _ ][Guid],
Text.Type
)
Table.AddColumn(
SourceTable,
"GUID",
each Function.Invoke((x)=> Text.NewGuid(), {_}),
Text.Type
)
思ったこと🙄
Number.Random 関数も同じ値を返すよね。
DateTime.LocalNow 関数はだいたいの場合で同じ日付時刻を返すよね。
話すことあらかじめまとめておくことがいいかなぁと思ってはいたのだけど、持ち時間なさそうだったしなという都合の良い解釈でほぼ準備なく正直すまんかった。