linq.tjs公開しました

TJSでLINQ to Objectsが使えるようになるライブラリlinq.tjsを公開しました。
https://github.com/sakano/linq.tjs

LINQ to ObjectsはC#の強力なコレクション操作ライブラリです。foreachすらないtjsではfor文を回すしかありませんでしたが、linq.tjsではより簡潔に複雑な処理を記述出来ます。

吉里吉里2/Zどちらでも使えます。

使用方法

  1. releaseページからSource codeをダウンロードします。
  2. linqフォルダに入っているlinq.tjs, linq_utility.tjs, linq__order.tjs, linq_generate.tjsを自分のプロジェクトフォルダに入れます。
  3. 最新のScriptsEx.dllをダウンロードして吉里吉里と同じフォルダに入れます。
  4. 以下のコードのようにlinq.tjsを読み込みます。
    Scripts.execStorage("linq.tjs");
    

使用例

例えば以下のように使えます。

Enumerable.range(1, 10)
    .where(function(x) { return x % 2 == 0;})
    .select(function(x) { return x * 2; })
    .forEach(function(x) { System.inform(x); });
// 4,8,12,16,20 が表示される

使い方はC#のLINQと全く同じです。簡単に説明すると次のように動作します。
1. Enumerable.range(1, 10)で1~10の整数を生成
2. .where(function(x) { return x % 2 == 0;}) で偶数のみ抽出
3. .select(function(x) { return x * 2; }) で抽出した偶数を2倍
4. .forEach(function(x) { System.inform(x); }); でダイアログを表示

文字列関数

function(x) { return ~; }が鬱陶しいのでlinq.jsを見習って文字列で指定できるようにしています。
“XXX => YYY”を”function(XXX) { return YYY; }”に変換するだけですが結構便利です。先の例は以下のようになります。

Enumerable.range(1, 10)
    .where("x => x % 2 == 0")
    .select("x => x * 2")
    .forEach("x => System.inform(x);");
// =>,4,8,12,16,20

さらに省略してYYY部分のみでも使えます。この場合は第1引数が_、第2引数が_2、第3引数が_3、…のようになります。つまり”function(_, _2, _3, _4, _5, _6, _7, _8, _9) { return YYY; }”に変換されます。

Enumerable.range(1, 10)
    .where("_ % 2 == 0")
    .select("_ * 2")
    .forEach("System.inform(_);");
// =>,4,8,12,16,20

引数の受け渡し

tjsにはjavascriptと異なりレキシカルスコープがありません。つまりfunctionの中で外にある変数が参照できずLINQを使う上で非常に不便です。なのでselectやwhereなどコールバックを引数にとる関数では、通常の引数の後ろに渡された引数をそのままコールバックに渡すようにしています。

以下の例ではargやcaptionとして引数を受け取っています。indexはここでは使っていませんが現在のインデックス番号です。

Enumerable.range(1, 10)
    .where("x, index, arg => x % arg == 0", 2)
    .select("x, index, arg => x * arg", 2)
    .forEach("x, index, caption => System.inform(x, caption);", "結果表示");
// =>,4,8,12,16,20

Enumerable.from

Enumerable.fromに配列、辞書配列、文字列を渡すとLINQが使えるようになります。

配列の場合はそれぞれの要素が順番に列挙されるだけです。また、toArray()を使うと列挙中のシーケンスを配列に変換できます。

var array = Enumerable.from([10, 20, 30])
    .select("x => x * x")
    .toArray();
// arrayは[100, 400, 900]になる

辞書の場合はkeyにキー、valueにその値が入った辞書配列として列挙されます。

Enumerable.from(%["hoge" => 100, "moge" => 200])
    .forEach(function(x) {
        System.inform(@"キー:${x.key}, 値:${x.value}");
    });
// "キー:hoge, 値:100"
// "キー:moge, 値:200"
// と表示される。

文字列の場合は先頭から1文字ずつ列挙されます。toString()でシーケンスを文字列に変換できます。

var str = Enumerable.from("StRinG")
    .where("_ == _.toUpperCase()") // 大文字のみ抽出
    .toString();
// strはSRGになる。

Enumerable.extendTo

LINQを頻繁に使う場合、Enumerable.fromを毎回呼び出すのは面倒です。今のところ配列限定ですが、一度Enumerable.extendTo(Array);を呼び出せば配列からselectなどの関数を直接呼び出せるようになります。

Enumerable.extendTo(Array);

var array = [1,2,3,4,5];
array = array.select("_ * 10").toArray();
// arrayは[10,20,30,40,50]になる

関数一覧

C#のLINQ to Objectsにある関数は全て移植済みです。その他にもInteractive Extensionsやlinq.jsなどから必要そうな関数を追加しています。

クエリ関数一覧

aggregate, aggregateWithSeed, all, any, average, buffer, concat, contains, count, defaultIfEmpty, distinct, do, elementAt, elementAtOrDefault, except, first, firstOrDefault, forEach, force, groupBy, groupJoin, indexOf, innerJoin, intersect, isEmpty, last, lastOrDefault, max, min, orderBy, orderByDescending, outerJoin, repeat, reverse, scan, scanWithSeed, select, selectMany, sequenceEqual, single, singleOrDefault, skip, skipWhile, startsWith, sum, take, takeWhile, thenBy, thenByDescending, toArray, toDictionary, toLookup, toString, trace, union, where, zip

生成関数一覧

Enumerable.from, Enumerable.empty, Enumerable.range, Enumerable.repeat, Enumerable.return, Enumerable.matches, Enumerable.random, Enumerable.randomInt, Enumerable.generate

Enumerable.matches

linq.tjs独自の関数としてEnumerable.matchesを追加しています。TJSではRegExpクラスで正規表現を使えますが癖があっていまいち使いづらいです。Enumerable.matchesでは正規表現のマッチ結果を扱えます。

Enumerable.matches(/([a-zA-Z]+)=([0-9]+)/g, "foo=100 bar=200")
    .forEach(function (matches) {
        System.inform(@"0:${matches[0]}, 1:${matches[1]}, 2:${matches[2]}");
    });
// "0:foo=100, 1:foo, 2:100"
// "0:bar=200, 1:bar, 2:200"
// が表示される

第1引数は使用する正規表現です。gフラグは必須です。第2引数は正規表現の対象となる文字列です。
このようにするとRegExp.matchesの結果が列挙されます。もう正規表現を使うのにfor文は必要ありません。

RoslynのCode Analyzerを作ってみるメモ

Visual Studio 2015ではAnalyzeなるものを自作して独自のエラーや警告を出せます。Metro.cs #1でそんな話をしていたので試してみた。

参考資料

VS2015のRoslynでCode Analyzerを自作する(ついでにUnityコードも解析する)
新しいコンパイラー“Roslyn”を用いたプログラミングを体験!
Roslyn を使用した API 向けライブ コード アナライザーの作成
Roslyn アナライザーへのコード修正の追加
Working with Types in Your Analyzer

必要な情報探すのにかなり時間かかった。検索するとほとんど英語で出てきて大変。上4つは貴重な日本語情報でまとまっているので読むべき。

準備

Visual Studio 2015をインストールする。このとき共通ツールのVisual Studio拡張性ツールもインストール。
それと.NET Compiler Platform SDKもインストールする。

プロジェクトを作る

メニューの ファイル>新規作成>プロジェクト を開いて Visual C#>Extensibility>Analyzer with Code Fix を選んで新規作成する。
そうすると説明書(ReadMe.txt)とサンプル(DiagnosticAnalyzer.cs, CodeFixProvicer.cs)が入ったプロジェクトが自動作成される。

コードを書く

DiagnositcAnalyzer.csで問題箇所を見つけてエラーや警告を出す。CodeFixProvider.csではその修正案を作る。
詳しくは参考資料を参照。Syntax Visualizerとデバッグ実行のウォッチ式などを見ながら気合いで何とかするのがデフォっぽい。
今日は調べるだけで力尽きたので、とりあえず非ジェネリックのIEnumearbleを見つけたら警告を出すAnalyzerを書いた。

using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace ForbidIEnumerable
{
    [DiagnosticAnalyzer(LanguageNames.CSharp)]
    public class ForbidIEnumerableAnalyzer : DiagnosticAnalyzer
    {
        public const string DiagnosticId = "ForbidIEnumerable";

        private static readonly LocalizableString Title = "Forbit to use IEnumerable";
        private static readonly LocalizableString MessageFormat = "IEnumerable is forbidden. Please use IEnumerable<T>.";
        private static readonly LocalizableString Description = "IEnumerable error {0}";
        private const string Category = "Prohibition";

        private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
        { get { return ImmutableArray.Create(Rule); } }

        public override void Initialize(AnalysisContext context)
        {
            context.RegisterSyntaxNodeAction(syntaxContext =>
            {
                var declarationSyntax = (VariableDeclarationSyntax)syntaxContext.Node;
                var typeInfo = syntaxContext.SemanticModel.GetTypeInfo(declarationSyntax.Type);
                if (typeInfo.ConvertedType.SpecialType == SpecialType.System_Collections_IEnumerable) {
                    syntaxContext.ReportDiagnostic(Diagnostic.Create(Rule, syntaxContext.Node.GetLocation()));
                }
            }, SyntaxKind.VariableDeclaration);
        }
    }
}

実行してみる

デバッグ実行するとアナライザを読み込んだVisualStudioが起動されるのでそれでテストできる。
完成したものを実際に使うには組み込むプロジェクトの「参照」を右クリックして「アナライザーの追加」からビルドされたdllを読み込む。

roslyn-code-analyzer
実行してみたところ。警告が出て何か変なことを教えてくれる。なぜか警告メッセージが二重で出ているが原因は不明。