TypeScript で作った、System.Drawingっぽい HTML Canvas ライブラリ
MSCC用に作ったアプリでは、HTML Canvasのお絵かきツールをTypeScriptでつくりました。
その中からCanvas面を操作する部分をライブラリに切り出してGitHubで公開してみました。
System.Drawing 風?
Canvasへの図形等の描画処理を .Net Framework の System.Drawing名前空間にあるようなGDI操作っぽく作ってみました。
- 描画面への書き込み処理は、CanvasクラスのonPaintイベントハンドラに記述
- onPaint の引数に渡されるGraphicsオブジェクトに用意された各種描画用メソッドを使用
- ストロークや塗りつぶしのスタイルは
Pen
やBrush
などの(それっぽい)オブジェクトで指定 - Canvasクラスには、マウスやタッチ操作に対応したイベントハンドラを用意(現状、Down、Move、Upしかありませんが)
window.onload = (ev: Event) => { var canvas = new Canvas("canvas1"); //描画処理はonPaint イベントハンドラで canvas.onPaint = (g: DrawingTs.Graphics): any => { g.clear(true); g.drawString(message, Font.Create("sans-serif", 18), new SolidBrush(new Color("blue")), 4, 18); }; //Pointerイベントは、Mouse,Touch,Pen の差異を吸収しいてる(はず。たぶん) canvas.onPointerDown = (ev: DrawingTs.CanvasPointerEvent) => { message = "PointerDown (" + ev.position.x + ", " + ev.position.y + ")" canvas.paint(); }; canvas.onPointerMove = (ev: DrawingTs.CanvasPointerEvent) => { message = "PointerMove (" + ev.position.x + ", " + ev.position.y + ")"; canvas.paint(); }; canvas.onPointerUp = (ev: DrawingTs.CanvasPointerEvent) => { message = "PointerUp (" + ev.position.x + ", " + ev.position.y + ")" canvas.paint(); }; //Canvas面への書き込みはpaintメソッドで実行 canvas.paint();
どんなクラスやメソッドがあるかは、こちらの定義ファイル(drawingTs.d.ts)で確認できます。
読み込んだ画像を表示する drawImage() はまだ未実装だったり、まだまだ機能不足ですが。。。
サンプルコード
サンプルコードと実行結果がこちらで確認できます。
ブラウザ上で描いた絵をTwitterで共有するWebアプリを作りました
ピクエスト(α)
MSCC 提出作品です。
現在α版としてテスト公開中です。
- リクエストされたテーマに沿って絵を描きます。
- 描いた絵を画像付きツイートとしてTwitterへ投稿します。
- リクエストは、ユーザーが自由に作成して追加することが出来ます。
描いた絵はTwitterで共有
画像付きツイートとして投稿しますので、フォロワーさんのタイムライン上に描いた絵を表示させることが出来ます。
Twitterとの連携について
リクエスト一覧内で[リクエストに応える]ボタンをクリックするとTwitterの認証画面に移動します。ここで、
- Twitterの使用を許可した場合は、許可したユーザーのアカウントで投稿します。
- 許可せず「キャンセル」した場合は、Piquestの公式アカウント @piquest で投稿します。
ASP.NET MVC
このWebアプリケーションは、ASP.NET MVC5で実装し、Azure Webサイト上で動いています。
Webアプリとしての基本的な動作は、ほとんどASP.NETが面倒を見てくれるため、この部分のコーディング量は驚くほど少なくて済みました。
CoreTweet
また、Twitterとの連携機能にはCoreTweetを使用させていただきました。こちらも数行書き加えるだけの簡単なお仕事でした。ありがとう CoreTweet。
HTML Canvas + TypeScript
お絵かきツール部分は、HTML Canvasの操作をTypeScriptで記述しました。
機能的には、最低限の物しか実装できていませんが、コーディング量としてはここが一番大きく、時間もかかりました。
TypeScriptは初めて触りましたが、思った以上に違和感なく使えていい感触でした。
最後に
機能的にはまだまだ貧弱で、間に合わせた感は否めませんが、コンテスト後も少しずつ手を加えて改良して行きたいと思います。
どなたか、使ってみてくだされ~
gooラボ 形態素解析APIを試してみる
これはPowerShell Advent Calendar 2014 : ATND 13日目の記事です。
PowerShell で REST API
近年、RESTfullなAPIで提供されるWebサービスが非常に増えているように感じます。
そんな中、ちょっと気になるAPIがあったら、気軽に試したりしたいですよね。
「PowerShellを使えば、お手軽にREST API がお試しできますよ!」
という事で、最近見かけた、gooラボの形態素解析APIを試してみたいと思います。
NTTレゾナント、gooで開発・蓄積した「日本語解析API」を公開、ビッグデータ解析機能なども提供予定:CodeZine
と、4種類のAPIが提供されています。
この中から今回は形態素解析APIを使います。
上の記事の内容を見る限りですと、文を語句に分割するだけ?のように思えますが、「PowerShell, 名詞, パワーシェル」の様に、分割した「語句」の「品詞(形態素)」と「読み」を付けて返してくれます。
形態素解析API:日本語文字列を語句に分割する技術 - gooラボ
Invoke-RestMethod でPOST
- リクエスト先URL
https://labs.goo.ne.jp/api/morph- リクエストパラメータ
application/x-www-form-urlencoded、application/json形式でのPOSTを受け付けます。
とあります。
Invoke-RestMethod
コマンドレットで試してみます。今回はapplication/jsonでPOSTします。
リクエストパラメータ
app_id
アプリケーションID。
必須項目。 こちら(https://labs.goo.ne.jp/apiusage/)で取得した、アプリケーションIDを設定します。(IDの取得にはGitHubアカウントが必要になります。)request_id
リクエストID。
省略時は”labs.goo.ne.jp[タブ文字]リクエスト受付時刻[タブ文字]連番”となります。sentence
解析対象テキスト。
必須項目。info_filter
形態素情報フィルタ。
form(表記)、pos(形態素)、read(読み)のうち、出力する情報を文字列で指定します。pos_filter
形態素品詞フィルタ。
出力対象とする品詞を”|”で区切って指定します。
リクエストパラメータは、HashTebleで作成し、Invoke-RestMethod
のBody
パラメータに指定します。
今回は、とりあえず必須項目のapp_id
とsentence
のみを使用します。
PS C:\> $text = "Windows PowerShell は、マイクロソフトが開発した拡張可能なコマンドラインインターフェース (CLI) シェルおよびスクリプト言語である。" + ` "オブジェクト指向に基づいて設計されており、.NET Framework 2.0 を基盤としている。" PS C:\> $res = Invoke-RestMethod -Method Post ` -Uri https://labs.goo.ne.jp/api/morph ` -Body @{ app_id = "your application id"; sentence = $text; }
レスポンスの取得
レスポンスパラメータは、以下の通りです。
request_id
リクエストID。
リクエストと同じ値となります。info_filter
形態素情報フィルタ。
入力と同じ値となります。pos_filter
形態素品詞フィルタ。
入力と同じ値となります。word_list
形態素リスト。
形態素リストは文ごとに分かれた文単位形態素リストの配列となります。
文単位形態素リストは形態素情報の配列で、形態素情報には表記・形態素・読みのうち形態素情報フィルタで指定された要素が含まれます。
例:
Invoke-RestMethod では、JSONで帰ってきたレスポンスパラメータをPSObjectにデシリアライズして返してくれます。大変便利ですね。
PS:\> $res request_id word_list ---------- --------- labs.goo.ne.jp 1418127883 0 {System.Object[] System.Object[] Sys...
解析結果の本体は、word_listにあります。展開してみましょう。
PS C:\> $res.word_list |%{$_} Windows åè© ã¦ã£ã³ãã¼ãº ç©ºç½ ï¼ Power åè© ãã¯ã¼ Shell åè© ã·ã§ã« ...
ん!? 文字化けしちゃってますね
レスポンスのエンコーディングについて
Invoke-RestMethod コマンドレットは、レスポンスヘッダ(Content-Type ;charset?) を見てエンコーディングしてくれるようなのですが、
このAPIのレスポンスヘッダにはcharsetの指定が無いようなのです。
その場合ってどうなるの?
どうやらcahrsetの指定がない場合は ISO-8859-1
でエンコーディングされるらしい。
確認してみましょう。生のレスポンスが必要なのでInvoke-RestMethod
ではなく、Invoke-WebRequest
コマンドの方を使います。
レスポンスの Content
に格納されている文字列を一旦Byte列に戻してからUtf-8でエンコーディングし直してみます。
PS C:\> $res = Invoke-WebRequest -Method Post ` -Uri https://labs.goo.ne.jp/api/morph ` -Body @{ app_id = "your application id"; sentence = $text; } PS C:\> $res.Content {"request_id":"labs.goo.ne.jp\t1418129359\t0","word_list":[[["Windows","åè©" ,"ã¦ã£ã³ãã¼ãº"],[" ","空ç½","ï¼"],["Power","åè©","ãã¯ã¼"],["Sh ell","åè©","ã·ã§ã«"],[" ","空ç½","ï¼"],["ã¯","é£ç¨å©è©","ã"],[" ... PS C:\> [System.Text.Encoding]::Utf8.GetString( ` [System.Text.Encoding]::GetEncoding("ISO-8859-1").GetBytes($res.Content)) {"request_id":"labs.goo.ne.jp\t1418129359\t0","word_list":[[["Windows","名詞","ウィ ンドーズ"],[" ","空白","$"],["Power","名詞","パワー"],["Shell","名詞","シェル"],[" ","空白","$"], ["は","連用助詞","ハ"],["、","読点","$"],["マイクロソフト","名詞","マイクロソフト"],["が","格助詞","ガ"],["開発 ...
おおー、ちゃんと読めるようになりました!
なお、レスポンスのクラスにはRawContentStream というプロパティがあり、ここからエンコード前のByte配列が取れるので、 こちらを使った方が1手間減って良いかもしれません。
PS C:\>$data = [System.Text.Encoding]::Utf8.GetString($res.RawContentStream.GetBuffer()))
データをもう少し扱いやすくする
解析結果の本体が格納された word_list
は配列が入れ子になっていて扱いづらいので、少し手を加えます。
# 以下のように入れ子になっているのを 単語(n) = [form(表記), pos(形態素), read(読み)] 文(n) = [単語(1), 単語(2), 単語(3),...] word_list = [文(1), 文(2), 文(3),...] # 以下のように平坦化します word_list = [@{SentenceNum(文番号), Form(表記), Pos(形態素), Read(読み)}, @{SentenceNum(文番号), Form(表記), Pos(形態素), Read(読み)}, @{SentenceNum(文番号), Form(表記), Pos(形態素), Read(読み)}, @{SentenceNum(文番号), Form(表記), Pos(形態素), Read(読み)},...]
PS C:\> $obj = $data | ConvertFrom-Json | %{$_.word_list} | % -Begin{ $n=0 } { $_ | % { [PSCustomObject]@{ SentenceNum=$n; Form=$_[0]; Pos=$_[1]; Read=$_[2] } }; $n++; } PS C:\> $obj SentenceNum Form Pos Read ----------- ---- --- ---- 0 Windows 名詞 ウィンドーズ 0 空白 $ 0 Power 名詞 パワー 0 Shell 名詞 シェル 0 空白 $ 0 は 連用助詞 ハ 0 、 読点 $ 0 マイクロソフト 名詞 マイクロソフト 0 が 格助詞 ガ 0 開発 名詞 カイハツ 0 し 動詞活用語尾 シ 0 た 動詞接尾辞 タ 0 拡張 名詞 カクチョウ 0 可能 名詞 カノウ 0 な 判定詞 ナ 0 コマンド 名詞 コマンド 0 ライン 名詞 ライン 0 インターフェース 名詞 インターフェース 0 空白 $ 1 ( 括弧 $ 1 CLI Alphabet シーエルアイ 1 ) 括弧 $ 1 空白 $ 2 シェル 名詞 シェル 2 および 連体詞 オヨビ 2 スクリプト 名詞 スクリプト 2 言語 名詞 ゲンゴ 2 で 判定詞 デ 2 あ 動詞語幹 ア 2 る 動詞接尾辞 ル 2 。 句点 $
これで大分扱いやすくなったと思います。
後の集計などは、PowerShellならお手の物ですよね。
名詞のみを抽出する
PS C:\> $obj | where {$_.Pos -eq "名詞"} | %{$_.Form} Windows Power Shell マイクロソフト 開発 拡張 可能 コマンド ライン インターフェース シェル スクリプト 言語 オブジェクト 指向 設計 NET Framework 基盤
形態素(品詞)のランキング
$obj | Group-Object Pos | Sort-Object count -Descending Count Name Group ----- ---- ----- 19 名詞 {@{SentenceNum=0; Form=Windows; Pos=名詞; Read=ウィンドーズ}, @{SentenceNum=0; Form=Power; Pos=名詞; Read=パワー}, @{SentenceNum=0; Form=Shell; Pos=名詞; Read=シェル}, @{SentenceNum=0; Form=マイク... 7 空白 {@{SentenceNum=0; Form= ; Pos=空白; Read=$}, @{SentenceNum=0; Form= ; Pos=空白; Read=$}, @{SentenceNum=0; Form= ; Pos=空白; Read=$}, @{SentenceNum=1; Form= ; Pos=空白; Read=$}...} 7 動詞接尾辞 {@{SentenceNum=0; Form=た; Pos=動詞接尾辞; Read=タ}, @{SentenceNum=2; Form=る; Pos=動詞接尾辞; Read=ル}, @{SentenceNum=3; Form=て; Pos=動詞接尾辞; Read=テ}, @{SentenceNum=3; Form=れ; Pos=動詞接尾辞; Rea... 4 格助詞 {@{SentenceNum=0; Form=が; Pos=格助詞; Read=ガ}, @{SentenceNum=3; Form=に; Pos=格助詞; Read=ニ}, @{SentenceNum=3; Form=を; Pos=格助詞; Read=ヲ}, @{SentenceNum=3; Form=として; Pos=格助詞; Read=トシテ}} 4 動詞語幹 {@{SentenceNum=2; Form=あ; Pos=動詞語幹; Read=ア}, @{SentenceNum=3; Form=基づ; Pos=動詞語幹; Read=モトヅ}, @{SentenceNum=3; Form=お; Pos=動詞語幹; Read=オ}, @{SentenceNum=3; Form=い; Pos=動詞語幹; Read... 3 動詞活用語尾 {@{SentenceNum=0; Form=し; Pos=動詞活用語尾; Read=シ}, @{SentenceNum=3; Form=い; Pos=動詞活用語尾; Read=イ}, @{SentenceNum=3; Form=さ; Pos=動詞活用語尾; Read=サ}} 3 句点 {@{SentenceNum=2; Form=。; Pos=句点; Read=$}, @{SentenceNum=3; Form=.; Pos=句点; Read=$}, @{SentenceNum=3; Form=。; Pos=句点; Read=$}} 2 読点 {@{SentenceNum=0; Form=、; Pos=読点; Read=$}, @{SentenceNum=3; Form=、; Pos=読点; Read=$}} 2 判定詞 {@{SentenceNum=0; Form=な; Pos=判定詞; Read=ナ}, @{SentenceNum=2; Form=で; Pos=判定詞; Read=デ}} 2 括弧 {@{SentenceNum=1; Form=(; Pos=括弧; Read=$}, @{SentenceNum=1; Form=); Pos=括弧; Read=$}} 1 連用助詞 {@{SentenceNum=0; Form=は; Pos=連用助詞; Read=ハ}} 1 Alphabet {@{SentenceNum=1; Form=CLI; Pos=Alphabet; Read=シーエルアイ}} 1 連体詞 {@{SentenceNum=2; Form=および; Pos=連体詞; Read=オヨビ}} 1 Number {@{SentenceNum=3; Form=2.0; Pos=Number; Read=ニテンゼロ}}
まとめ
(レスポンスのエンコーディングのところは予定外でしたが、)
Invoke-RestMethod
あるいはInvoke-WebRequest
コマンドレット1つでWeb APIを試すことが出来ました。
返却されるJSON(やXML)のデータも直ぐにオブジェクトとして扱うことが出来るのも良いですね。
皆さんも、気になるWeb API がありましたら、PowerShellでお試ししてみましょう!
PowerShell で ToDo 管理
これはPowerShell Advent Calendar 2014 : ATND 12日目の記事です。
PowerShellでToDo管理するスクリプトモジュールを作りました。
元々、PowerShellでスクリプトを書く練習用に作り始めたのですが、ちょっとしたメモなどを手軽に書き留めておけるので、割と重宝しています。
- コマンド1つでToDoの追加、削除、一覧できます。
- ToDoの項目毎にステータスを付けられます。
- Todo,Doing(着手・進行中),Done(完了)
- Milestone (期日・イベント事等、マイルストーン)
- 特に重要な項目にフラグ
!
を付けることが出来ます。 - ステータス、フラグの状態で色分け表示
コードは、github に置いてあります。 posh-todo.psm1 だけ落とせば使用できます。 https://github.com/pierre3/posh-todo
使い方
使い方は、以下の実行例を参照ください。
ちなみに、PowerShell_ISE 上での使用を推奨しています。(PowerShell.exe の方でも使用は可能ですが、色分けがちゃんとできないです。)
完全に自分用に作ったものなので、他の人にとって使いやすいかどうかは微妙なところですが、気になった方(がもしいれば)是非試してみて下さい。
TIPS
以下、小ネタです。
PowerShell起動時にposh-todoを開始してTODO一覧を表示
PowerShell(ISE)のプロファイルに以下の2行を追加しておくと、起動時にposh-todoが開始されて登録済みのToDo一覧が表示されるようになります。
Import-Module path\to\posh-todo.psm1 Start-PoshTodo path\to\todoFile.json
単にToDo一覧を表示するだけでは味気ないので、ようこそ!的な挨拶文も表示したいですね。
以下をStart-PoshTodo 内で呼ぶようにします。
function Show-StartingMessage { $today = (Get-Date).ToString("yyyy/MM/dd") Write-Host "こんにちは $env:USERNAME さん! 今日は $today です。" Write-Host ('{0}も{1}{2}{3}{4}ぞい!' -f ('今日', '1', '日', 'がん', 'ばる' | % { ($_,'ぞい')[(random 2)] })) }
これでPowerShellを起動するたびに以下のように表示されるようになります。
日付の入力はダイアログで
ToDo追加時のAdd-Todoコマンドで -setDate スイッチを指定すると、次のようなダイアログから日付を指定できるようになります。
このダイアログは、「Hey, Scripting Guy Windows PowerShell を使用して日付を入力する手間を省く方法はありますか 」から頂きました。
一部手を加えて、ダイアログのタイトルを指定したり、Enterキーでダイアログを閉じれるようにしたりしています。
コントロールにイベントハンドラを追加するにはAdd_KeyPress
のようなAdd_ + イベント名
のメソッドを使用するのですね。
ToDoデータは、.Netオブジェクトで保持してJSON 形式で保存
ToDoデータの扱い
ToDoデータは、後で扱うのに型があった方が何かと便利であろうと考え、.Netオブジェクトで保持するようにしました。
Add-Type -TypeDefinition @' using System; public enum TodoStatus { Todo, Milestone, Doing, Done } public class TodoItem { public int index { get; set; } public string text { get; set; } public DateTime date { get; set; } public bool flag { get; set; } public TodoStatus status { get; set; } } '@
保存はJSON
データは、ConvertTo-Json
コマンドとConvertFrom-Json
コマンドを使ってJSON形式で保存、復元します。
書き出し時は、TodoItemの配列をそのままConvertTo-Json
に渡すだけで楽ちんなのですが、
読み取った結果はPSCustomObjectで帰ってくるため、TodoItemオブジェクトに格納するのにひと手間かかります。
# 書き出しはTodoItem の配列をそのまま渡すだけでOK PS C:\> ConvertTo-Json $todo.items | Out-File $path -Encoding utf8 # 読み取った結果はPSCustomObjectの配列で帰ってくる。TodoItemに変換するのがメンドイ PS C:\> obj = Get-Content $todo.filePath -Raw -Encoding UTF8 | ConvertFrom-Json PS C:\> $todo.items = $obj | % { $todoitem = New-Object 'TodoItem' $todoitem.date = $_.date $todoitem.flag = $_.flag $todoitem.index = $_.index $todoitem.status = $_.status $todoitem.text = $_.text $todoitem }
変換部分の記述がちょっと面倒です。.Net(TodoItem)側で何とかしたいですね。
明示的型変換を実装
という事で、明示的型変換を実装してキャスト一発で変換可能にしてみます。
Add-Type -TypeDefinition @' using System; using System.Management.Automation; public enum TodoStatus { Todo, Milestone, Doing, Done } public class TodoItem { public int index { get; set; } public string text { get; set; } public DateTime date { get; set; } public bool flag { get; set; } public TodoStatus status { get; set; } public static explicit operator TodoItem(PSObject source) { return new TodoItem() { index = (int)source.index, text = (string)source.text, date = (DateTime)source.date, flag = (bool)source.flag, status = (TodoStatus)source.status } } } '@
これでOK!と思いきや、これではコンパイルが通りません。
変換元のPSObjectは、PowerShellの世界では動的に型を解決してくれますが、.Net側では「そんなメンバ定義されていませんよ」と怒られてしまいます。
インデクサでできない? index =(int)source["index"]
...これもダメでした。
.Net側でPSObjectのプロパティにアクセスするには?
.Net側ではProperties というメンバからプロパティの値を取得する必要があるようです。
source.Properties["propertyName"].Value
のように記述します。
(このPropertiesですが、PowerShell側からは見えない(参照できない)ため、その存在に気付けず苦労しました。)
Add-Type -TypeDefinition @' using System; using System.Management.Automation; public enum TodoStatus { Todo, Milestone, Doing, Done } public class TodoItem { public int index { get; set; } public string text { get; set; } public DateTime date { get; set; } public bool flag { get; set; } public TodoStatus status { get; set; } public static explicit operator TodoItem(PSObject source) { return new TodoItem() { index = (int)source.Properties["index"].Value, text = (string)source.Properties["text"].Value, date = (DateTime)source.Properties["date"].Value, flag = (bool)source.Properties["flag"].Value, status = (TodoStatus)source.Properties["status"].Value } } } '@
これで、無事コンパイルできるようになりました。キャストもちゃんと動きます。
# 読み取り処理もスッキリ! $obj = Get-Content $todo.filePath -Raw -Encoding UTF8 | ConvertFrom-Json $todo.items = $obj | % { [TodoItem]$_ } # Todoの追加時も、HashTableのキャストでOK! $todo.items += [TodoItem]@{ index = 5; text = "todo"; date = "2014/12/1"; flag = false; status = [TodoStatus]::Todo; }
PSObjectのプロパティにアクセスする方法の別解
Dynamicを使えばPSObjectのプロパティに.
でアクセス可能になります。
但し、Dynamicを使用するには"Microsoft.CSharp"を参照アセンブリに指定してあげる必要があります。
public class TodoItem { public int index { get; set; } public string text { get; set; } public DateTime date { get; set; } public bool flag { get; set; } public TodoStatus status { get; set; } public static explicit operator TodoItem(PSObject source){ dynamic d = source; return new TodoItem(){ index = (int)d.index, text = (string)d.text, date = (DateTime)d.date, flag = (bool)d.flag, status = (TodoStatus)d.status }; } } '@ -ReferencedAssemblies "Microsoft.CSharp"
MSCCに参加登録しました
Microsoft Community Champion (MSCC) に勇者として登録しました。
Microsoft Community Champion -
残念ながら戦士と魔法使いが決まらぬままスタートしてしまいそうですが、一人でもがんばるぞい!
今回はWebアプリケーションに挑戦しようかと思っています。
これまで、Webアプリケーションにはあまり縁が無く、一からのチャレンジになりますが、リタイアだけはしないように頑張りたいと思います。
という事で現在、ASP.NET MVC 5 のチュートリアルを写経したり、サンプルを眺めたりしてべんきょー中
Getting Started with ASP.NET MVC 5 | The ASP.NET Site
こちらで紹介されていた本も気になります。
PowerShellのパイプラインを遅延評価にしてみる
パイプラインからの入力をスクリプトブロックに包んで返せば、遅延評価にできるのでは? という思い付きのネタです。
例題
1から10までの整数列から偶数を抽出してその値を2乗するだけの簡単な処理を例に見てみます。
C#で書くとこんな感じ
var query = Enumerable.Range(1,10) .Where(n=> (n % 2) == 0) .Select(n=> n*n); foreach(var n in query) { Console.WriteLine(n); } /** output ** 4 16 36 64 100 */
PowerShell では以下のように書けます。
C#のSelect()
に相当する処理はSelect-Object
ではなくForEach-Object
を使いました。
また、このForEach-Object
は出力時のforeach ステートメントも兼ねています。
この一連の処理は遅延評価はされず、各コマンドレットへ値が渡った時点で即時実行されます。
PS C:\> 1..10 | Where-Object {($_ % 2) -eq 0 } | Foreach-Object { $_*$_ } 4 16 36 64 100
遅延評価版 Where、Select、Foreach
では、遅延評価のためのコマンドレットを作ってみます。
各コマンドレットの名前は Where-Block 、Select-Block、Foreach-Block とします。
Where-Block 、Select-Block はほぼ同じコードで、以下のような処理を行っています。
- Where-Block では $Predicate パラメータに bool値を返すスクリプトブロックを指定します。
- Select-Block では $Selector パラメータに 任意のオブジェクトを返すスクリプトブロックを指定します。
- Process{}では、スクリプトブロックを返却します
- スクリプトプロック内では以下の処理を行います
Where-Block、Select-Block とコマンドをつなげて実行すると、つなげたコマンドの数だけネストしたスクリプトブロック(の配列)が得られます。 これを Foreach-Block で 一気に実行する、という仕組みです。
実行してみる
$Predicate,$Selector に渡ってくる引数はParam($x)でアクセスします。
PS C:\> $query = 1..10 | Where-Block -Predicate {Param($x) ($x % 2) -eq 0} | Select-Block -Selector {Param($x) $x*$x} PS C:\> $query | Foreach-Block 4 16 36 64 100
期待通りの結果にはなりました。が、本当に遅延評価になっているのかよくわかりません。
各コマンドレットに Write-Verbose を仕込んで確認してみましょう。
(Write-Verbose版のスクリプトはこちら https://gist.github.com/pierre3/c62db52d977bc2841ff4)
PS C:\> $query = 1..10 | Where-Block -Predicate {Param($x) ($x % 2) -eq 0} -Verbose | Select-Block -Selector {Param($x) $x*$x} -Verbose PS C:\> $query | Foreach-Block -Verbose 詳細: [Where] Input Data = 1 詳細: [Where] Predicate(1) 詳細: [Where] Input Data = 2 詳細: [Where] Predicate(2) 詳細: [Where] return 2 詳細: [Select] Predicate(2) 詳細: [Select] return 4 詳細: [Foreach] return 4 4 詳細: [Where] Input Data = 3 詳細: [Where] Predicate(3) 詳細: [Where] Input Data = 4 詳細: [Where] Predicate(4) 詳細: [Where] return 4 詳細: [Select] Predicate(4) 詳細: [Select] return 16 詳細: [Foreach] return 16 16 詳細: [Where] Input Data = 5 詳細: [Where] Predicate(5) 詳細: [Where] Input Data = 6 詳細: [Where] Predicate(6) 詳細: [Where] return 6 詳細: [Select] Predicate(6) 詳細: [Select] return 36 詳細: [Foreach] return 36 36 詳細: [Where] Input Data = 7 詳細: [Where] Predicate(7) 詳細: [Where] Input Data = 8 詳細: [Where] Predicate(8) 詳細: [Where] return 8 詳細: [Select] Predicate(8) 詳細: [Select] return 64 詳細: [Foreach] return 64 64 詳細: [Where] Input Data = 9 詳細: [Where] Predicate(9) 詳細: [Where] Input Data = 10 詳細: [Where] Predicate(10) 詳細: [Where] return 10 詳細: [Select] Predicate(10) 詳細: [Select] return 100 詳細: [Foreach] return 100 100
1行目の実行時には何も表示されず、Foreach-Block を実行した時点で一度に表示されました。
スクリプトブロックが順番に実行される様子も良く分かります。
スクリプトブロックにまとめる段階で一度 配列(コレクション)が展開されてしまうのは、ショウガナイ?
PowerShell で ジェネリック版コレクション IEnumerable<T>を列挙する場合の注意
パイプ経由でのコレクションの受け渡し方などを調べようと思い、以下のようなイテレータクラスを用意しました。
IEnumerable<T>
および IEnumerator<T>
インターフェースを実装して、インターフェースの各メソッドが実行されるとそのメソッドの名前を出力するようにしています。
まずは、C# で実行してみます。
[c#] static void Main(string[] args) { var ite = IteratorHelper.CreateIterator(100, 200, 300); Console.WriteLine("[foreach]"); foreach (var s in ite) { Console.WriteLine("> " + s); } Console.WriteLine("[count]"); var count = ite.Count(); Console.WriteLine("count =" + count); Console.Read(); } /*** Output *** Constructor() [foreach] GetEnumerator() MoveNext() == True > 100 MoveNext() == True > 200 MoveNext() == True > 300 MoveNext() == False Dispose() [count] GetEnumerator() Constructor() MoveNext() == True MoveNext() == True MoveNext() == True MoveNext() == False Dispose() count =3 */
foreach や、Count(),ToArray()などの列挙が走るLinqの拡張メソッドを実行すると、上記の通り
GetEnumerator()、MoveNext()、Dispose() が順に呼ばれるのが確認できます。
では、早速PowerShellで使ってみましょう。
手始めにforeachから
PS C:\> $ite = [IteratorTest.IteratorHelper]::CreateIterator(100,200,300) Constructor() PS C:\> foreach($item in $ite){$item} GetEnumerator() MoveNext() == True 100 MoveNext() == True 200 MoveNext() == True 300 MoveNext() == False
ん?なにか違和感が... というかC#でforeachした場合と明らかに違う箇所が!
IEnumerator<T>.Dispose()
が呼ばれていない!
どうやらPowerShell のforeachは IEnumerator<T>.Dispose()
を呼んでくれないようです。
PowerShell 的には、非ジェネリック版のIEnumerable
,IEnumerator
のみを想定しているという事でしょうか?
ForEach-Object は?
PS C:\> $ite | ForEach-Object {$_} GetEnumerator() Constructor() MoveNext() == True 100 MoveNext() == True 200 MoveNext() == True 300 MoveNext() == False
呼ばれませんね。
.ForEach() メソッド?
PS C:\> $ite.ForEach({$_}) GetEnumerator() Constructor() GetEnumerator() MoveNext() == True MoveNext() == True MoveNext() == True MoveNext() == False 100 200 300
こちらもDispose()されません。
(Dispose()とは関係ありませんが、.ForEach()では列挙が先に走ってからスクリプトブロックに値が渡っているように見えます。
本来の目的としては、こういった挙動の違いを見たかったのです)
Dispose()が呼ばれないと問題になりそうなケース
PowerShellでは、通常であれば非ジェネリック版のコレクションを前提として問題なさそうなのですが、
.Net Framework のクラス等を使ってジェネリックなコレクションを扱う場合は注意しないとハマってしまいそうです。
たとえば、IEnumerable<string>
を返す System.IO.File.ReadLines()
メソッドでは
以下のように foreachがDispose()呼んでくれるおかげで明示的にファイルを閉じる必要がありません。
[C#] IEnumerable<string> lines = System.IO.File.ReadLines(); //この時点では読み取り実行されていない。ファイルは開いたまま foreach(var line in lines) { Console.WriteLine(line); } //foreachを抜けたらDispose()が呼ばれてファイルが閉じられる //途中でBreak;してもDispose()されます
PowerShellで試してみましょう。
$lines = [System.IO.File]::ReadLines("d:\test\test.txt") foreach($line in $lines){$line} line1 line2 line3 line4
ここで、Dispose()が呼ばれず、ファイルが開きっぱなしのはずなので、Remove-Item でエラーとなるはず。
PS C:\> Remove-Item D:\test\test.txt -Verbose 詳細: 対象 "D:\test\test.txt" に対して操作 "ファイルの削除" を実行しています。
あれ??普通に出来ちゃいました。どうなっているのでしょう??
ちょっと予想が外れてしまいましたが、気を取り直してMicrosoft Reference Sourceでソースコードを見てみます。
[C#] internal class ReadLinesIterator : Iterator<string> { //.... public override bool MoveNext() { if (this._reader != null) { this.current = _reader.ReadLine(); if (this.current != null) return true; // To maintain 4.0 behavior we Dispose // after reading to the end of the reader. Dispose(); } return false; } }
File.ReadLines() が実際に返しているオブジェクトはReadLinesIterator というクラスなのですが、
そのMoveNext()メソッドを見てみると、自らDispose()を呼んでいるじゃないですか。
じゃあ、問題ない?
でも待ってください。これだけでは途中でBreakした場合にDispose()されないのでは?
先ほどのコードにBreakを入れて実行してみましょう。
PS C:\> $lines = [System.IO.File]::ReadLines("d:\test\test.txt") PS C:\> foreach($line in $lines){$line; break} line1 PS C:\> Remove-Item D:\test\test.txt -Verbose 詳細: 対象 "D:\test\test.txt" に対して操作 "ファイルの削除" を実行しています。 Remove-Item : 項目 D:\test\test.txt を削除できません: 別のプロセスで使用されているため、プロセスはファイル 'D:\test\test.txt' にアクセスできません。 発生場所 行:1 文字:1 + Remove-Item D:\test\test.txt -Verbose + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : WriteError: (D:\test\test.txt:FileInfo) [Remove-Item], IOException + FullyQualifiedErrorId : RemoveFileSystemItemIOError,Microsoft.PowerShell.Commands.RemoveItemCommand
今度は予想通りの結果になりました。ファイルが開いたままです。
まとめ
ジェネリック版のIEnumerable<T>
を列挙する場合は、foreach に任せずに、GetEnumerator()- MoveNext()- Dispose()の一連の処理を自前で書いた方が良さそうです。
※ ただ、これを裏付けるような他の記事、ドキュメント等を見つけることが出来ず、いまいち確信が持てません。
有識者の方々のご意見を伺いたいところです。