20191205のC#に関する記事は14件です。

C#(csc.exe) - トレイアイコンをクリックするとToast通知を出すサンプル

サンプルアプリの仕様概要

Windows10で動作します。ただしvisuals studio等でwindows sdk入れてないとwindows.winmdファイルがなくてコンパイルできないっぽい

  • アイコンを左クリックで通知が出ます。(連打すると通知が延々と何度も出るので注意)
  • アイコンを右クリック→Exitで終了できます。

image.png

ソースコード

ToastNotificationManager.CreateToastNotifier("Microsoft.Windows.Computer");のところはかなり強引です。1

using 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.bat
csc /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" %*

参考記事(参考サイト)

  1. Win向け通知アプリにトーストを使う - Qiita
  2. Windows 10 でトースト通知を飛ばす - みかづきメモ

ボタンの追加とイベント処理

参考記事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. 詳しくは参考記事1のコードの説明を参照ください。 

  2. UTF-8を使うべきだと思いますが、PC環境の私的な事情でShift_JISにしています・・ 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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++;
}
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「MonKey - Productivity Commands」でパラメータを渡す

PONOS Advent Calendar 2019の6日目の記事です。

昨日は私の「MonKey - Productivity Commands」のカスタムコマンドでさらにUnity上の作業を効率化するでした。

はじめに

assetstore.png
MonKey - Productivity Commands - Asset Store

本記事はPONOS Advent Calendar 2019の以下の記事の続編となります。

今回はコマンドにパラメータを渡して実行する方法について紹介します。

なお、

  • 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}\"です。");
    }
}

このコマンドをコマンドパレットで開くと以下のような表示になります。
パラメータが設定されたコマンドは、コマンド名の左側に吹き出しアイコンが表示されており、パラメータの有無を判別できます。
ParameterCommand_01.png
このコマンドを選択すると、以下のようなパラメータ入力画面が表示されます。
ParameterCommand_02.png
任意のパラメータ(今回はint型のパラメータなので整数値)を入力して実行します。
ParameterCommand_03.png
コマンドに渡されたパラメータの値を使用して、処理が実行されました。

利用可能なコマンドパラメータの型

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()}");
}

入力された矩形情報をコンソール出力するだけの、シンプルなコマンドです。
さて、試しにこのコマンドを実行してみますが…
ParameterCommand_04.png
パラメータの入力後に「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;
    }
}

では、もう一度実行してみましょう。
ParameterCommand_05.png
ParameterCommand_06.png
ParameterCommand_07.png
入力した文字列から矩形情報を作成し、コンソールに出力することができました!

おわりに

コマンドパラメータを指定してコマンドを実行することで、柔軟に処理を実行することができます。
MonKeyの優れている点の一つだと思うので、是非活用していきましょう。

明日は@karizumaiさんです!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.md

public interface IDim
{
    //+1して返すだけの関数
    int Inc(int x) => x+1;
}

これによって何がうれしいかといえば、

  1. interfaceにmethodまたはpropertyを後から足しても、既存のclass/structがコンパイルエラーにならない
  2. 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だけがターゲットだとかなら使いましょう。

詳しい方がいればコメント等で訂正、補足等してくれると幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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していく。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

迷路ゲームにランキング機能をつけるお話

N高アドカレ16日目の記事ですー
データベース一切わかんない人がオンラインランキングの実装を頑張った話になります。

はじめに

軽く自己紹介

N高の江坂プロクラの高三の受験生です。
大学に無事AOで合格して時間ができたのでアドカレ参加。
UnityとBlenderでゲーム作ったり色々してます。
Webとサーバーサイドはほとんどわかんないです…

ゲームの説明

迷路ゲーム自体は自作キャラがモデリングできたからそれを歩かせてゲームにしたいという思いから作ったもので、シンプルなものです。
スクリーンショット 2019-12-03 13.55.58.png
敵キャラのゴーストを避けながらゴールを目指すゲームです。
このゲームにオンラインランキング(タイムアタックのハイスコア)をつけるときの話です。

スクリーンショット 2019-12-04 15.21.26.png
自信作の自作キャラ

どうやってスコアを保存するか

はじめにどうやってクリアタイムを保存させようかと考えました。
ゲームに記憶させるのか、データをサーバーに保存するか…と色々考えていた時に、こちらの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の非表示と同時にクリア時の時間をデータベースに送っています。

これで、データベースにクリア時の時間が記録されました。
スクリーンショット 2019-12-03 15.03.20.png

クリアタイムの取得

クリアタイムをデータベースから取得して表示させます。

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にして表示させることができます。
(もうちょっといいコードがかけそうな気がしたけど、私のコーディング力ではこれぐらいしかかけなかった…)

スクリーンショット 2019-12-04 15.16.24.png
ゲームクリア画面
Nextボタンを押すと
スクリーンショット 2019-12-04 15.16.31.png
ランキング表示させることができた!

追記

ゲームにランキングが追加できたので、しばらくテストプレイしていると、なぜか大量のAPIリクエストを送っていた…
スクリーンショット 2019-12-04 15.44.06.png
この日と次の日はすごい数送ってました…その数約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上で遊べるようにしようか検討中。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Xamarin】SecKeyChain備忘録(基本的なパスワードの取扱)

SecKeyChain周り、触れば触るほどややこしいので分かったこと取り敢えずまとめ。

  • アプリケーションパスワード
  • インターネットパスワード

の形で、ユーザーが入力したパスワードを端末に保存できる。

※多分他にもあるっぽいですが今の所この2つしか活用出来てません……しかも異なる仕様だと思ってた部分が同じだったので(後述)違いもあまりわからない。。

パスワード保存する

何はともあれパスワードを保存する

SecKeyChain.AddGenericPassword(serviceName, accountName, password);
SecKeyChain.AddInternetPassword(serverName, accountName, password);

AddGenericPasswordはアプリケーションパスワードとして、AddInternetPasswordはインターネットパスワードとしてキーチェーンアクセスに保存する。

serviceName,serverName,accountNameはいずれもstring
serviceNameserverNameの違いこそあれど、結局はキーチェーンアクセスに表示される名前なのであまり差異はない(と思う)。

ちなみにブラウザなどで登録したパスワードについてはこの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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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()直呼び出し程度ならば、コンパイルの段階で自動検出してくれるので、記述の必要は多くの場合は無い。
しかし、自動検出されなかった場合、ネイティブバイナリ実行時にMissingMetadataExceptionTypeLoadExceptionが発生する
この時にrd.xmlを書くことを検討する。

書き方

  • Directivesを頂点として、その下にApplicationLibrary要素を追加する
    • 現時点ではどちらでも入れて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のネイティブバイナリと変わらない)するので、将来的に使いやすいものになると良いと思う。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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倍じゃん!
やっぱり無駄なものを削りましょう:military_medal::relaxed:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「MonKey - Productivity Commands」のカスタムコマンドでさらにUnity上の作業を効率化する

PONOS Advent Calendar 2019の5日目の記事です。

昨日は@nimitsuさんのGameLift RealTimeServerで遊んでみよう for Unity(AWS設定編)でした。

はじめに

本記事はPONOS Advent Calendar 2019の2日目の記事である「MonKey - Productivity Commands」のコマンド操作でUnity開発を効率化するの続編となります。
assetstore.png
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\"を実行したログです。");
    }
}

SampleCommand01.png
MonKeyのコマンドパレット上に表示されました!
image.png
コマンドを実行すると、メソッド内に記述した処理が実行されています。
このように、メソッドをMonKeyのコマンド化するのは非常に簡単です。

使用例:選択中のGameObjectの階層パスをコンソールに出力する

さて、ここからはより実用的なカスタムコマンドを作成してみます。
今回作成するのは「選択中のGameObjectの階層パスをコンソールに出力する」コマンドです。
Example.png
例えば、上のような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()
{
}

SampleCommand02_01.png
設定した「ヘルプ」と「短縮されたコマンド名」がコマンドパレット上に表示されていることがわかります。
また、短縮されたコマンド名である「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());
}

SampleCommand02_02.png
この選択状態でコマンドを実行すると、
SampleCommand02_03.png
このように、各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()
{

SampleCommand02_04.png
GameObjectを選択していない状態でコマンドを選択すると…
SampleCommand02_05.png
ご覧のように「Select at least one GameObject」の警告が表示され、コマンドを実行することができません。

コマンドの製作者が自身だけで使用する分には使い方を気をつければいいので、検証条件の定義は必須ではありませんが、他者へ共有する予定がある場合は検証条件を正しく定義して正しい状況で使用してもらえるようにしたいですね。

おわりに

カスタムコマンドを作成してMonKeyが実行できる機能を充実させることで、Unity上の作業を更に効率化することができます。

なお、Unityエディタ上からstaticメソッドを実行するにはUnityEditor.MenuItem属性でメニューアイテム化する、という方法もありますが、MonKeyなら処理の実行条件の検証を行ったり実行時に引数を与えることができるので、メニューアイテムとして実行するよりも柔軟な処理の実行が可能となります。
そういった用途で実行したい処理がある場合にはMonKeyのカスタムコマンド作成を検討してみてください。

さて、PONOS Advent Calendar 2019は6日目も続けて私@e73ryoが担当する予定です。
次回は引数を与えてカスタムコマンドを実行する方法について紹介したいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.cs
private 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.cs
public 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.cs
public 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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VR剣戟ゲーのための自作当たり判定処理

はじめに

SEKIROみたいな剣の斬り合いがVRでやりたい!!!!

こんにちは、ZeniZeniです。
昨今、Sword Of GargantuaSword Master VRなど、面白い良VR剣戟ゲームが増えてきました。
それらをプレイしてると、自分の理想の剣戟ゲームというのを作りたい欲がふつふつと湧き上がってきます。
というわけで絶賛開発中です。

今回は、VRで剣戟ゲームを作るための第一歩として、剣を高速で振っても、剣の当たり判定と剣同士が交差した座標を取得できる機能を実装しようと思います。
下の動画のようなことができるようになります。

これは、剣が交差した瞬間の座標を取得して、その座標から火花のエフェクトを発生させています。

実装方法

実装方法ですが、コリダーは使わずにやっています。
なぜかというと、高速な物体同士の当たり判定は、コリダーだと簡単にすり抜けてしまうからです。
下の動画くらいの速度が限界でした。

プラス、座標を取得するために小さいコリダーを大量に配置していたので、剣を変えるときは設定がめんどくさいですし、パフォーマンスもよろしくなさそうです。

それではコリダーを使わない当たり判定の実装方法を考えていきましょう。
まず、剣と剣がぶつかった判定をどうとるかを考えてみます。
これは三次元空間において、ある線分と線分の距離が一定値以下になったときを考えればよさそうです。

剣同士の距離の導出

計算方法

それでは、$点(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$となります。
剣同士の距離の導出.png

計算がうまくできないとき

上記の導出では、線分同士が同一平面上にあるときには距離$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}$を代入したものとなります。

絶対に交差しないとき

剣が絶対に交差しない状況のときは上のような計算をするのは無駄なので、そのような状況は早い段階ではじきましょう。
剣が絶対に交差しない状況は、下図のようなときです。
線分同士の交差判定例01.png
これにz座標の判定も加わります。

実際のコード

それでは実際に書いた線分同士の距離と交点を導出するコードがこちらです。

線分同士の距離とその交点を同時に取得したかったので、IntersectionInfoという構造体を作っています。
線分はLineという構造体を作成していて、剣の刃の部分の根本と剣先の2点を設定してください。

IntersectionChecker.cs
using 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となります。

下図のような感じで設定してください。
線分同士の交差判定実例.png
後は、GetIntersectionInfo関数を呼んで得られたIntersectionInfoのMidPointで火花等のエフェクトを発生させればよいのです。

剣を速く振ったときだけ呼びたい場合、まず剣の振る速度を求める方法を考えると思います。
剣の振る速度は、SteamVR SDKアセットに入っている、VelocityEstimatorというコンポーネントを使うことをお勧めします。

開発、執筆にあたり、下記のサイト様を参考、引用させていただきました。

  1. 直線と直線の距離を与える公式
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

クエリ記法を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 allPossibility

sgnは-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 35 

Haskellの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 => Nothing

Writerモナド

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 15

main関数を見てみると、 readerという関数に15が渡されています。

readerの中では、引数を2倍する処理、引数を文字列化して文字列の長さを得る処理、100を足す処理の3つの処理全てに15が渡されます。
その結果、twiceには30が、lenには2が、plusHundredには115が入ります。

出力は以下の通りです。

twice: 30, length: 2, plus 100: 115

C#での例

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: 115

Stateモナド

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としたときに拡張メソッド方式でSelectSelectManyを書くと、各メソッドのシグネチャと返り値の型は

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のモナドを定義するにはreturnbindを定義すればよいのですが、これらは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記法のように機能をさせるためのSelectSelectManyは、ReturnBindの組み合わせによって実装できます。よって、簡単な方の組み合わせとしてReturnBindを使って説明していきます。

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があります。

今回書いたコードのリンクはこちらです。
読んでいただきありがとうございました。皆様に幸せな年末がありますように!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 allPossibility

sgnは-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 35 

Haskellの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 => Nothing

Writerモナド

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 15

main関数を見てみると、 readerという関数に15が渡されています。

readerの中では、引数を2倍する処理、引数を文字列化して文字列の長さを得る処理、100を足す処理の3つの処理全てに15が渡されます。
その結果、twiceには30が、lenには2が、plusHundredには115が入ります。

出力は以下の通りです。

twice: 30, length: 2, plus 100: 115

C#での例

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: 115

Stateモナド

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としたときに拡張メソッド方式でSelectSelectManyを書くと、各メソッドのシグネチャと返り値の型は

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のモナドを定義するにはreturnbindを定義すればよいのですが、これらは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記法のように機能をさせるためのSelectSelectManyは、ReturnBindの組み合わせによって実装できます。よって、簡単な方の組み合わせとしてReturnBindを使って説明していきます。

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があります。

今回書いたコードのリンクはこちらです。
読んでいただきありがとうございました。皆様に幸せな年末がありますように!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む