- 投稿日:2019-12-05T23:08:32+09:00
C#(csc.exe) - トレイアイコンをクリックするとToast通知を出すサンプル
サンプルアプリの仕様概要
Windows10で動作します。ただしvisuals studio等でwindows sdk入れてないとwindows.winmdファイルがなくてコンパイルできないっぽい
- アイコンを左クリックで通知が出ます。(連打すると通知が延々と何度も出るので注意)
- アイコンを右クリック→Exitで終了できます。
ソースコード
ToastNotificationManager.CreateToastNotifier("Microsoft.Windows.Computer");
のところはかなり強引です。1using System; using System.Drawing; using System.IO; using System.Runtime.InteropServices; using System.Text; using System.Windows.Forms; using Windows.UI.Notifications; public static class IconUtil { static class NativeMethods { [DllImport("user32.dll", CharSet = CharSet.Auto)] public extern static bool DestroyIcon(IntPtr handle); } static readonly string[] iconDot = new string[]{ "................", ".###.###..##.###", "..#..#...#....#.", "..#..#...#....#.", "..#..#...#....#.", "..#..#...#....#.", "..#..#...#....#.", "..#..###..#...#.", "..#..#.....#..#.", "..#..#.....#..#.", "..#..#.....#..#.", "..#..#.....#..#.", "..#..#.....#..#.", "..#..#.....#..#.", "..#..###.##...#.", "................", }; public static Icon MakeDefaultIcon() { using ( Bitmap bmp = new Bitmap(16,16) ) { using ( Graphics g = Graphics.FromImage(bmp) ) { g.Clear(Color.Blue); } for(int y=0;y<16;y++){ for(int x=0;x<16;x++){ if (iconDot[y][x]=='#') { bmp.SetPixel(x,y,Color.Yellow); } } } IntPtr Hicon = bmp.GetHicon(); return Icon.FromHandle(Hicon); } } public static void DestroyIcon(Icon ico) { NativeMethods.DestroyIcon(ico.Handle); } } class TaskTrayLauncher { NotifyIcon trayIcon; static void ShowSampleToast() { string xmlStr = File.ReadAllText("sample.xml", Encoding.GetEncoding("Shift_JIS")); var content = new Windows.Data.Xml.Dom.XmlDocument(); content.LoadXml(xmlStr); var notifier = ToastNotificationManager.CreateToastNotifier("Microsoft.Windows.Computer"); notifier.Show(new ToastNotification(content)); } TaskTrayLauncher() { trayIcon = new NotifyIcon(); Icon tmpIcon = IconUtil.MakeDefaultIcon(); trayIcon.Icon = tmpIcon; trayIcon.Visible = true; trayIcon.Text = "Launcher"; var menu = new ContextMenuStrip(); var menuItem = new ToolStripMenuItem(); menu.Items.AddRange(new ToolStripMenuItem[]{ new ToolStripMenuItem("E&xit", null, (s,e)=>{Application.Exit();}, "Exit") }); trayIcon.MouseClick += TrayIcon_MouseClick; trayIcon.ContextMenuStrip = menu; } void TrayIcon_MouseClick(object sender, MouseEventArgs e) { if (e.Button == MouseButtons.Left) { ShowSampleToast(); } // 右クリックはcontextmenuを表示させるので、ここでは何もしない } [STAThread] static void Main(string[] args) { new TaskTrayLauncher(); Application.Run(); } }toastのxml
同じフォルダにおいてください。
Shift_JIS2で保存してください。sample.xml<toast activationType='foreground' launch='args'> <visual> <binding template='ToastGeneric'> <text>test</text> <text>testtest</text> </binding> </visual> <audio src='ms-winsoundevent:Notification.SMS' /> </toast>コンパイルバッチ
使い方:
compile.bat ファイル名.cs
compile.batcsc /r:C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Runtime.WindowsRuntime\v4.0_4.0.0.0__b77a5c561934e089\system.runtime.windowsruntime.dll ^ /r:C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Runtime.InteropServices.WindowsRuntime\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Runtime.InteropServices.WindowsRuntime.dll ^ /r:C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Runtime\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Runtime.dll ^ "/r:C:\Program Files (x86)\Windows Kits\8.1\References\CommonConfiguration\Neutral\Annotated\Windows.winmd" %*参考記事(参考サイト)
ボタンの追加とイベント処理
参考記事2にあるように、xmlを修正すればToast通知上にボタンを追加することは簡単にできるのですが、
参考記事1で言及されている通り、イベントを受け取るのは困難で、特に、
https://docs.microsoft.com/ja-jp/windows/uwp/design/shell/tiles-and-notifications/send-local-toast-desktop#step-4-implement-the-activator
にあるGUIDの登録が必要になるっぽいです。
詳しくは参考記事1のコードの説明を参照ください。 ↩
UTF-8を使うべきだと思いますが、PC環境の私的な事情でShift_JISにしています・・ ↩
- 投稿日:2019-12-05T20:10:23+09:00
ClosedXMLを使用してExcelファイル出力
初心者です。今、ClosedXMLを使用してExcelファイルの表の作成をしています。
ピボットテーブルでは作成してみたので今回は別のやり方でExcelファイルを作っています。表の出力がうまくいきません。
重複した名前を1つだけ表示し金額も適切なところに表示されるようにしたい。
今後、罫線、名前の下に「合計」と表示、年月の隣に「合計」と表示、行合計、列合計を表示したいのですが、データを追加する予定なので最終の行と列が分からない状態の設定方法を知りたいです。※出力されたExcelファイル
名前 コード 2019年4月 2019年5月 2019年6月 2019年7月 2020年4月
Croissant 1 150
Croissant 1 200
Doughnut 2 250
Bearclaw 3 134
Danish 4 394
Scone 5 135
Croissant 1 250
Doughnut 2 225
Bearclaw 3 184
Danish 4 190
Scone 5 122
Croissant 1 134
Doughnut 2 210
Bearclaw 3 124
Danish 4 221
Scone 5 243
test 6 777
test 6 250ソース
public class Pastry
{
public Pastry(string code, string name, int amount, string yearMonth)
{
Code = code;
Name = name;
NumberOfOrders = amount;
YearMonth = yearMonth;
}public string Code { get; set; }
public string Name { get; set; }
public int NumberOfOrders { get; set; }
public string YearMonth { get; set; }
}pastries = new List
{
new Pastry("1","Croissant", 150, "2019年4月"),
new Pastry("1","Croissant", 200, "2019年4月"),
new Pastry("1","Croissant", 250, "2019年5月"),
new Pastry("1","Croissant", 134, "2019年6月"),
new Pastry("2","Doughnut", 250, "2019年4月"),
new Pastry("2","Doughnut", 225, "2019年5月"),
new Pastry("2","Doughnut", 210, "2019年6月"),
new Pastry("3","Bearclaw", 134, "2019年4月"),
new Pastry("3","Bearclaw", 184, "2019年5月"),
new Pastry("3","Bearclaw", 124, "2019年6月"),
new Pastry("4","Danish", 394, "2019年4月"),
new Pastry("4","Danish", 190, "2019年5月"),
new Pastry("4","Danish", 221, "2019年6月"),
new Pastry("5","Scone", 135, "2019年4月"),
new Pastry("5","Scone", 122, "2019年5月"),
new Pastry("5","Scone", 243, "2019年6月"),
new Pastry("6","test", 777, "2019年7月"),
new Pastry("6", "test", 250, "2020年4月")
};var workbook = new XLWorkbook();
workbook.Style.Font.FontName = "游ゴシック";
var aggregateSheet = workbook.Worksheets.Add("集計");
//ヘッダ出力
aggregateSheet.Cell("A2").Value = "名前";
aggregateSheet.Cell("B2").Value = "コード";var test = pastries.GroupBy(x => x.YearMonth);
int rowIndex = 3;
int collumnIndex = 3;foreach (var group in test)
{
aggregateSheet.Cell(2, collumnIndex).Value = group.Key;
aggregateSheet.Cell(2, collumnIndex).Style.NumberFormat.SetFormat("yyyy年M月");
collumnIndex++;foreach (var item in group)
{
aggregateSheet.Cell(rowIndex, "A").Value = item.Name;
aggregateSheet.Cell(rowIndex, "B").Value = item.Code;
aggregateSheet.Cell(rowIndex, collumnIndex).Value = item.NumberOfOrders;
rowIndex++;
}
}
- 投稿日:2019-12-05T19:43:41+09:00
「MonKey - Productivity Commands」でパラメータを渡す
PONOS Advent Calendar 2019の6日目の記事です。
昨日は私の「MonKey - Productivity Commands」のカスタムコマンドでさらにUnity上の作業を効率化するでした。
はじめに
MonKey - Productivity Commands - Asset Store本記事はPONOS Advent Calendar 2019の以下の記事の続編となります。
- 「MonKey - Productivity Commands」のコマンド操作でUnity開発を効率化する
- 「MonKey - Productivity Commands」のカスタムコマンドでさらにUnity上の作業を効率化する
今回はコマンドにパラメータを渡して実行する方法について紹介します。
なお、
- Unity 2019.2.12f1
- MacOS 10.14.6
の環境で動作確認しています。
コマンドのパラメータを指定する
コマンドの実行時にパラメータを追加するためには
MonKey.Command
属性の付与されたstaticメソッドの引数に対してMonKey.CommandParameter
属性を付与します。
MonKey.CommandParameter
属性の第一引数にはパラメータ表示時の説明文を設定してください。using MonKey; public static class SampleCommands { [Command("Sample Parameter Command", QuickName = "SPC", Help = "好きな数字を選んでコンソールへ出力する。" )] public static void SampleParameterCommand( [CommandParameter("好きな数字を選んでください。")] // 「value」をコマンドのパラメータ化する。 int value ) { Debug.Log($"選んだ数字は\"{value}\"です。"); } }このコマンドをコマンドパレットで開くと以下のような表示になります。
パラメータが設定されたコマンドは、コマンド名の左側に吹き出しアイコンが表示されており、パラメータの有無を判別できます。
このコマンドを選択すると、以下のようなパラメータ入力画面が表示されます。
任意のパラメータ(今回はint型のパラメータなので整数値)を入力して実行します。
コマンドに渡されたパラメータの値を使用して、処理が実行されました。利用可能なコマンドパラメータの型
MonKeyがデフォルトでサポートしているパラメータの型は以下の通りです。
これらの型の引数はMonKey.CommandParameter
属性を付与するだけでコマンドパラメータ化が可能です。
- String
- Int
- Float
- Double
- Byte
- Bool
- Char
- Long
- Short
- Enum
- Vector2
- Vector3
- Vector4
- Quaternion
- Color
- Object
- Component
- GameObject
- LayerMask
- Scene
- Type
- Arrays
なお、
MonKey.Editor.Internal.CommandParameterInterprete
を継承したクラスを用意することで、ここにある以外の型もパラメータとして利用することが出来ます。使用例:Rect型のコマンドパラメータを受け取り、コンソールに矩形情報を出力する
今回は使用例として、デフォルトで定義されていない
Rect
型のコマンドパラメータに対応してみましょう。Rect型を入力するルールを決める
MonKeyのコマンド実行に入力されるパラメータは文字列です。
そのままではRect
型のコマンドパラメータとして渡すことはできないので、入力された文字列を適切にRect
型へパースする処理が必要となります。
Rect
型のオブジェクトを作成するためには「x」「y」「width」「height」の4個の数値が必要ですので、今回はこれらの4個の数値をカンマ(,)区切りで入力してもらうルールにします。このルールに従うとコマンドパラメータに
1, 2, 100, 200
という文字列が入力された場合、
x = 1, y = 2, width = 100, height = 200
というRect
オブジェクトが作成されます。Rect型のコマンドパラメータを持つコマンドを作成する
次に
Rect
型をパラメータに指定したコマンドを用意します。[Command("Rect Parameter Command", QuickName = "RPC", Help = "矩形情報を出力する。" )] public static void RectParameterCommand( [CommandParameter] Rect value ) { Debug.Log($"入力された矩形の情報:{value.ToString()}"); }入力された矩形情報をコンソール出力するだけの、シンプルなコマンドです。
さて、試しにこのコマンドを実行してみますが…
パラメータの入力後に「Error」と表示され、コマンドを実行することができませんでした。
Rect
型はデフォルトでサポートされている型ではなく、「文字列を適切にRect
型へパースする処理」もまだ実装していないので、Rect
型のコマンドパラメータをコマンドへ渡せずにエラーとなってしまっているようです。また、コマンド内の以下の処理についても、valueがnullで渡されてしまうため、
NullReferenceException
が発生していました。Debug.Log($"入力された矩形の情報:{value.ToString()}");Rect型に対応するためのCommandParameterInterpreterを作成する
さて、それでは「文字列を適切に
Rect
型へパースする処理」を実装するために専用のCommandParameterInterpreter
クラスを用意しましょう。今回は
Rect
型に対応するので、RectInterpreter
という名前にします。
スクリプトはEditor
以下に設置してください。// Rectの構造体をコマンドパラメータで使用するためのCommandParameterInterpreter。 class RectInterpreter : MonKey.Editor.Internal.CommandParameterInterpreter { [InitializeOnLoadMethod] static void AddInterpreter() { // CommandParameterInterpreterの定義を追加する。 AddInterpreter(new RectInterpreter()); } RectInterpreter() : base(typeof(Rect)) // 対応する型を指定する。今回はRect。 { } // 入力された文字列を、コマンドパラメータの型にパースする。 public override bool TryParse(string text, out object obj, System.Type subType = null) { // カンマ区切りでx, y, width, heightの4個の数値を指定させる。 var parts = text.Split(','); if (parts.Length == 4) { // それぞれの数値をfloatへ変換し、Rectを作成する。 float[] values = new float[4]; for (int i = 0; i < 4; ++i) { float result; if (float.TryParse(parts[i], out result)) { values[i] = result; } } var rect = new Rect(values[0], values[1], values[2], values[3]); obj = rect; // パースした結果はobjに格納する。 return true; } obj = null; return false; } }では、もう一度実行してみましょう。
入力した文字列から矩形情報を作成し、コンソールに出力することができました!おわりに
コマンドパラメータを指定してコマンドを実行することで、柔軟に処理を実行することができます。
MonKeyの優れている点の一つだと思うので、是非活用していきましょう。明日は@karizumaiさんです!
- 投稿日:2019-12-05T19:27:22+09:00
C#8.0 Default Interface Methods でstructがBoxingされる,されない話
この記事はC# Advent Calendar 2019の15日目です。
初投稿です。
TL;DR
- C#8.0でInterfaceにも実装できるようになった
- しかしstructとの相性が悪い仕様でBoxingされる
- 条件によってはされないこともある?
Default Interface Methods とは
Default Interface Methods (以下DIMs) とは、C#8.0の新機能の一つで、今まで宣言しかできなかったinterfaceでメソッドを実装できるようになりました。JavaのDefault Methodsのようなものです。
https://github.com/dotnet/csharplang/blob/master/proposals/csharp-8.0/default-interface-methods.mdpublic interface IDim { //+1して返すだけの関数 int Inc(int x) => x+1; }これによって何がうれしいかといえば、
- interfaceにmethodまたはpropertyを後から足しても、既存のclass/structがコンパイルエラーにならない
- interfaceで実装することで継承先の実装の手間が省ける
というところがあります。
特に1が重要で、ライブラリなど第三者が利用するようなinterfaceにmethodを追加することが破壊的変更になっているため、なかなか追加しづらいという問題が解決されます。structのgenericでの展開
もともとstructと素の(C#7.3以前の)interfaceはgenericを使うことでstructが展開されるため
- boxingされない
- 脱仮想化(devirtualize)される
- またmethodによってはinlineまでかかる
というパフォーマンス上のメリットがあります。
interface IA { int One(); } struct B : IA { int One() => 1;} int GetOne<TA>(TA a) => a.One(); //というのがあったとすると var b = new B(); GetOne(b); //とつかうとGetOne()内で直接B.One()というメソッドが呼ばれます IA a = new B(); GetOne(a); //とつかうとGetOne()内ではIA.One()から仮想関数として呼ばれるclassでは継承などでどのmethodが使われるか変わる可能性があるため仮想関数テーブルからmethodを呼び出します。そしてそもそもheap上に乗っています。
しかしstructは継承不可のため実装されているinterfaceのメソッドが決まり、またstack上にあり、仮想的に扱おうとするとheapに乗せられる(boxing)ためそれを回避するために展開されます。
(genericではなくinterfaceな変数として宣言するとboxingします。)DIMsでは?
しかしDIMsではstruct自体にmethodの実装がないです。ということで呼び出す関数をinterfaceから持ってこなければいけないのでboxingしなければなりません。
原理的にはstructにないmethodもboxingしないでもってこれますが…interface-methods-vs-structsで結論づいているように言語仕様としてboxingを回避しないことを決め、これに対する変更は破壊的変更になるから今後これを覆すつもりがない、ということになりました。
(上のリンクによると、できるけどコストが割に合わないだろうということで回避しないそうです。)これはなかなか困ったことで、まずboxingされるとそのメソッドでのstruct内部の状態変化(副作用)は無視されます。これは割と混乱するかもしれません。
さらにref structにinterfaceを付けれるようにするためのref interfaceの提案などでもA ref interface cannot define DIMs (alternative: can only define a DIM which is guaranteed inlineable and does not box).
などと今後のパフォーマンス改善系の言語拡張の方向と相性が悪そうです。C#7.2から続く、heapへのallocationをなるべく回避することで高速化する、という言語の流れと逆行してるように感じますが…。
一応struct側で実装があればboxingされないのでdimをただコンパイルエラーをさけるための応急処置と考えれば使えそうです。
またUnityのJobSystem(というよりBurst Compilerがmanaged禁止のため)で使えなさそうですが…
そこらへんはBurstCompilerなりil2cppで頑張ってくれることを願ってます。とりあえずsharplabでどのようなコードになるか見てみましょう。
...あれ?
Boxingされていない?
JITの結果を見る限り、空のstructでのdimは、なぜかboxingされていないように見えます
これはフィールドがない空のstructだから特殊処理されたのだろうと思うのですが…
こういうところの仕様が謎です。これならshape and extension(特にstaticなメソッド周り)などの将来的機能には一応使える?
ただ仕様が分からないので将来的にどうなるか分からないのが不安ですね。
いまのところは「空のstructならboxingされない」ということで使いましょう。まとめ
とりあえず実用的な機能であるが、structに対するパフォーマンス上のリスクがあることを気を付けなければならないと思われます。
警告もとくに出す予定がない(Analyzerで出す気はあるらしい)ので一番怖いことはlibrary側でinterfaceにDIMsを使われ、かつそのmethodを知らないうちにstructで使ってわれているとパフォーマンスが悪化する可能性があることでしょうか。
破壊的変更をせずにinterfaceを変えれるのはlibrary作者などにとっては利点なのでclassだけがターゲットだとかなら使いましょう。詳しい方がいればコメント等で訂正、補足等してくれると幸いです。
- 投稿日:2019-12-05T14:22:09+09:00
LINQについて
LINQの考え方
- 自分の為に思い出すため記載
- LINQはソースを簡略化し、わかりやすく書くことができる。
var list = new List<string>() { "abc", "abbc", "babcc" }; var list2 = list.Select(x => x + "1"); var list3 = list.Where(str => str == "b"); var list4 = list.Where(str => str.Contains("abc")); var lista = new List<int>() { 10, 20, 30, 100 }; var lista1 = lista.Where(x => x >= 15); var lista2 = lista.Select(x => x + 1); var lista3 = lista.Where(x => x >= 15).Select(x => x + 1); var lista4 = lista1.Select(x => x + 1);
- このようにselect,whereなどを使い、簡略化することができる。
- ちなみにcontainsは、右辺と左辺が同じであれば、bool値で返すことができる。 (boolとはtrueとfalseで表されることが多い)
- selectは、処理したものをそのまま射影する。
- whereは、処理し、当てはまるものが返される。 (つまり、list3には、すべて入り、list4には、abcとbabccが該当しているので入ります。)
また勉強したことを記入し、refineしていく。
- 投稿日:2019-12-05T14:12:20+09:00
迷路ゲームにランキング機能をつけるお話
N高アドカレ16日目の記事ですー
データベース一切わかんない人がオンラインランキングの実装を頑張った話になります。はじめに
軽く自己紹介
N高の江坂プロクラの高三の受験生です。
大学に無事AOで合格して時間ができたのでアドカレ参加。
UnityとBlenderでゲーム作ったり色々してます。
Webとサーバーサイドはほとんどわかんないです…ゲームの説明
迷路ゲーム自体は自作キャラがモデリングできたからそれを歩かせてゲームにしたいという思いから作ったもので、シンプルなものです。
敵キャラのゴーストを避けながらゴールを目指すゲームです。
このゲームにオンラインランキング(タイムアタックのハイスコア)をつけるときの話です。どうやってスコアを保存するか
はじめにどうやってクリアタイムを保存させようかと考えました。
ゲームに記憶させるのか、データをサーバーに保存するか…と色々考えていた時に、こちらのUnity + NCMBでスコアランキングを実装するという記事を見つけました。この記事の通りにやってみることにしました。記事を参考に作業
NCMB側の設定
Googleでログインしました。
そしてクラスをHighScoreという名前で作成。
Unity側の設定
UnityにNCMBのパッケージを導入。
NCMBSettingsという名前でオブジェクトを作ってNCMBSettingsというスクリプトをアタッチしました。
アプリケーションキーとクライアントキーをNCMBSettingsのインスペクタにコピペ。
ここまでは事前準備です、次にコードを書いていきます。
クリアタイムの保存
データを保存するためのオブジェクト(箱)を作ってその中にクリアタイムを保存します。
NCMBObject HighScore = new NCMBObject("HighScore");これでデータを入れるオブジェクトを作ります。
HighScore["Score"] = script.totalTime;HighScoreに値を代入。
私は別の時間制御をしているスクリプトのtotalTimeを参照させるようにしています。
HighScore.SaveAsync();そしてNCMB側に保存させます。
私が実際に書いたコードの一部を参考に置いておきます。
void OnTriggerEnter(Collider collision) { if (collision.gameObject.tag == "Player") { gameclear.SetActive(true); TimerUI.SetActive(false); NCMBObject HighScore = new NCMBObject("HighScore"); HighScore["Score"] = script.totalTime; HighScore.SaveAsync(); } }プレイヤーとゴールの判定オブジェクトが衝突判定を起こした時に、gameclearUIの表示、制限時間UIの非表示と同時にクリア時の時間をデータベースに送っています。
クリアタイムの取得
クリアタイムをデータベースから取得して表示させます。
NCMBQuery<NCMBObject> query = new NCMBQuery<NCMBObject>("HighScore");取得したデータはNCMBQueryに格納されるのでこれを先に作ります。
query.OrderByAscending("Score"); query.Limit = 5;並び替え&5つだけ取得させる設定
取得設定ができました。
そして実際に取得させようと思い、
query.FindAsync ((List<NCMBObject> objList ,NCMBException e) => { if (e == null) { objList[0]["Score"] = score; objList[0].SaveAsync(); } });このコードを参考にしてコードを書いたところ、エラーでデータの取得ができませんでした。
Scoreデータを関数に代入する方法がわからず、しばらく試行錯誤しました。query.FindAsync((List<NCMBObject> objList, NCMBException e) =>{ if (e == null) { HighScore_1 = System.Convert.ToSingle(objList[0]["Score"]); HighScore_2 = System.Convert.ToSingle(objList[1]["Score"]); HighScore_3 = System.Convert.ToSingle(objList[2]["Score"]); HighScore_4 = System.Convert.ToSingle(objList[3]["Score"]); HighScore_5 = System.Convert.ToSingle(objList[4]["Score"]); }結果このコードでHighScoreにListのScoreを代入することができました…
どうやらSaveAsyncが邪魔してたみたいでした…
ランキング表示
取得して関数に代入した値をランキングのUIとして表示させます。
参考に私が書いたコード置いときます。
using UnityEngine; using UnityEngine.UI; public class Ranking_1 : MonoBehaviour { private Text Ranking1; public GameObject Ranking; ranking script; public GameObject Next; Next script2; int display; float totalTime; int minute; float seconds; void Start() { script = Ranking.GetComponent<ranking>();//HighScoreを代入した関数を参照させる設定 script2 = Next.GetComponent<Next>();//HighScoreを表示させる関数を参照させる設定 Ranking1 = GetComponentInChildren<Text>(); } void Update() { totalTime = script.HighScore_1;//totalTimeにHighScore_1の値を入れる display = script2.rankingdisplay;//HighScoreを表示させるための関数 if (display == 1) { minute = (int)totalTime / 60; seconds = totalTime - minute * 60; Ranking1.text = minute.ToString("00") + ":" + ((int)seconds).ToString("00"); } } }HighScoreの値とHighScoreを表示させる関数の値を参照させる設定をして、HighScoreを表示させる関数が1の時にHighScoreをUIに表示させるようにしています。
ゲームクリア画面からランキング画面の画面推移時に関数を0から1にして表示させることができます。
(もうちょっといいコードがかけそうな気がしたけど、私のコーディング力ではこれぐらいしかかけなかった…)
ゲームクリア画面
Nextボタンを押すと
ランキング表示させることができた!追記
ゲームにランキングが追加できたので、しばらくテストプレイしていると、なぜか大量のAPIリクエストを送っていた…
この日と次の日はすごい数送ってました…その数約5万件…
今は改善して減りましたが…
大量のAPIリクエストの原因は、ゴール後にUpdate処理で1フレームに1回、データベースのデータを取得していたからでした…void Update() { int GameclearJudgment = script.Gameclearfunction; if (GameclearJudgment == 1) { NCMBQuery<NCMBObject> query = new NCMBQuery<NCMBObject>("HighScore"); query.OrderByAscending("Score"); query.Limit = 5; query.FindAsync((List<NCMBObject> objList, NCMBException e) =>{ if (e == null) { HighScore_1 = System.Convert.ToSingle(objList[0]["Score"]); HighScore_2 = System.Convert.ToSingle(objList[1]["Score"]); HighScore_3 = System.Convert.ToSingle(objList[2]["Score"]); HighScore_4 = System.Convert.ToSingle(objList[3]["Score"]); HighScore_5 = System.Convert.ToSingle(objList[4]["Score"]); } }); } }問題のコード
ゲームクリアした時に値が1になる関数を参照して、ゲームクリアしていたらデータベースの値を取得するようにする処理をUpdateに書いてしまった。void Update() { int RankingJudgment = script.Rankingfunction; if (RankingJudgment == 1) { NCMBQuery<NCMBObject> query = new NCMBQuery<NCMBObject>("HighScore"); query.OrderByAscending("Score"); query.Limit = 5; query.FindAsync((List<NCMBObject> objList, NCMBException e) =>{ if (e == null) { HighScore_1 = System.Convert.ToSingle(objList[0]["Score"]); HighScore_2 = System.Convert.ToSingle(objList[1]["Score"]); HighScore_3 = System.Convert.ToSingle(objList[2]["Score"]); HighScore_4 = System.Convert.ToSingle(objList[3]["Score"]); HighScore_5 = System.Convert.ToSingle(objList[4]["Score"]); } script.RankingJudgmentReset(); }); } }データベースのデータを取得した後に参照元の関数を0にリセットするように改善。
無事にリクエスト数が減りました。まとめ
データベースを触るのは初めてだったのでわからないことも多かった…
NCNBは細かい設定が必要ないのでわかりやすくて触りやすかった
次はランキングのスコアと同時にuser nameを表示できるようにしたい…
WebGL書き出ししてWeb上で遊べるようにしようか検討中。
- 投稿日:2019-12-05T13:06:28+09:00
【Xamarin】SecKeyChain備忘録(基本的なパスワードの取扱)
SecKeyChain
周り、触れば触るほどややこしいので分かったこと取り敢えずまとめ。
- アプリケーションパスワード
- インターネットパスワード
の形で、ユーザーが入力したパスワードを端末に保存できる。
※多分他にもあるっぽいですが今の所この2つしか活用出来てません……しかも異なる仕様だと思ってた部分が同じだったので(後述)違いもあまりわからない。。
パスワード保存する
何はともあれパスワードを保存する
SecKeyChain.AddGenericPassword(serviceName, accountName, password); SecKeyChain.AddInternetPassword(serverName, accountName, password);
AddGenericPassword
はアプリケーションパスワードとして、AddInternetPassword
はインターネットパスワードとしてキーチェーンアクセスに保存する。
serviceName
,serverName
,accountName
はいずれもstring
。
serviceName
とserverName
の違いこそあれど、結局はキーチェーンアクセスに表示される名前なのであまり差異はない(と思う)。ちなみにブラウザなどで登録したパスワードについてはこの
serverName
にWebサイトのホスト名が入るので、アプリケーションの場合はそのままアプリ名をserviceName
に入れれば良い。
password
のみbyte[]
となるので、事前にbyte配列への変換が必要となる。var bytePassword = Encoding.UTF8.GetBytes(password);パスワードを取り出す
アプリを再起動した時に前回の値が入力されている……みたいなやつ作るときのための、パスワードの取り出し方。
// 何かしらのアプリケーションパスワードを取り出す想定 // パスワードを取り出すためのクエリ var passwordQuery = new SecRecord(SecKind.GenericPassword) { Service = serviceName, Account = accountName };
SecRecord
オブジェクトを作成し、キーチェーンアクセスからパスワードを取り出すためのクエリを作成する。
引数にはパスワードの種類を示すSecKind
列挙型の値を代入し、レコードを特定するための情報をブロック内に記述する。ややこしいのはこの
SecRecord
オブジェクト自体はパスワードそのものではないということ。
取り出すにはこの発行したクエリを元にQueryAsData
メソッドを使う。// QueryAsData()メソッドはNSData型を返すので文字列に変換する NSData passwordData = SecKeyChain.QueryAsData(passwordQuery); string password = passwordData.ToString();
QueryAsData()
は引数として受け取ったSecRecord
が持つ情報に一致したキーチェーンアクセスのレコードをNSData
型で一つだけ返してくれる。最大数を指定することで一致するものを複数返してくれるオーバーロードも存在するっぽい。保存するときには
byte[]
型に変換したが、取り出す場合はこれで平文のパスワードが取り出せる。パスワードを更新する
ユーザーがパスワードを書き換えた際などに、新規追加時と同様に
AddGenericPassword()
を使うと上手くいかない。これはキーチェーンアクセスの仕様上重複するレコードは作れないから。
→ 恥ずかしながらこれに気付くのに結構時間をかけてしまいました。。Server
が同じでもAccount
が異なるレコードは作れるのでInternetPassword
だけの話かと何となく思っていたら、GenericPassword
も同様だったので、余計にこの2つの違いがわからなくなる。。因みに厄介なことに(?)重複するレコードを作ろうとしてもエラーなどは出ず、只々キーチェーンアクセスに更新がかからないだけとなる。
ただ確かめる術はあるようで、キーチェーンアクセス操作系のメソッドが返してくれるHTTPステータスコードのようなSecStatusCode
という列挙型オブジェクトを参照する。var bytePassword = Encoding.UTF8.GetBytes("password"); var byteNewPassword = Encoding.UTF8.GetBytes("newPassword"); // 返り値のSecStatusCodeを参照するとレコードの追加がうまくいったかどうかが分かる SecStatusCode code; // 成功した場合はSuccessが返る code = SecKeyChain.AddPassword(serviceName, accountName, bytePassword); Debug.WriteLine(code) //=> Success // 失敗時はそれに応じたコード code = SecKeyChain.AddPassword(serviceName, accountName, byteNewPassword); Debug.WriteLine(code) //=> DuplicateItemこの
SecStatusCode
、めちゃくちゃ沢山種類があるので(ドキュメント参照)Success
とそれ以外で分岐させるのが無難……かもしれない。本題に戻ると、ユーザーの入力などによって特定のレコードのパスワードだけを更新したい、という場合は
SecKeyChain.Update()
メソッドを使う。
→ これを想定する場合はDuplicateItem
による分岐を考えても良い。と思う。
Update
メソッドは2つのSecRecord
を引数として受け取る。これらはそれぞれ既存のレコードを探すためのクエリ、新しい値を表すクエリとして使われる。// 対象のレコードを特定するためのクエリ var passwordQuery = new SecRecord(SecKind.GenericPassword) { Service = serviceName, Account = accountName }; // 新しいパスワードを持つクエリ var newPasswordQuery = new SecRecord(SecKind.GenericPassword) { ValueData = NSData.FromArray(byteNewPassword) }; // Update()メソッドもSecStatusCodeを返してくれる SecStatusCode code = SecKeyChain.Update(passwordQuery, newPasswordQuery); Debug.WriteLine(code) //=> SuccessこうしてみるとSQLに似通っている部分がありますが、クエリをオブジェクトとして作るって所が自分には少しややこしかった。。(しかも
SecRecord
って名前なのが余計に……)因みに
SecRecord
オブジェクトにおいて実際のパスワードの情報が代入されるのは上記でも示している通りValueData
というNSData
型のプロパティ。
パスワードはバイト配列として保存されているので、これまで通りbyte[]
型に変換したものを用意しNSData.FromArray()
でNSDataへと変換する。補足: 雑に更新する
var serviceName = "hogeService"; var accountName = "hogeAccount"; SecStatusCode code; var bytePassword = Encoding.UTF8.GetBytes("password"); code = SecKeyChain.AddGenericPassword(serviceName, accountName, bytePassword); Debug.WriteLine(code) //=> Success var byteNewPassword = Encoding.UTF8.GetBytes("newPassword"); var passwordQuery = new SecRecord(SecKind.GenericPassword) { Service = serviceName, Account = accountName }; code = SecKeyChain.Remove(passwordQuery); Debug.WriteLine(code) //=> Success code = SecKeyChain.AddGenericPassword(serviceName, accountName, byteNewPassword); Debug.WriteLine(code) //=> Success
SecKeyChain.Remove()
メソッドを用いて、一回消して追加し直すという形でも更新は可能です。
というかキーチェーンアクセスの様子を見る限りUpdate()
でも内部的には一回消して追加し直す……をやっているような気がするので、強ち雑とも言えないかもしれない。。そもそも検索用のクエリを立てなきゃいけないのでコードの量も大して変わらないという。
参考(主に公式ドキュメント)
https://docs.microsoft.com/en-us/dotnet/api/security.seckeychain?view=xamarin-ios-sdk-12
https://docs.microsoft.com/en-us/dotnet/api/security.seckeychain.queryasdata?view=xamarin-ios-sdk-12#Security_SecKeyChain_QueryAsData_Security_SecRecord_
https://docs.microsoft.com/en-us/dotnet/api/security.secstatuscode?view=xamarin-ios-sdk-12
- 投稿日:2019-12-05T12:03:32+09:00
corertのrd.xmlについて
始めに
dotnet coreでAOTでネイティブシングルバイナリを実現するための一つのツールとして、corertというものがある。
成熟度合でいうと絶賛開発中で、仕様もこれからいくらでも変わる可能性があるので、プロダクションで使えるかと言われるとお薦めはしないが、自分プロジェクトのちょっとしたツールでは色々と便利なので使っている。この中でリフレクション関連の処理を行うための補助設定ファイルとしてrd.xmlというファイルがあるが、それについて現時点でどうすれば良いかという文書は無かったので、ここに書いておく。
なお、corertは現在開発中のライブラリであり、仕様変更も予告なく行うため、記事執筆時の最新コミットを参照して解説する。
corertにおけるリフレクション
corertはコンパイル時にネイティブバイナリを作成する関係上、リフレクション関連の処理に大きな制限がかかる。
しかし、昨今のC#の状況をみると、リフレクション関連の処理を全てNGとするのは非現実的(enum関連とか)なため、API呼び出しやフロー解析を用いて、コンパイル時にメタ情報を静的に作成して使用することで、一定の範囲でリフレクションAPIを利用できるようにしている。しかし、自動的な解析だけではどうしても限界が出てくるため、予めメタ情報を保持しておくものを指定しておくファイルとして、rd(Runtime Directive).xmlが使用される。
この辺りの、API呼び出し+フロー解析+補助設定ファイルというのはGraalVMでも似たようなことをしており、AOTでは結局同じような手法に行きつくという事だと思う。
rd.xmlの出自
rd.xml自体はcorertのオリジナルではなく、.NET Nativeからのもの。
しかし、corertで使えるのはそのサブセットであり、全ての記法をサポートしているわけではない。いつ書くべきか
Type.GetType()
直呼び出し程度ならば、コンパイルの段階で自動検出してくれるので、記述の必要は多くの場合は無い。
しかし、自動検出されなかった場合、ネイティブバイナリ実行時にMissingMetadataException
やTypeLoadException
が発生する。
この時にrd.xmlを書くことを検討する。書き方
Directives
を頂点として、その下にApplication
かLibrary
要素を追加する
- 現時点ではどちらでも入れてOK
- その配下に
Assembly
要素を追加し、属性としてそのアセンブリ名(System.Reflection.AssemblyName.Name
)を指定- 配下に
Type
要素を追加し、属性Name
に名前空間を含めたタイプ名と、属性Dynamic
に`Required All"を指定する
- corertでは今の所
Dynamic
にはRequired All
以外の記述は認められていない- メソッド指定のみ必要な場合は、
Dynamic
属性は付けなくてもOK
- どのような効用があるかは今の所不明なので、とりあえずつけるという感じで良いかもしれない
- ジェネリッククラスの場合、
TypeName`1[[Type1,AssemblyName]]
のような記述になる- インナークラスの場合、
OuterClass+InnerClass
のような記述になる- メソッドの指定をしたい場合は、
Type
配下にMethod
要素を追加する
- ジェネリックメソッドの場合は、更に配下に
GenericArgument
を追加する<Directives xmlns="http://schemas.microsoft.com/netfx/2013/01/metadata"> <Application> <Assembly Name="My.Assembly.Name"> <Type Name="Full.Typename" Dynamic="Required All" /> <Type Name="Full.Typename2"> <Method Name="Method1"/> <Method Name="Method2> <GenericArgument Name="System.Int32"/> </Method> </Type> </Assembly> </Application> </Directives>プロジェクト設定
rd.xmlはItemGroupに
RdXmlFile
を追加することで読み込んでくれる。
rd.xmlが不正なフォーマットだったり、クラス指定等が間違っていると、コンパイル時に例外として捕捉される。終りに
簡単ではあるが、corertのrd.xmlについて書いた。
corertは、ネイティブバイナリ実行時にしか出ないエラーがあったり、何も言わずにSEGVで落ちる場合等があるので、お世辞にも現時点では使いやすいとは言えないが、PublishSingleFileよりもコンパクトにまとまったり(1-10MB程度)、初回起動が非常に早かったり(ほぼCのネイティブバイナリと変わらない)するので、将来的に使いやすいものになると良いと思う。
- 投稿日:2019-12-05T11:52:22+09:00
JSONパース最適化
現在使ってるJsonライブラリの処理を早く出来ないかどうか検討しました。
目的
・現在サーバーAPIのレスポンスをパースするJSONライブラリが遅いと感じてるのでなんか出来ないかどうか検討したい
・もっと早いライブラリがあると思うけどライブラリを差し替えるとけっこうな作業になるので今のままでやってみたい
・本来であればJSONを無くしてgRPCみたいなものを使いたいけどこれも差し替える作業が大きいので現在のものを改善できれば良いstringアロケーションを少なくしましょう
最初にクラスを開いてみるとstringのアロケーションが多いなーと思いました。stringがコンストラクタに渡されてパースしながらstring.substring()を利用してガンガン再帰呼び出しがされてしまいます。
list.Add(new JSONObject(str.Substring(start, end)));jsonが深くなるほどこれの呼ぶ回数が多くなるのでもともとのstringよりメモリが結構膨らむよね。パースが遅いだけじゃなくてGCも多く走らせてしまいます。
もし次のオブジェクトがstringじゃなければstring化しなくていいのではないか?
1個ずつのcharをチェックして次のデータが true、false、null、数字などだったらsubstringのコールを飛ばしてみよう。擬似コード:
private static readonly char[] ms_digits = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '-', 'e', 'E' }; private JSONObject parseSubString(string str, int start, int length){ //true, false, nullなどのタイプをチェック if(length == 4 && (str[start]=='t' || str[start]=='T')){ //true return new JSONObject(true); } if(length == 5 && (str[start]=='f' || str[start+1]=='F')){ //false return new JSONObject(false); } if(length == 4 && (str[start]=='n' || str[start+1]=='N')){ //null return new JSONObject(Type.NULL); } bool isNumber = false; for(int i=0;i<length;i++){ //ms_digitsに入ったらindex返す int numIdx = GetNumberFromChar(nextChar); //数字じゃないならやめる if(numIdx < 0){ isNumber = false; break;} // hogehoge //1個ずつの数字を追加していく //num = (num*10)+numIdx 的な感じ // マイナス、少点数などの対応も忘れずに } //数字を返す if(isNumber) return new JSONObject(number); //結局stringかオブジェクトだったらsubstringしちゃうかー return new JSONObject(str.Substring(start, length)); }これを実装したらsubstringを呼ぶ回数が減るはずです!
ちなみに同じロジックを使ってStringBuilderでも拡張できます!
Gavin Pughさんが作ってくれた http://www.gavpugh.com/source/StringBuilderExtNumeric.cs を参考してくださいさらに減らせるでしょうか
上記だとある程度stringアロケーション減らせたけどまだjsonオブジェクトや配列などがsubstringに入ってくる。最終的なstringオブジェクトのみアロケートするように出来るでしょうか?
現在のライブラリだとstringが受け取るconstructorしかないけど、これMemoryStreamだったらどうかな?
もともとのpublic JSONObject(string str)コンストラクタじゃなくて新しく
public JSONObject(Stream stream)を作ってみました。もともとのパース処理を真似しながら
stream.Read(buffer,0,1)1個ずつのcharをチェックしながらjsonパースしていく。 こうするとアロケーションが発生する時はjsonの中のstringタイプのオブジェクトがある時だけになります。
では、このMemoryStreamがどこから来るでしょうか? stringをMemoryStreamに変換するだけだと意味がなくなります。HTTP通信周りのシステムを見てみると内部にMemoryStreamを使ってます! 今までは通信が終わったらこのMemoryStreamがbyte[]に変換されてたけどそれを飛ばして直接にJsonに渡したらさらに快適。けっこう大きい通信もあるのでこのbyte[]変換が必要ないならGCなども改善されます。
結果
こっちのテスト環境で試してみたけどもともと1.25秒かかったものが0.25秒になりました! 5倍じゃん!
やっぱり無駄なものを削りましょう
- 投稿日:2019-12-05T04:09:46+09:00
「MonKey - Productivity Commands」のカスタムコマンドでさらにUnity上の作業を効率化する
PONOS Advent Calendar 2019の5日目の記事です。
昨日は@nimitsuさんのGameLift RealTimeServerで遊んでみよう for Unity(AWS設定編)でした。
はじめに
本記事はPONOS Advent Calendar 2019の2日目の記事である「MonKey - Productivity Commands」のコマンド操作でUnity開発を効率化するの続編となります。
MonKey - Productivity Commands - Asset Store前回はMonKeyを使用してどんな事ができるのか、導入について書かせてもらいました。
今回は少し発展して、カスタムコマンドの作り方についてまとめたいと思います。MonKeyには予め130超もの豊富なコマンドが用意されており、様々な処理をコマンドから実行できますが、それらのコマンドだけではそのプロジェクト特有の処理まではサポートできません。
カスタムコマンドを利用することにより、プロジェクト独自の処理についてもコマンド化し操作時間を短縮することができるので、MonKeyを有効利用したいと思っている方は参考にしてみてください。なお、
- Unity 2019.2.12f1
- MacOS 10.14.6
の環境で動作確認しています。
カスタムコマンド
MonKey.Command
属性をstaticメソッドに付与することで、そのメソッドをコマンド化することができます。
なお、スクリプトはEditor
フォルダ以下に存在している必要がありますのでご注意ください。using MonKey; public static class SampleCommands { [Command("Sample Command 01")] // 引数にMonKey上のコマンド名を指定する static void SampleCommand01() { Debug.Log($"コマンド\"SampleCommand01\"を実行したログです。"); } }
MonKeyのコマンドパレット上に表示されました!
コマンドを実行すると、メソッド内に記述した処理が実行されています。
このように、メソッドをMonKeyのコマンド化するのは非常に簡単です。使用例:選択中のGameObjectの階層パスをコンソールに出力する
さて、ここからはより実用的なカスタムコマンドを作成してみます。
今回作成するのは「選択中のGameObjectの階層パスをコンソールに出力する」コマンドです。
例えば、上のようなHierarchyで実行した場合には「Parent/SecondSon/Grandchild/Great-grandson」とコンソールにログ出力する挙動となります。『SecondSon』の下にある『Grand-grandson』のことを他の人に伝えたいとき、Hierarchyの階層のパスを提示できれば、『EldestSon』の下にいるGrand-grandsonを誤って参照しまうことを防止できます。
コマンドの基本情報を設定する
前項で作成した
Sample Command 01
ですが、他のコマンドと異なり「ヘルプ」や「短縮されたコマンド名」が表示されておらず、実際に使うには不便でした。
今回はそれらコマンドに関する基本情報をちゃんと設定してみましょう。
MonKey.Command
属性には上記の「ヘルプ」や「短縮されたコマンド名」のようなコマンドの基本情報を設定するためのプロパティが用意されています。MonKey.Command属性のプロパティ(抜粋)
プロパティ 用途 Name コマンド名。コマンドの検索に用いられる。 QuickName 短縮されたコマンド名。
3文字以内に収めることを推奨されている。Help コマンド名の下に表示されるヘルプ。
1行より長くなるとフォーマットが完全ではない可能性がある。実際にこれらのプロパティを使用して「ヘルプ」と「短縮されたコマンド名」が設定された
Sample Command 02
を作成します。[Command( "Sample Command 02", QuickName = "SC2", Help = "選択中のGameObjectの階層パスをコンソールに出力する" )] static void SampleCommand02() { }
設定した「ヘルプ」と「短縮されたコマンド名」がコマンドパレット上に表示されていることがわかります。
また、短縮されたコマンド名である「sc2」を入力しただけでSmaple Command 02
が候補として表示されます。コマンドから選択されているGameObjectを参照する
次に、実際に選択中のGameObjectを参照してコンソールに出力する、というコマンドの処理部分を実装してみましょう。
現在選択されているGameObjectを参照するためには、通常エディタ拡張で使用しているUnityEditor.Selection
ではなく、MonKeyから提供されているMonKey.Editor.MonkeyEditorUtils.OrderedSelectedGameObjects
を使用します。(なお、選択されたGameObjectではなく、選択されたTransformを取得することのできる
MonKey.Editor.MonkeyEditorUtils.OrderedSelectedTransform
も用意されています)
IEnumerable<GameObject>
で返ってくるので、foreachで列挙し処理していきます。選択中のGameObjectを取得し、そのパスを出力するコードは以下になります。
using MonKey.Editor; ... // OrderedSelectedGameObjectsから選択中のGameObjectを列挙できる。 foreach (var selectedGameObject in MonkeyEditorUtils.OrderedSelectedGameObjects) { // 親のGameObjectの名前を手前に連結していく。 var pathBuilder = new System.Text.StringBuilder(selectedGameObject.name); var parent = selectedGameObject.transform.parent; while (parent != null) { pathBuilder.Insert(0, parent.name + "/"); parent = parent.transform.parent; } Debug.Log(pathBuilder.ToString()); }
この選択状態でコマンドを実行すると、
このように、各GameObjectの階層をパスとして出力することができました。コマンドを実行できる条件を設定する
現在の
Sample Command 02
は、GameObjectを全く選択していなくてもコマンドを実行することができます。選択していない状態で実行したとしても
MonkeyEditorUtils.OrderedSelectedGameObjects
の要素が0で返ってくるだけなのでエラーが発生することはありませんが、それではコマンドを実行する意味がないため、「GameObjectを選択している状態でのみ」実行できるように、検証条件を設定してみましょう。コマンドの検証条件は
MonKey.Command
属性のDefaultValidation
プロパティで設定します。
DefaultValidation
にはMonKey.DefaultValidation
列挙体の値が使用できます。MonKey.DefaultValidation列挙体(抜粋)
値 用途 AT_LEAST_ONE_GAME_OBJECT 1個以上のGameObjectを選択している。 AT_LEAST_TWO_GAME_OBJECTS 2個以上のGameObjectを選択している。 IN_PLAY_MODE プレイモード中である。 IN_EDIT_MODE プレイモード中ではない。 今回は、「GameObjectを選択している状態でのみ」実行できるようにしたいので、
MonKey.DefaultValidation.AT_LEAST_ONE_GAME_OBJECT
を指定します。[Command( "Sample Command 02", QuickName = "SC2", Help = "選択中のGameObjectの階層パスをコンソールに出力する", DefaultValidation = DefaultValidation.AT_LEAST_ONE_GAME_OBJECT )] static void SampleCommand02() {
GameObjectを選択していない状態でコマンドを選択すると…
ご覧のように「Select at least one GameObject」の警告が表示され、コマンドを実行することができません。コマンドの製作者が自身だけで使用する分には使い方を気をつければいいので、検証条件の定義は必須ではありませんが、他者へ共有する予定がある場合は検証条件を正しく定義して正しい状況で使用してもらえるようにしたいですね。
おわりに
カスタムコマンドを作成してMonKeyが実行できる機能を充実させることで、Unity上の作業を更に効率化することができます。
なお、Unityエディタ上からstaticメソッドを実行するには
UnityEditor.MenuItem
属性でメニューアイテム化する、という方法もありますが、MonKeyなら処理の実行条件の検証を行ったり実行時に引数を与えることができるので、メニューアイテムとして実行するよりも柔軟な処理の実行が可能となります。
そういった用途で実行したい処理がある場合にはMonKeyのカスタムコマンド作成を検討してみてください。さて、PONOS Advent Calendar 2019は6日目も続けて私@e73ryoが担当する予定です。
次回は引数を与えてカスタムコマンドを実行する方法について紹介したいと思います。
- 投稿日:2019-12-05T00:57:39+09:00
DynamicObjectとClosedXMLによるExcelファイルのデータ読み取り
C# その2 Advent Calendar 2019 の 11 日目の記事です。
はじめに
今まで、記事とか書いたこともないし、他の方々と違いレベルの低い話題なので、そこのところご承知おきください。
経緯
業務上、Excel ファイル にエクスポートされたデータを取り扱うことが多く、VBA でマクロを書くのも
VBE がアレすぎて面倒なため、C#で処理したいなーと今回作るにあたっては、読み込んで値を変える程度ですが、業務によっては読み込んだものを加工して転記したり、DB に登録したりすると思うので自分用にメモ
なお、今回ClosedXMLについては、2019/12/01時点の最新版を使ってます
サンプルデータ
こんな感じでサンプルデータを XLSX ファイルとして作成
Id Name Affiliated Age Position 1 A1 総務部 28 部長 2 B2 人事部 19 係長 3 C3 開発部 34 課長 4 D4 開発部 23 係長 5 E5 製造部 18 部長 6 F6 製造部 69 一般
サンプルに使えそうな人な名前が思いつかない早速作成
ということで、DynamicObject を継承する形で作成
基本的には、TryGetMember と TrySetMember を override する
なお、ベースは IDictionaryとしてフィールド名にあたるものとその値を用意 (状況によっては、TrySetIndex、TrySetIndex も override してもいいのかも)DataRecord.csprivate readonly IDictionary<string, object> dictionary; public DataRecord(IDictionary<string, object> dictionary) => this.dictionary = dictionary; public override bool TrySetMember(SetMemberBinder binder, object value) { if (!IsTypeCheck(binder.Name, value)) return false; dictionary[binder.Name] = value; return true; } private bool IsTypeCheck(string key, object value) { // キーがないならNG if (!dictionary.TryGetValue(key, out var result)) return false; // 型が一致しない場合はNG return IsTypeMatch(result.GetType(), value.GetType()); } private bool IsTypeMatch(Type baseType, Type valueType) => valueType.Equals(baseType) || valueType.IsSubclassOf(baseType); public override bool TryGetMember(GetMemberBinder binder, out object result) => dictionary.TryGetValue(binder.Name, out result);読み取りとか行うものを
XLSX を ClosedXML で読み込み
項目名称の取得やデータ取得の仕方は Excel ファイルのデータ次第なので、そこは適宜に
今回は RangeUsed メソッドからテーブル変換し、 Fields プロパティから項目名称を、データについては DataRange プロパティを用いて取得します。ExcelControl.cspublic IEnumerable<dynamic> ReadExcelData() { using (IXLWorkbook workbook = new XLWorkbook(Path)) { IXLWorksheet worksheet = workbook.Worksheet(1); // 項目名称の取得 var tables = worksheet.RangeUsed().AsTable(); var columnNames = tables.Fields.Select(field => field.Name); var values = tables.DataRange.Rows(); // 生成開始 var generator = new DataRecordGenerator(columnNames, values); return generator.Generate(); } }DataRecordGenerator.cspublic IEnumerable<dynamic> Generate() { foreach (var row in rows) { var dic = columnNames.Select((name, index) => (name, index)) .Select(x => (x.name, row.Cell(x.index + 1).Value)) .ToDictionary(k => k.name, v => v.Value); yield return new DataRecord(dic); } }んで、テストコード
[TestMethod] public void TestMethod1() { var excelControl = new ExcelControl(path); var result = excelControl.ReadExcelData().ToArray(); foreach (var item in result) { Console.WriteLine($"{item.Id},{item.Name},{item.Affiliated},{item.Age},{item.Position}"); } // 数値に関してはClosedXMLの読み取るとdoubleで取得される result[0].Age = (double)50; result[0].Affiliated = "役員"; result[0].Position = "執行役員"; Console.WriteLine($"{result[0].Id},{result[0].Name},{result[0].Affiliated},{result[0].Age},{result[0].Position}"); }実行結果
1,A1,総務部,28,部長 2,B2,人事部,19,係長 3,C3,システム開発部,34,課長 4,D4,システム開発部,23,係長 5,E5,製造部,18,部長 6,F6,製造部,69,一般 1,A1,役員,50,執行役員Excel の内容を読み込みすることに成功し、また書き換えた後の出力も問題ありません。
感想
今回はテキストにしただけですが、読み込みして書き換えできるようにすることができました。
そもそも、ClosedXML だと自動マッピングできるかといわれるとそこらへんは勉強不足です。(できるんだったらそっち使ったほうがいいかもです)
リフレクション使ってのマッピングということで自分で実装するのがベターなのかもしれません、というより実際にテストした際も実行速度については、普通に型マッピングしたほうが早かったですしただ、業務で使うとなると、データ都合上Excel 中に 50 列くらい普通に並ぶものもあると思うので、マッピングしたい項目数が多い場合だったり、とりあえず意識しとうないときとかは dynamic を使うこともありなのかもしれません。
(そもそも、項目が多い時点で「孟徳!なぜ俺がこんなものを見なきゃならん!」的な作業なので)サンプルは以下
https://github.com/exactead/ExcelReaderDynamic参考ソース・記事
・ClosedXML (https://github.com/ClosedXML/ClosedXML )
・「C# DynamicObjectの基本と細かい部分について」
(http://neue.cc/2010/05/06_257.html )・「【C#】ClosedXML で Excel テーブルを IEnumerableオブジェクトに変換」
(https://qiita.com/penguinshunya/items/dd586b1e42b7a66e552e )
- 投稿日:2019-12-05T00:53:53+09:00
VR剣戟ゲーのための自作当たり判定処理
はじめに
SEKIROみたいな剣の斬り合いがVRでやりたい!!!!
こんにちは、ZeniZeniです。
昨今、Sword Of GargantuaやSword Master VRなど、面白い良VR剣戟ゲームが増えてきました。
それらをプレイしてると、自分の理想の剣戟ゲームというのを作りたい欲がふつふつと湧き上がってきます。
というわけで絶賛開発中です。今回は、VRで剣戟ゲームを作るための第一歩として、剣を高速で振っても、剣の当たり判定と剣同士が交差した座標を取得できる機能を実装しようと思います。
下の動画のようなことができるようになります。この速度で当たり判定とるの苦労したができた!
— ZeniZeni (@ZeniYuki0922) November 13, 2019
めっっっっちゃ楽しい!!!
VR剣戟ゲーはこれくらい斬りあう感じが俺の理想
(音あり) pic.twitter.com/7ZMnJZBgdPこれは、剣が交差した瞬間の座標を取得して、その座標から火花のエフェクトを発生させています。
実装方法
実装方法ですが、コリダーは使わずにやっています。
なぜかというと、高速な物体同士の当たり判定は、コリダーだと簡単にすり抜けてしまうからです。
下の動画くらいの速度が限界でした。SEKIROみたいに火花を散らすとめちゃめちゃ楽しいことに気づいた pic.twitter.com/B4hWueOCrl
— ZeniZeni (@ZeniYuki0922) November 12, 2019プラス、座標を取得するために小さいコリダーを大量に配置していたので、剣を変えるときは設定がめんどくさいですし、パフォーマンスもよろしくなさそうです。
それではコリダーを使わない当たり判定の実装方法を考えていきましょう。
まず、剣と剣がぶつかった判定をどうとるかを考えてみます。
これは三次元空間において、ある線分と線分の距離が一定値以下になったときを考えればよさそうです。剣同士の距離の導出
計算方法
それでは、$点(p_{11}, p_{12})$からなる線分$L_1$と、$点(p_{21}, p_{22})$からなる線分$L_2$の距離$d$を導出していきます。
こちらのサイトを参考にしてみます。
まず線分$L_1$の方向ベクトルを$V_1$、線分$L_2$の方向ベクトルを$V_2$として、$V_1$と$V_2$の外積、すなわち線分$L_1$から線分$L_2$に垂直なベクトル$n$を求めます。
$L_1$上の任意の点$P_1$から$L_2$上の任意の点$P_2$へのベクトルを$V_{12}$とすれば、ベクトル$n$とベクトル$V_{12}$の内積が、そのまま距離$d$となります。
計算がうまくできないとき
上記の導出では、線分同士が同一平面上にあるときには距離$d$は必ず0になり、正確な値が出ません。
ベクトル$V_{12}$とベクトル$n$が垂直になり、内積$(V_{12},n) = 0$となるからです。(垂直なベクトルの内積は0)
実際の所、剣同士をぶんぶん振り回している中で、剣同士が同一平面上になるときなど滅多にないのですが、一応考慮しておきます。剣同士が交差した座標の導出
計算方法
火花を剣同士がぶつかった瞬間にぶつかった場所から発生させたいので、剣同士が交差した座標を導出していきます。
これがちょっとめんどくさいです。
考え方としては、まず線分$L_2$上の点で、線分$L_1$に最も近い点を$P_{min}$とします。その点$P_{min}$上から線分$L_1$への垂線の方向ベクトルの単位ベクトルを$\hat{n}$とします。
剣同士の交点ですが、例えば剣同士が10cmより小さくなったときを剣同士が接触したと考えれば、剣同士の交点は剣同士の互いに最も近い点二つの中点とするのがよさそうです。
ゆえに交点$M$は、剣同士の距離を$d$とすればM = P_{min} + \frac{d}{2} * \hat{n}となります。
それでは次に、$P_{min}$を導出していきます。
まず、線分$L1$上の任意の点$P_1$は、線分$L_1$の始点$P_{11}$の$x$座標を$P_{11x}$、線分$L_1$の方向ベクトル(始点$P_{11} - $ 終点$P_{12}$)の$x$成分を$v_{1x}$のようにあらわすとして、状態変数$t_1$$(0 \leqq t_1 \leqq 1)$を用いれば
\begin{align} P_1 &= (l_{1x},l_{1y},l_{1z}) \\ &= (p_{11x} + t_1v_{1x},p_{11y} + t_1v_{1y},p_{11z} + t_1v_{1z}) \\ \end{align}と表せます。
また線分$L2$上の任意の点$P_2$も同様にして\begin{align} P_2 &= (l_{2x},l_{2y},l_{2z}) \\ &= (p_{21x} + t_2v_{2x},p_{21y} + t_2v_{2y},p_{21z} + t_2v_{2z}) \\ \end{align}と表せます。
すると距離$d$は
\begin{align} d^2 &= (l_{1x} - l_{2x})^2 + (l_{1y} - l_{2y})^2 +(l_{1z} - l_{2z})^2 \\ &= (v_{1x}^2 + v_{1y}^2 + v_{1z}^2)t_1^2 \\ & \quad \quad + 2(v_{1x}v_{2x} + v_{1y}v_{2y} + v_{1z}v_{2z})t_1t_2 \\ & \quad \quad + 2(v_{1x}(p_{11x} - p_{21x}) + v_{1y}(p_{11y} - p_{21y}) + v_{1z}(p_{11z} - p_{21z}))t_1 \\ & \quad \quad + (v_{2x}^2 + v_{2y}^2 + v_{2z}^2)t_2^2 \\ & \quad \quad + 2(v_{2x}(p_{21x} - p_{11x}) + v_{2y}(p_{21y} - p_{11y}) + v_{2z}(p_{21z} - p_{11z}))t_2 \\ & \quad \quad + (p_{11x}-p_{21x})^2 + (p_{11y}-p_{21y})^2 + (p_{11z}-p_{21z})^2 \end{align}というようにあらわせます。
うへぇ…って思いますよね、僕は思いました。
これを次数に注目して、係数は適当な文字に置き換えて、平方完成してみます。\begin{align} d^2 &= At_1^2 + Bt_1 + Ct_1t_2 + Dt_2^2 + Et_2 + F \\ &= A\biggr(t_1 + \frac{C}{2A}t_2 + \frac{B}{2A}\biggr)^2 + \biggr(D - \frac{C^2}{4A}\biggr)\biggr(t_2 + \frac{E - \frac{2BC}{4A}}{2D-\frac{C^2}{4A}}\biggr)^2 -\frac{B^2}{4A} + F \end{align}今求めようとしている点$P_{min}$は、$d$が最小のとき、すなわち平方完成した部分が0になるときなので、
\begin{align} t_2 &= - \frac{E - \frac{2BC}{4A}}{2D-\frac{C^2}{4A}} \\ &= \frac{BC - 2AE}{4AD - C^2} \end{align}のときです。
したがって$P_{min}$は$P_2 = (p_{21x} + t_2v_{2x},p_{21y} + t_2v_{2y},p_{21z} + t_2v_{2z})$の$t_2$に$\frac{BC - 2AE}{4AD - C^2}$を代入したものとなります。絶対に交差しないとき
剣が絶対に交差しない状況のときは上のような計算をするのは無駄なので、そのような状況は早い段階ではじきましょう。
剣が絶対に交差しない状況は、下図のようなときです。
これにz座標の判定も加わります。実際のコード
それでは実際に書いた線分同士の距離と交点を導出するコードがこちらです。
線分同士の距離とその交点を同時に取得したかったので、IntersectionInfoという構造体を作っています。
線分はLineという構造体を作成していて、剣の刃の部分の根本と剣先の2点を設定してください。IntersectionChecker.csusing System.Collections; using System.Collections.Generic; using UnityEngine; using System; [Serializable] public struct Line { public Transform p1; public Transform p2; } public struct IntersectionInfo { public float Distance; public Vector3 MidPoint; } public class IntersectionChecker : MonoBehaviour { public Line l1; public Line l2; public IntersectionInfo info; // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { if (Input.GetKeyDown(KeyCode.Space)) { var t = GetIntersectionInfo(l1, l2); Debug.Log("distance is " + t.Distance); Debug.Log("mid point is " + t.MidPoint); } } public IntersectionInfo GetIntersectionInfo(Line line1, Line line2, float dThreshold = 0.5f) { //各平面で交差していない時は排除 if (!CheckIntersectionException(line1, line2)) { Debug.Log("not intersect"); info.Distance = -1; return info; } Debug.Log("intersect!"); var p11 = line1.p1.position; var p12 = line1.p2.position; var p21 = line2.p1.position; var p22 = line2.p2.position; var v1 = p12 - p11; var v2 = p22 - p21; var v12 = p22 - p11; var n = Vector3.Cross(v1, v2).normalized; var d = Mathf.Abs(Vector3.Dot(n, v12)); //線分同士が同一平面上にあるとき if (d == 0) { if (IsInSamePlane(line1, line2)) { Debug.Log("lines are in same plane"); info.Distance = -1; return info; } } //dThresholdより離れている時を排除 if (d > dThreshold) { info.Distance = -1; return info; } info.Distance = d; //線分ががもう一つの線分に対して手前か奥にあるかの判定 var side = (Vector3.Cross(v1, v12).y < 0 ? 1 : -1); var tmpA = v1.x * v1.x + v1.y * v1.y + v1.z * v1.z; var tmpB = 2 * (v1.x * (p11.x - p21.x) + v1.y * (p11.y - p21.y) + v1.z * (p11.z - p21.z) ); var tmpC = 2 * (v1.x * v2.x + v1.y * v2.y + v1.z * v2.z); var tmpD = v2.x * v2.x + v2.y * v2.y + v2.z * v2.z; var tmpE = 2 * ( v2.x * (p21.x - p11.x) + v2.y * (p21.y - p11.y) + v2.z * (p21.z - p11.z) ); //var t2 = -( tmpE - ( (2 * tmpB * tmpC) / (4 * tmpA) ) ) / ( 2 * (tmpD - ( (tmpC * tmpC ) / (4 * tmpA) )) ); var t2 = ( tmpB * tmpC - 2 * tmpA * tmpE) / ( 4 * tmpA * tmpD - tmpC * tmpC); Debug.Log("P min is " + (p21 + (t2 * v2))); info.MidPoint = p21 + (t2 * v2) + ((d/2) * side * n); return info; } public bool IsInSamePlane(Line line1, Line line2) { var p1 = line1.p1.position; var p2 = line1.p2.position; var p3 = line2.p1.position; var p4 = line2.p2.position; var v1 = p2 - p1; var v2 = p3 - p1; var v3 = p4 - p1; var det = (v1.y * v2.z * v3.x) + (v1.z * v2.x * v3.y) + (v1.x * v2.y * v3.z) - (v1.z * v2.y * v3.x) - (v1.x * v2.z * v3.y) - (v1.y * v2.x * v3.z); return det == 0; } public bool CheckIntersectionException(Line line1, Line line2) { var p1 = line1.p1.position; var p2 = line1.p2.position; var p3 = line2.p1.position; var p4 = line2.p2.position; //x座標チェック if (p1.x <= p2.x) { if ((p3.x < p1.x && p4.x < p1.x) || (p2.x < p3.x && p2.x < p4.x)) { return false; } } else { if ((p3.x < p2.x && p4.x < p2.x) || (p1.x < p3.x && p1.x < p4.x)) { return false; } } //y座標チェック if (p1.y <= p2.y) { if ((p3.y < p1.y && p4.y < p1.y) || (p2.y < p3.y && p2.y < p4.y)) { return false; } } else { if ((p3.y < p2.y && p4.y < p2.y) || (p1.y < p3.y && p1.y < p4.y)) { return false; } } //z座標チェック if (p1.z <= p2.z) { if ((p3.z < p1.z && p4.z < p1.z) || (p2.z < p3.z && p2.z < p4.z)) { return false; } } else { if ((p3.z < p2.z && p4.z < p2.z) || (p1.z < p3.z && p1.z < p4.z)) { return false; } } return true; } } }という感じになります。
交差しない場合やいくつかの例外時には、IntersectionInfoのDistanceは-1となります。下図のような感じで設定してください。
後は、GetIntersectionInfo関数を呼んで得られたIntersectionInfoのMidPointで火花等のエフェクトを発生させればよいのです。剣を速く振ったときだけ呼びたい場合、まず剣の振る速度を求める方法を考えると思います。
剣の振る速度は、SteamVR SDKアセットに入っている、VelocityEstimatorというコンポーネントを使うことをお勧めします。開発、執筆にあたり、下記のサイト様を参考、引用させていただきました。
- 投稿日:2019-12-05T00:02:45+09:00
クエリ記法をHaskellのモナド&do記法みたいに使う話
この記事はC# Advent Calendar 2019の12月5日の記事として書かれました。
この記事はkekyoさんのスライド「C#でわかる こわくないMonad」をモチベーションに書かれています。
kekyoさんのスライドでは、HaskellのMaybeモナド(Optionモナド)に相当するクラスを、まるでHaskellのdo記法のようにC#のクエリ記法で扱う方法が丁寧に紹介されています。本記事ではその他のモナドも同様の手法を用いて実装していきます。TL; DR
- C#のクエリ記法でHaskellのモナド & do 記法みたいな機能が実現できるよ!
- 簡単な実装でそれを実現できるよ!
- この手法が用いられたプロダクトもあるので紹介するよ!
概要
LINQでおなじみのクエリ記法(
from ... in ... select ...
)ですが、これを用いてHaskellのモナド&Do記法に近い書き方がC#でもできることを紹介します。HaskellでおなじみのモナドのうちMaybeモナド(Optionモナド) & do記法をC#で再現する方法については、既にkekyoさんのスライド「C#でわかる こわくないMonad」で大変詳しく紹介されています。
本記事ではHaskellの入門者である『すごいHaskellたのしく学ぼう!』に掲載されているモナドのうち幾つかについて簡単に紹介しつつ、適切な実装を与えることでHaskellのdo記法に似たことがC#のクエリ構文でもできることを例示します。そして、その実装がC#で記述量的にそこまで重くないことを確認します。kekyoさんのスライドでも触れられている通り、C#でわざわざこの書き方をする意義は多くの場合あまりなさそうです。しかし、この手法が効果的に使われているSpracheというプロダクトがあります。最後にそのプロダクトがどのようにクエリ構文を活用しているか簡単に紹介します。
前提知識
HaskellやHaskellのモナドの知識は特に仮定しません。Haskellのコードが出てきたときは解説を入れます。漠然とコードを眺めて「Haskellのdo記法と似た書き方がとC#でもできるのだなあ」と納得していだければ筆者の目的は達成します。
Haskellのモナド+do記法とは
文脈を伴う計算を簡単な構文で貼り合わせて大きな単位にしていける機能...などと言葉を尽くしたいところですが、おそらく自然言語で言葉を尽くすより以下の実例を見たほうが直感が得られやすいと思います。
この辺りの話については、『すごいHaskellたのしく学ぼう!』という本にわかりやすい例と詳しい説明があるので、興味のある方はそちらの本でHaskellに触れてみてもいいかもしれません。実例
以下ではHaskellのモナド & Do記法の例とそれに対応するC#のコードを幾つか見ていきます。
まず、それぞれのモナドに対して使われ方を確認します。
C#のクエリ記法で同様の書き方をするために必要となる実装は、最後にまとめて確認します。使われ方
List モナド
Listモナドにおける文脈は「複数の可能性」です。Listモナドは、可能なケースの組み合わせ全てに対して答えを返すときに有用です。以下のHaskellでの例を見てみましょう。
Haskellでの例
allPossibility :: [Int] allPossibility = do sgn <- [-1, 1] a <- [1, 2, 3] b <- [4, 5] return $ sgn * (10 * a + b) main :: IO () main = print allPossibilitysgnは-1と1の可能性があり、aは1と2と3の可能性があり、bは4と5の可能性があります。
これに対して、sgn * (10 * a + b)
という計算をしようとしています。実行すると以下の出力を得ます。
[-14,-15,-24,-25,-34,-35,14,15,24,25,34,35]可能な組み合わせ全てを網羅した結果がリストとして得られます。
C#での例
我々が見慣れているC# での IEnumerableに対するクエリ構文は、HaskellのListモナドに対するdo記法に対応していると考えることができます。
var allPossibility = from sgn in new int[] { -1, 1 } from a in new int[] { 1, 2, 3 } from b in new int[] { 4, 5 } select sgn * (10 * a + b); allPossibility.ToList().ForEach(x => Console.Write($"{x} ");実行すると、同じく全ての可能性を網羅した以下の出力を得ます。
-14 -15 -24 -25 -34 -35 14 15 24 25 34 35Haskellのdo記法を用いた書き方と近い書き方ができています。
Maybe モナド
Maybeモナドにおける文脈は「失敗する可能性」「値が無い可能性」です。
Maybeモナドは失敗する可能性のある計算を張り合わせるときに用いられます。
こちらもHaskellでの例を通じて説明します。Haskell での例
Maybe Int型
はC#のint?
に似た型です。
何らかの値が入っているかもしれませんし(Just n
)、値が入っていないかもしれません(Nothing
)。以下のコードのsumThreeは、Maybe Int型の3つ組を受け取ってそれらの和を返そうとする関数です。
mainではtestCasesの中の各test caseに対して、そのsumThreeを求めて出力しています。sumThree :: (Maybe Int, Maybe Int, Maybe Int) -> Maybe Int sumThree (ma, mb, mc) = do a <- ma b <- mb c <- mc return $ a + b + c testCases = [ (Just 1, Just 2, Just 3), (Nothing, Just 2, Just 3), (Just 1, Nothing, Just 3), (Just 1, Just 2, Nothing), (Nothing, Nothing, Nothing) ] main :: IO () main = mapM_ (print . sumThree) testCases結果は
Just 6 Nothing Nothing Nothing Nothingとなります。ma, mb, mcが全て
Just n
だったときのみ計算は成功し、目的の値を返します。
どれか一つでもNothing
だった場合、計算は失敗し、Nothing
を返します。C#での例
C#ではうまくMaybeクラスを定義してやれば以下のように書くことができます。
Maybe<int> sumThree(Maybe<int> ma, Maybe<int> mb, Maybe<int> mc) => from a in ma from b in mb from c in mc select a + b + c; var testCases = new List<(Maybe<int>, Maybe<int>, Maybe<int>)>{ (Just(1), Just(2), Just(3)), (Nothing, Just(2), Just(3)), (Just(1), Nothing, Just(3)), (Just(1), Just(2), Nothing), (Nothing, Nothing, Nothing), }; testCases.ForEach(testCase => { var (ma, mb, mc) = testCase; Console.WriteLine($"{ma} + {mb} + {mc} = {sumThree(ma, mb, mc)}"); });Haskellでの例と同様に、以下の出力を得ます。
Just(1) + Just(2) + Just(3) => Just(6) Nothing + Just(2) + Just(3) => Nothing Just(1) + Nothing + Just(3) => Nothing Just(1) + Just(2) + Nothing => Nothing Nothing + Nothing + Nothing => NothingWriterモナド
Writerモナドにおける文脈は「ロギング」です。なんらかのログをとりながら計算を進めていくことができます。
Haskellでの例
import Control.Monad.Writer sw :: Writer String Int sw = do a <- writer (3, "a is 3. ") b <- writer (a + 4, "b is a + 4. ") c <- writer (a + b, "c is a + b. ") return c main :: IO () main = print swこれを実行すると、
WriterT (Identity (10,"a is 3. b is a + 4. c is a + b. "))との出力を得ます。出力の仔細については今回見る必要はありません。
a =3; b = a + 4: c = a + b
の計算結果である10と、そこに至るまでのログである"a is 3. b is a + 4. c is a + b. "
の両方の情報が保持されていることが確認できます。C#での例
こちらもうまくStringWriterクラスを定義してやれば以下のように書くことができます。
StringWriter<int> SW(int i, String s) => new StringWriter<int>(i, s); var sw = from a in SW(3, "a is 3. ") from b in SW(b + 4, "b is a + 4. ") from c in SW(a + b, "c is a + b. ") select c; Console.WriteLine($"value: {sw.Value}"); Console.WriteLine($"log: {sw.Log}");こちらはStringに限ったWriterの実装にしています。(
+
をどう持つかなどを決めるのが面倒だったため)出力は以下の通りです。
value: 10 log: a is 3. b is a + 4. c is a + b.Readerモナド
Readerモナドにおける文脈は、全ての関数に共通して渡される引数です。言葉による説明では少しわかりづらいと思うので、Haskellでの例を見てみましょう。
Haskellでの例
reader :: Int -> String reader = do twice <- (* 2) len <- (length . show) plusHundred <- (+ 100) return $ "twice: " ++ show twice ++ ", length: " ++ show len ++ ", plus 100: " ++ show plusHundred main :: IO () main = putStrLn $ r 15main関数を見てみると、 readerという関数に15が渡されています。
readerの中では、引数を2倍する処理、引数を文字列化して文字列の長さを得る処理、100を足す処理の3つの処理全てに15が渡されます。
その結果、twiceには30が、lenには2が、plusHundredには115が入ります。出力は以下の通りです。
twice: 30, length: 2, plus 100: 115C#での例
Readerクラスを定義して、以下のように書けます。
Reader<int, int> R(Func<int, int> f) => new Reader<int, int>(f); var reader = from twice in R(x => x * 2) from len in R(x => x.ToString().Length) from plusHundred in R(x => x + 100) select $"twice: {twice}, length: {len}, plus 100: {plusHundred}"; Console.WriteLine(reader.F(15));出力は以下の通りです。
twice: 30, length: 2, plus 100: 115Stateモナド
Stateモナドにおける文脈は状態です。
裏で状態を持ち回して使います。C#では、例えば
currentState
のような変数を用意してその変数の値を書き換えていけば、状態を読み、変更して、下の行に伝播させることは容易です。
しかし、Haskellでは全ての変数はImmutableなので、変数を書き換える方法では状態を持ち回せません。そこで、状態を簡単に持ち回すためにStateモナドが用いられます。Haskellでの例
import Control.Monad.State type Stack = [Int] pop :: State Stack Int pop = state $ \(x : xs) -> (x, xs) push :: Int -> State Stack () push a = state $ \xs -> ((), a : xs) f :: State Stack Int f = do push 3 push 1 push 4 push 1 push 5 push 9 a <- pop b <- pop c <- pop return $ a + b + c main :: IO () main = print $ runState f []stackに、 3, 1, 4, 1, 5, 9を順番に積んでいき、3回popして、得られた値を足し合わせています。
最後に積まれた3つの値は1, 5, 9なので、和として15が出力され、 stackには最終的に一番上から順に4, 1, 3が積まれています。
以下は実行結果です。(15,[4,1,3])C#での例
Stateクラスを定義して、以下のようにすることができます。
例示の都合上、Stackクラスも定義しています。State<Stack<int>, int> pop() => new State<Stack<int>, int>(stack => stack.Pop()); State<Stack<int>, UnitType> push(int i) => new State<Stack<int>, UnitType>(stack => (new Cons<int>(i, stack), Unit)); var f = from _1 in push(3) from _2 in push(1) from _3 in push(4) from _4 in push(1) from _5 in push(5) from _6 in push(9) from a in pop() from b in pop() from c in pop() select a + b + c; var (state, result) = f.F(new Nil<int>()); Console.Write("state: "); state.ToList().ForEach(x => Console.Write($"{x} ")); Console.WriteLine(); Console.WriteLine($"result: {result}");現行のC#ではこの場面でdiscard
_
が使えないため、使わない値に一々_1
,_2
などと名前をつけています。実行結果は以下の通りです。
state: 4 1 3 result: 15実装側
このセクションの目的は「クエリ構文をHaskellのdo記法みたいに使うためには、それほど大変な実装をする必要はなさそうだ」という雰囲気を掴んでもらうことにあります。したがって本セクションを細かく読んでいただく必要はありません。「このくらいの行数で済むのかー」くらいの読み方をしていただければ幸いです。
Select & SelectMany v.s. Return & Bind
クエリ構文を上で紹介したように使うには、from の右側に来る値が
Select
メソッドとSelectMany
メソッドを持っている必要があります。
この値の型をMとしたときに拡張メソッド方式でSelect
とSelectMany
を書くと、各メソッドのシグネチャと返り値の型はM<T2> Select<T1, T2>(this M<T1>, Func<T1, T2>) M<T3> SelectMany<T1, T2, T3>(this M<T1>, Func<T1, Maybe<T2>>, Func<T1, T2, T3>)となります。
これはHaskellの型の書き方では
select :: m t1 -> (t1 -> t2) -> m t2 selectMany :: m t1 -> (t1 -> m t2) -> (t1 -> t2 -> t3) -> m t3に相当します。見ての通り
selectMany
が少し複雑です。
haskellのモナドを定義するにはreturn
とbind
を定義すればよいのですが、これらはC#のシグネチャ+返り値の型ではM<T1> Return<T1>(T1 value) M<T2> Bind<T1, T2>(M<T1>, Func<T1, Maybe<T2>>)であり。Haskellの型では
return :: t1 -> m t1 bind :: m t1 -> (t1 -> m t2) -> m t2となるような関数です。
Select
&SelectMany
の組み合わせと比べると単純なのが見て取れると思います。
実は、クエリ構文をdo記法のように機能をさせるためのSelect
とSelectMany
は、Return
とBind
の組み合わせによって実装できます。よって、簡単な方の組み合わせとしてReturn
とBind
を使って説明していきます。Listモナド
今回自前で実装していないので説明は省略します。
Maybeモナド
Maybe<T>
クラスとそのサブクラスを以下のように定義します。abstract class Maybe<T> { public static Maybe<T> Nothing => Nothing<T>.Instance; public static Maybe<T> Just(T value) => new Just<T>(value); } sealed class Nothing<T> : Maybe<T> { private static Nothing<T> instance = new Nothing<T>(); public static Nothing<T> Instance => instance; private Nothing() { } public override string ToString() => "Nothing"; } sealed class Just<T> : Maybe<T> { public T Value { get; } public Just(T value) => Value = value; public void Deconstruct(out T value) => value = Value; public override string ToString() => $"Just({Value})"; }サブクラス関係によって、値がない状態
Nothing<T>
と値がある状態Just<T>
を表現できていることがわかります。
これに対して以下のようにReturnとBindを定めます。public static Maybe<T> Return<T>(T value) => Maybe<T>.Just(value); public static Maybe<T2> Bind<T1, T2>(Maybe<T1> x, Func<T1, Maybe<T2>> f) => x switch { Nothing<T1> _ => Maybe<T2>.Nothing, Just<T1>(var v) => f(v) };Returnは受け取った値を特に何も処理せずにJustでくるんで返します。
Bindは受け取った値がNothing型かJust型かで場合分けし、Nothing型の場合はNothingを、Just型の場合は中の値を取り出し、その値に関数を適用して、再びJust型でくるんで返します。Writerモナド
ValueとLogを持つStringWriterクラスを定義します。
class StringWriter<T> { public T Value { get; } public string Log { get; } public StringWriter(T value, String log = "") => (Value, Log) = (value, log); }これに対して以下のようにReturnとBindを定義します。
public static StringWriter<T> Return<T>(T value) => new StringWriter<T>(value); public static StringWriter<T2> Bind<T1, T2>(StringWriter<T1> sw, Func<T1, StringWriter<T2>> f) { var sw2 = f(sw.Value); return new StringWriter<T2>(sw2.Value, sw.Log + sw2.Log); }やはりReturnはくるむだけです。特に説明することはありません。
Readerモナド
Reader<TIn, TOut>は関数を保持するクラスです。 class Reader<TIn, TOut> { public Func<TIn, TOut> F { get; } public Reader(Func<TIn, TOut> f) => F = f; }これに対して以下のようにReturnとBindを定義します。
public static Reader<TIn, T> Return<TIn, T>(T value) => new Reader<TIn, T>(_ => value); public static Reader<TIn, T2> Bind<TIn, T1, T2>(Reader<TIn, T1> x, Func<T1, Reader<TIn, T2>> f) => new Reader<TIn, T2>(y => f(x.F(y)).F(y));Returnは値を受け取って、「引数を受け取るがその値を無視して常に決まった値を返す関数」を持ったReaderクラスの値を作ります。
Stateモナド
class State<TState, T> { public Func<TState, (TState, T)> F { get; } public State(Func<TState, (TState, T)> f) => F = f; }に対して、
public static State<TState, T> Return<TState, T>(T value) => new State<TState, T>(s => (s, value)); public static State<TState, T2> Bind<TState, T1, T2>(State<TState, T1> x, Func<T1, State<TState, T2>> f) => new State<TState, T2>(st => { var (newSt, v) = x.F(st); return f(v).F(newSt); });で済みます。
いずれもかなり単純に実装できていることがお分かりいただけたかと思います。実用の話
私が以前書いた記事の「C#のパーサコンビネータライブラリSpracheでML風言語のインタプリタを実装する」で紹介していたSpracheというパーサコンビネータはクエリ記法で書けるように設計されています。
たとえばif e1 then e2 else e3
という式をパースするパーサーはParser<Expr> IfParser = from ifToken in Parse.String("if").Token() from p1 in PrimaryParser from thenToken in Parse.String("then").Token() from p2 in PrimaryParser from elseToken in Parse.String("else").Token() from p3 in PrimaryParser select new IfExpr(p1, p2, p3);のように書くことができます。
Parser型は文字列をパースしてT型の値を生成するパーサーの型です。例中だとprimaryParserというパーサーから、あたかもパース結果の値を取り出してp1, p2, p3に束縛しているかのように処理を書くことができます。まとめ
- C#のクエリ記法で、Haskellのモナド & do 記法相当の機能が実現できます。(C#においてわざわざこのように書いた方がいいケースはあまりなさそうにも思えますが...)
- 紹介した各モナドは記述量的にそこまで重くなく実装できます。
- クエリ記法を使うように設計されているプロダクトとしてSpracheがあります。
今回書いたコードのリンクはこちらです。
読んでいただきありがとうございました。皆様に幸せな年末がありますように!
- 投稿日:2019-12-05T00:02:45+09:00
C#のクエリ記法をHaskellのモナド&do記法みたいに使う話
この記事はC# Advent Calendar 2019の12月5日の記事として書かれました。
この記事はkekyoさんのスライド「C#でわかる こわくないMonad」をモチベーションに書かれています。
kekyoさんのスライドでは、HaskellのMaybeモナド(Optionモナド)に相当するクラスを、まるでHaskellのdo記法のようにC#のクエリ記法で扱う方法が丁寧に紹介されています。本記事ではその他のモナドも同様の手法を用いて実装していきます。TL; DR
- C#のクエリ記法でHaskellのモナド & do 記法みたいな機能が実現できるよ!
- 簡単な実装でそれを実現できるよ!
- この手法が用いられたプロダクトもあるので紹介するよ!
概要
LINQでおなじみのクエリ記法(
from ... in ... select ...
)ですが、これを用いてHaskellのモナド&Do記法に近い書き方がC#でもできることを紹介します。HaskellでおなじみのモナドのうちMaybeモナド(Optionモナド) & do記法をC#で再現する方法については、既にkekyoさんのスライド「C#でわかる こわくないMonad」で大変詳しく紹介されています。
本記事ではHaskellの入門者である『すごいHaskellたのしく学ぼう!』に掲載されているモナドのうち幾つかについて簡単に紹介しつつ、適切な実装を与えることでHaskellのdo記法に似たことがC#のクエリ構文でもできることを例示します。そして、その実装がC#で記述量的にそこまで重くないことを確認します。kekyoさんのスライドでも触れられている通り、C#でわざわざこの書き方をする意義は多くの場合あまりなさそうです。しかし、この手法が効果的に使われているSpracheというプロダクトがあります。最後にそのプロダクトがどのようにクエリ構文を活用しているか簡単に紹介します。
前提知識
HaskellやHaskellのモナドの知識は特に仮定しません。Haskellのコードが出てきたときは解説を入れます。漠然とコードを眺めて「Haskellのdo記法と似た書き方がとC#でもできるのだなあ」と納得していだければ筆者の目的は達成します。
Haskellのモナド+do記法とは
文脈を伴う計算を簡単な構文で貼り合わせて大きな単位にしていける機能...などと言葉を尽くしたいところですが、おそらく自然言語で言葉を尽くすより以下の実例を見たほうが直感が得られやすいと思います。
この辺りの話については、『すごいHaskellたのしく学ぼう!』という本にわかりやすい例と詳しい説明があるので、興味のある方はそちらの本でHaskellに触れてみてもいいかもしれません。実例
以下ではHaskellのモナド & Do記法の例とそれに対応するC#のコードを幾つか見ていきます。
まず、それぞれのモナドに対して使われ方を確認します。
C#のクエリ記法で同様の書き方をするために必要となる実装は、最後にまとめて確認します。使われ方
List モナド
Listモナドにおける文脈は「複数の可能性」です。Listモナドは、可能なケースの組み合わせ全てに対して答えを返すときに有用です。以下のHaskellでの例を見てみましょう。
Haskellでの例
allPossibility :: [Int] allPossibility = do sgn <- [-1, 1] a <- [1, 2, 3] b <- [4, 5] return $ sgn * (10 * a + b) main :: IO () main = print allPossibilitysgnは-1と1の可能性があり、aは1と2と3の可能性があり、bは4と5の可能性があります。
これに対して、sgn * (10 * a + b)
という計算をしようとしています。実行すると以下の出力を得ます。
[-14,-15,-24,-25,-34,-35,14,15,24,25,34,35]可能な組み合わせ全てを網羅した結果がリストとして得られます。
C#での例
我々が見慣れているC# での IEnumerableに対するクエリ構文は、HaskellのListモナドに対するdo記法に対応していると考えることができます。
var allPossibility = from sgn in new int[] { -1, 1 } from a in new int[] { 1, 2, 3 } from b in new int[] { 4, 5 } select sgn * (10 * a + b); allPossibility.ToList().ForEach(x => Console.Write($"{x} ");実行すると、同じく全ての可能性を網羅した以下の出力を得ます。
-14 -15 -24 -25 -34 -35 14 15 24 25 34 35Haskellのdo記法を用いた書き方と近い書き方ができています。
Maybe モナド
Maybeモナドにおける文脈は「失敗する可能性」「値が無い可能性」です。
Maybeモナドは失敗する可能性のある計算を張り合わせるときに用いられます。
こちらもHaskellでの例を通じて説明します。Haskell での例
Maybe Int型
はC#のint?
に似た型です。
何らかの値が入っているかもしれませんし(Just n
)、値が入っていないかもしれません(Nothing
)。以下のコードのsumThreeは、Maybe Int型の3つ組を受け取ってそれらの和を返そうとする関数です。
mainではtestCasesの中の各test caseに対して、そのsumThreeを求めて出力しています。sumThree :: (Maybe Int, Maybe Int, Maybe Int) -> Maybe Int sumThree (ma, mb, mc) = do a <- ma b <- mb c <- mc return $ a + b + c testCases = [ (Just 1, Just 2, Just 3), (Nothing, Just 2, Just 3), (Just 1, Nothing, Just 3), (Just 1, Just 2, Nothing), (Nothing, Nothing, Nothing) ] main :: IO () main = mapM_ (print . sumThree) testCases結果は
Just 6 Nothing Nothing Nothing Nothingとなります。ma, mb, mcが全て
Just n
だったときのみ計算は成功し、目的の値を返します。
どれか一つでもNothing
だった場合、計算は失敗し、Nothing
を返します。C#での例
C#ではうまくMaybeクラスを定義してやれば以下のように書くことができます。
Maybe<int> sumThree(Maybe<int> ma, Maybe<int> mb, Maybe<int> mc) => from a in ma from b in mb from c in mc select a + b + c; var testCases = new List<(Maybe<int>, Maybe<int>, Maybe<int>)>{ (Just(1), Just(2), Just(3)), (Nothing, Just(2), Just(3)), (Just(1), Nothing, Just(3)), (Just(1), Just(2), Nothing), (Nothing, Nothing, Nothing), }; testCases.ForEach(testCase => { var (ma, mb, mc) = testCase; Console.WriteLine($"{ma} + {mb} + {mc} = {sumThree(ma, mb, mc)}"); });Haskellでの例と同様に、以下の出力を得ます。
Just(1) + Just(2) + Just(3) => Just(6) Nothing + Just(2) + Just(3) => Nothing Just(1) + Nothing + Just(3) => Nothing Just(1) + Just(2) + Nothing => Nothing Nothing + Nothing + Nothing => NothingWriterモナド
Writerモナドにおける文脈は「ロギング」です。なんらかのログをとりながら計算を進めていくことができます。
Haskellでの例
import Control.Monad.Writer sw :: Writer String Int sw = do a <- writer (3, "a is 3. ") b <- writer (a + 4, "b is a + 4. ") c <- writer (a + b, "c is a + b. ") return c main :: IO () main = print swこれを実行すると、
WriterT (Identity (10,"a is 3. b is a + 4. c is a + b. "))との出力を得ます。出力の仔細については今回見る必要はありません。
a =3; b = a + 4: c = a + b
の計算結果である10と、そこに至るまでのログである"a is 3. b is a + 4. c is a + b. "
の両方の情報が保持されていることが確認できます。C#での例
こちらもうまくStringWriterクラスを定義してやれば以下のように書くことができます。
StringWriter<int> SW(int i, String s) => new StringWriter<int>(i, s); var sw = from a in SW(3, "a is 3. ") from b in SW(b + 4, "b is a + 4. ") from c in SW(a + b, "c is a + b. ") select c; Console.WriteLine($"value: {sw.Value}"); Console.WriteLine($"log: {sw.Log}");こちらはStringに限ったWriterの実装にしています。(
+
をどう持つかなどを決めるのが面倒だったため)出力は以下の通りです。
value: 10 log: a is 3. b is a + 4. c is a + b.Readerモナド
Readerモナドにおける文脈は、全ての関数に共通して渡される引数です。言葉による説明では少しわかりづらいと思うので、Haskellでの例を見てみましょう。
Haskellでの例
reader :: Int -> String reader = do twice <- (* 2) len <- (length . show) plusHundred <- (+ 100) return $ "twice: " ++ show twice ++ ", length: " ++ show len ++ ", plus 100: " ++ show plusHundred main :: IO () main = putStrLn $ r 15main関数を見てみると、 readerという関数に15が渡されています。
readerの中では、引数を2倍する処理、引数を文字列化して文字列の長さを得る処理、100を足す処理の3つの処理全てに15が渡されます。
その結果、twiceには30が、lenには2が、plusHundredには115が入ります。出力は以下の通りです。
twice: 30, length: 2, plus 100: 115C#での例
Readerクラスを定義して、以下のように書けます。
Reader<int, int> R(Func<int, int> f) => new Reader<int, int>(f); var reader = from twice in R(x => x * 2) from len in R(x => x.ToString().Length) from plusHundred in R(x => x + 100) select $"twice: {twice}, length: {len}, plus 100: {plusHundred}"; Console.WriteLine(reader.F(15));出力は以下の通りです。
twice: 30, length: 2, plus 100: 115Stateモナド
Stateモナドにおける文脈は状態です。
裏で状態を持ち回して使います。C#では、例えば
currentState
のような変数を用意してその変数の値を書き換えていけば、状態を読み、変更して、下の行に伝播させることは容易です。
しかし、Haskellでは全ての変数はImmutableなので、変数を書き換える方法では状態を持ち回せません。そこで、状態を簡単に持ち回すためにStateモナドが用いられます。Haskellでの例
import Control.Monad.State type Stack = [Int] pop :: State Stack Int pop = state $ \(x : xs) -> (x, xs) push :: Int -> State Stack () push a = state $ \xs -> ((), a : xs) f :: State Stack Int f = do push 3 push 1 push 4 push 1 push 5 push 9 a <- pop b <- pop c <- pop return $ a + b + c main :: IO () main = print $ runState f []stackに、 3, 1, 4, 1, 5, 9を順番に積んでいき、3回popして、得られた値を足し合わせています。
最後に積まれた3つの値は1, 5, 9なので、和として15が出力され、 stackには最終的に一番上から順に4, 1, 3が積まれています。
以下は実行結果です。(15,[4,1,3])C#での例
Stateクラスを定義して、以下のようにすることができます。
例示の都合上、Stackクラスも定義しています。State<Stack<int>, int> pop() => new State<Stack<int>, int>(stack => stack.Pop()); State<Stack<int>, UnitType> push(int i) => new State<Stack<int>, UnitType>(stack => (new Cons<int>(i, stack), Unit)); var f = from _1 in push(3) from _2 in push(1) from _3 in push(4) from _4 in push(1) from _5 in push(5) from _6 in push(9) from a in pop() from b in pop() from c in pop() select a + b + c; var (state, result) = f.F(new Nil<int>()); Console.Write("state: "); state.ToList().ForEach(x => Console.Write($"{x} ")); Console.WriteLine(); Console.WriteLine($"result: {result}");現行のC#ではこの場面でdiscard
_
が使えないため、使わない値に一々_1
,_2
などと名前をつけています。実行結果は以下の通りです。
state: 4 1 3 result: 15実装側
このセクションの目的は「クエリ構文をHaskellのdo記法みたいに使うためには、それほど大変な実装をする必要はなさそうだ」という雰囲気を掴んでもらうことにあります。したがって本セクションを細かく読んでいただく必要はありません。「このくらいの行数で済むのかー」くらいの読み方をしていただければ幸いです。
Select & SelectMany v.s. Return & Bind
クエリ構文を上で紹介したように使うには、from の右側に来る値が
Select
メソッドとSelectMany
メソッドを持っている必要があります。
この値の型をMとしたときに拡張メソッド方式でSelect
とSelectMany
を書くと、各メソッドのシグネチャと返り値の型はM<T2> Select<T1, T2>(this M<T1>, Func<T1, T2>) M<T3> SelectMany<T1, T2, T3>(this M<T1>, Func<T1, Maybe<T2>>, Func<T1, T2, T3>)となります。
これはHaskellの型の書き方では
select :: m t1 -> (t1 -> t2) -> m t2 selectMany :: m t1 -> (t1 -> m t2) -> (t1 -> t2 -> t3) -> m t3に相当します。見ての通り
selectMany
が少し複雑です。
haskellのモナドを定義するにはreturn
とbind
を定義すればよいのですが、これらはC#のシグネチャ+返り値の型ではM<T1> Return<T1>(T1 value) M<T2> Bind<T1, T2>(M<T1>, Func<T1, Maybe<T2>>)であり。Haskellの型では
return :: t1 -> m t1 bind :: m t1 -> (t1 -> m t2) -> m t2となるような関数です。
Select
&SelectMany
の組み合わせと比べると単純なのが見て取れると思います。
実は、クエリ構文をdo記法のように機能をさせるためのSelect
とSelectMany
は、Return
とBind
の組み合わせによって実装できます。よって、簡単な方の組み合わせとしてReturn
とBind
を使って説明していきます。Listモナド
今回自前で実装していないので説明は省略します。
Maybeモナド
Maybe<T>
クラスとそのサブクラスを以下のように定義します。abstract class Maybe<T> { public static Maybe<T> Nothing => Nothing<T>.Instance; public static Maybe<T> Just(T value) => new Just<T>(value); } sealed class Nothing<T> : Maybe<T> { private static Nothing<T> instance = new Nothing<T>(); public static Nothing<T> Instance => instance; private Nothing() { } public override string ToString() => "Nothing"; } sealed class Just<T> : Maybe<T> { public T Value { get; } public Just(T value) => Value = value; public void Deconstruct(out T value) => value = Value; public override string ToString() => $"Just({Value})"; }サブクラス関係によって、値がない状態
Nothing<T>
と値がある状態Just<T>
を表現できていることがわかります。
これに対して以下のようにReturnとBindを定めます。public static Maybe<T> Return<T>(T value) => Maybe<T>.Just(value); public static Maybe<T2> Bind<T1, T2>(Maybe<T1> x, Func<T1, Maybe<T2>> f) => x switch { Nothing<T1> _ => Maybe<T2>.Nothing, Just<T1>(var v) => f(v) };Returnは受け取った値を特に何も処理せずにJustでくるんで返します。
Bindは受け取った値がNothing型かJust型かで場合分けし、Nothing型の場合はNothingを、Just型の場合は中の値を取り出し、その値に関数を適用して、再びJust型でくるんで返します。Writerモナド
ValueとLogを持つStringWriterクラスを定義します。
class StringWriter<T> { public T Value { get; } public string Log { get; } public StringWriter(T value, String log = "") => (Value, Log) = (value, log); }これに対して以下のようにReturnとBindを定義します。
public static StringWriter<T> Return<T>(T value) => new StringWriter<T>(value); public static StringWriter<T2> Bind<T1, T2>(StringWriter<T1> sw, Func<T1, StringWriter<T2>> f) { var sw2 = f(sw.Value); return new StringWriter<T2>(sw2.Value, sw.Log + sw2.Log); }やはりReturnはくるむだけです。特に説明することはありません。
Readerモナド
Reader<TIn, TOut>は関数を保持するクラスです。 class Reader<TIn, TOut> { public Func<TIn, TOut> F { get; } public Reader(Func<TIn, TOut> f) => F = f; }これに対して以下のようにReturnとBindを定義します。
public static Reader<TIn, T> Return<TIn, T>(T value) => new Reader<TIn, T>(_ => value); public static Reader<TIn, T2> Bind<TIn, T1, T2>(Reader<TIn, T1> x, Func<T1, Reader<TIn, T2>> f) => new Reader<TIn, T2>(y => f(x.F(y)).F(y));Returnは値を受け取って、「引数を受け取るがその値を無視して常に決まった値を返す関数」を持ったReaderクラスの値を作ります。
Stateモナド
class State<TState, T> { public Func<TState, (TState, T)> F { get; } public State(Func<TState, (TState, T)> f) => F = f; }に対して、
public static State<TState, T> Return<TState, T>(T value) => new State<TState, T>(s => (s, value)); public static State<TState, T2> Bind<TState, T1, T2>(State<TState, T1> x, Func<T1, State<TState, T2>> f) => new State<TState, T2>(st => { var (newSt, v) = x.F(st); return f(v).F(newSt); });で済みます。
いずれもかなり単純に実装できていることがお分かりいただけたかと思います。実用の話
私が以前書いた記事の「C#のパーサコンビネータライブラリSpracheでML風言語のインタプリタを実装する」で紹介していたSpracheというパーサコンビネータはクエリ記法で書けるように設計されています。
たとえばif e1 then e2 else e3
という式をパースするパーサーはParser<Expr> IfParser = from ifToken in Parse.String("if").Token() from p1 in PrimaryParser from thenToken in Parse.String("then").Token() from p2 in PrimaryParser from elseToken in Parse.String("else").Token() from p3 in PrimaryParser select new IfExpr(p1, p2, p3);のように書くことができます。
Parser型は文字列をパースしてT型の値を生成するパーサーの型です。例中だとprimaryParserというパーサーから、あたかもパース結果の値を取り出してp1, p2, p3に束縛しているかのように処理を書くことができます。まとめ
- C#のクエリ記法で、Haskellのモナド & do 記法相当の機能が実現できます。(C#においてわざわざこのように書いた方がいいケースはあまりなさそうにも思えますが...)
- 紹介した各モナドは記述量的にそこまで重くなく実装できます。
- クエリ記法を使うように設計されているプロダクトとしてSpracheがあります。
今回書いたコードのリンクはこちらです。
読んでいただきありがとうございました。皆様に幸せな年末がありますように!