お米 is ライス

C#やらUnityやらを勉強していて、これはメモっといたほうがええやろ、ということを書くつもりです

ロビンソンはタイムリープの歌だった説(構成編)

ロビンソンとは言わずもがなスピッツの御三家のうちの一角であり、その中でも特に歌詞・音楽性ともに話題になることの多い曲である。
この楽曲をリリースした頃のスピッツはイマイチ売れ切らずにくすぶっていた上に、特に目立ったプロモーション活動や有力なCMやドラマとのタイアップもなかったにも関わらずなぜか最終的にミリオンヒットをたたき出した化け物曲であり、それほどまでに不思議な魅力を持っている曲なのである。

とまあお約束の礼賛はさておき、何番煎じになるか考えるだけでも気の遠くなるようなこの楽曲の歌詞解釈ですが、自分なりに割と筋がいいんではないかという答えを見つけたのでここにメモしておきたいと思います。
ポイントは文の区切り方でした。Aメロの前半・後半というようにブロックごとに意味を読み取りがちなんですが、草野さんの歌詞って結構文脈が複数のブロックにまたがってることが多いんですよね。
(「楓」なんかもそうで、あれも2番のAメロとBメロは繋げて解釈するのが正解なんだと思っています。)

というわけで歌詞に適切な区切りをつけるところから始めましょう。

歌詞に区切りをつける

といっても単に文法通りに句読点を付けるだけではなく、ちゃんと”意味の通る”文章にすることを心がけます。
(草野さんの歌詞はファンであればあるほど、はっきりとした意味を見出すのを諦めてしまいがちです)

1番Aメロ前半

新しい季節は
なぜかせつない日々で
河原の道を自転車で
走る君を追いかけた

「新しい季節はなぜか切ない日々で、河原の道を自転車で走る君を追いかけた」
この部分だけに句読点を付けるとこうなりますが、まだ句点は入りません。この文章は後にくる名詞を説明するいわば名詞節、英語でいうところのthat節にくくられた部分なのです。
というわけで次に行きましょう。

1番Aメロ後半

思い出のレコードと
大げさなエピソードを
疲れた肩にぶらさげて
しかめつらまぶしそうに

1番Aメロ前半はすべてこの「思い出のレコードと大げさなエピソード」を説明している文章です。
つまりはこの部分までで「(新しい季節はなぜか切ない日々で、河原の道を自転車で走る君を追いかけた)思い出のレコードと大げさなエピソード」という名詞を形成しています。

次の「疲れた肩にぶらさげてしかめつらまぶしそうに」ですが、ぶらさげているのは当然直前のレコードとエピソードになります。
レコードとエピソードを肩にぶらさげた状態で、何かをまぶしがって顔をしかめているのです。
そしてこのブロックの最後は「そうに」と連用形になっていますから、まだ文章としては終わっていません。
というわけでさらに次のブロックへと続きます。

1番Bメロ

同じセリフ同じ時
思わず口にするような
ありふれたこの魔法で
つくり上げたよ

前ブロックの「まぶしそうに」という形容はこの「思わず口にする」という動詞にかかっています。
わかりやすくするため少し順序を入れ替えると、「同じセリフ同じ時、しかめつらまぶしそうに思わず口にするような」となり、直後の「ありふれたこの魔法」を説明しています。
そして「この魔法で」は次に来る「作り上げたよ」という動詞の補語となっています。

ここまでの文章をまとめると下記のようになります。
「(((新しい季節はなぜか切ない日々で、河原の道を自転車で走る君を追いかけた)思い出のレコードと大げさなエピソードを疲れた肩にぶらさげてしかめつらまぶしそうに)同じセリフ同じ時思わず口にするような)ありふれたこの魔法で作り上げたよ」

括弧が深くてわけがわからないと思いますが、おそらくこれで区切り方はあっていると思います。

恐ろしいことに気が付いたでしょうか?
そうです。この歌詞はここまででようやく1つのまとまった文節の動詞と補語になっているのです。
さらに恐ろしいことに、なんとまだ終わりではありません(!)。
なぜなら「何を」作り上げたかという目的語がまだ示されていません。

1番サビ

誰も触れない二人だけの国
君の手を離さぬように
大きな力で空に浮かべたら
ルララ宇宙の風に乗る

何を作り上げたのか。それはこの「誰も触れない二人だけの国」です。
なんとAメロとBメロはこの「二人だけの国」を説明するためのまとまった文節だったのです。

そしてここまででようやく目的語であり、さらに後の歌詞へと続きます。

「君の手を離さぬように大きな力で空に浮かべたら」は英語でいえばWhenやIfでくくられた文節ですね。
そしてここまでの文章の本体となる動詞が「ルララ宇宙の風に乗る」となります。

適切な区切りをつけるといいましたが、驚くべきことにここでようやく一区切り、一つの文章なのです。
わかりやすく簡略化すると下記のようになります。

『(君を追いかけた思い出をぶらさげて思わず口にするような)魔法で作り上げた二人だけの国を空に浮かべたら宇宙の風に乗った』



そりゃあ歌詞の解釈がまともにできんわけだ。今までブツ切りの文章を読んでいたんだから。




では同じように2番の歌詞も区切りをつけていきましょう。
といっても2番は1番と比べるとかなりわかりやすいものになっています。

2番Aメロ

片隅に捨てられて
呼吸をやめない猫も
どこか似ている抱き上げて
無理やりに頬寄せるよ

いつもの交差点で
見上げた丸い窓は
うす汚れてる
ぎりぎりの三日月も僕を見てた

このブロックは細かく三つの文章になっています。
「~~無理やりに頬寄せるよ」
「いつもの交差点で見上げた丸い窓はうす汚れてる」
「ぎりぎりの三日月も僕を見てた」
がそれぞれ独立した文章になっており、半分解釈の域ではありますがBメロの「夢のほとり」の情景描写となっています。

2番Bメロ

待ちぶせた夢のほとり
驚いた君の瞳
そして僕ら今ここで
生まれ変わるよ

このブロックもこれだけで一つの文章になっていますが、1番Bメロと同じようにサビに登場する「二人だけの国」を説明する文節でもあります。

2番サビ

誰も触れない二人だけの国
終わらない歌ばらまいて
大きな力で空に浮かべたら
ルララ宇宙の風に乗る

「二人だけの国」は1番にも登場しましたが、ここでは「そして僕ら今ここで生まれ変わる」場所として説明されています。
あとの構成は1番と同じですから省略します。
2番Aメロ後半から曲終わりまでの歌詞を大雑把に訳すと下記のようになります。

『(いつもの交差点という)夢のほとりで待ち伏せて出会った君は驚いた顔をした』
『そして僕たちは二人だけの国に生まれ変わり、その国を空に浮かべたら宇宙の風に乗った』

歌詞の構成

ここまで長くなりましたが、歌詞の構成をまとめるとこのようになります。

1番
『(君を追いかけた思い出をぶらさげて思わず口にするような)魔法で作り上げた二人だけの国を空に浮かべたら宇宙の風に乗った』

2番
捨て猫を抱き上げて頬を寄せた』
『いつもの交差点で見上げた丸い窓はうす汚れてる』
『ぎりぎりの三日月も僕を見てた』
『(上3つで描写したような)夢のほとりで待ち伏せて出会った君は驚いた顔をした』
『そして僕たちは二人だけの国に生まれ変わり、その国を空に浮かべたら宇宙の風に乗った』

自分としてはかなり正確に構成を読み取れたと思っていますが、異論は認めます。
というか、そもそも草野さんも言っているようにロビンソンの歌詞解釈に答えなんかないのかもしれない。
しかし、このように構成したうえで一つ一つ意味を読み取っていくと確かに何らかのストーリーが見えてくる気がしています。

ここまで読んでいただいた方は私がどういう解釈をこれから行おうとしているか薄々わかっているかもしれませんが、解釈については次回詳しく述べようと思います(やる気があれば)。

【C#】SpracheでJsonのパーサを実装する方法を1行ずつ解説

下記のサンプルをパースすることを目的とする
サンプルはJSON入門 - とほほのWWW入門様から引用させていただいた(いい感じにいろいろなパターンが詰め込まれているので非常に便利でした)

{
  "color_list": [ "red", "green", "blue" ],
  "num_list": [ 123, 456, 789 ],
  "mix_list": [ "red", 456, null, true ],
  "array_list": [ [ 12, 23 ], [ 34, 45 ], [ 56, 67 ] ],
  "object_list": [
    { "name": "Tanaka", "age": 26 },
    { "name": "Suzuki", "age": 32 }
  ]
}

パーサの書き始め方

Spracheの使い方はあいわかった、じゃあ書いてみるかと思ってもパーサを書いたことが無いから何から始めていいのかわからんかった。
とりあえずパースしようとしている構文がどんな要素から出来ているかを把握する。
そして最小の要素のパーサから書き始めて、一階層ずつ大きくしていくといい感じにするすると実装できた。多分これが正解。
Jsonの最小要素は「数値・文字列・真偽値の値」なので、まずはこれをパースするコードを書いていく。
配列の中身が配列だったりと、一階層ずつ大きくできない(配列のパーサの定義で配列のパーサが必要になる)こともあるが、そういう場合は飛ばしておいて後で追加する感じにすればよい。

数値・文字列・真偽値をパース

数値

  • 数字(Digit)が
  • 少なくとも一文字以上連続している部分(AtLeastOnce())の
  • 文字列(Text())をintに変換した値を返す

小数とか指数表記の実装はさぼる

    public static Parser<object>? numParser = from num in Parse.Digit.AtLeastOnce().Text()
                                              select int.Parse(num) as object;

文字列

  • ダブルクォーテーションで始まり(from open in Parse.Char('"'))、
  • ダブルクォーテーションで終わる(from close in Parse.Char('"'))部分の
  • 文字列(Parse.CharExcept('"').AtLeastOnce().Text())を返す

文字列の中にダブルクォーテーションが入ってる場合は死ぬ

    public static Parser<object>? strParser = from open in Parse.Char('"')
                                              from str in Parse.CharExcept('"').AtLeastOnce().Text()
                                              from close in Parse.Char('"')
                                              select str;

真偽値

  • ダブルクォーテーション無しでtrueかfalsetという文字列であれば(Parse.String("true").Or(Parse.String("false")))
  • 文字列を真偽値に変換した値を返す
    public static Parser<object>? boolParser = from b in Parse.String("true").Or(Parse.String("false")).Text()
                                               select bool.Parse(b) as object;

null

  • ダブルクォーテーション無しでnullという文字列であれば(Parse.String("null"))
  • nullを返す

(nullだけだと型を推論できないので、オブジェクトとして用いる型(Dictionary)を使っておく)

    public static Parser<object>? nullParser = from _ in Parse.String("null").Token()
                                               select (Dictionary<string, object>?)null;

配列をパース

次に大きな要素は配列の値。

  • 角括弧で始まり(from open in Parse.Char('['))
  • 角括弧で終わる(from close in Parse.Char(']'))部分の中に
  • 任意の型の要素が(numParser.Or(strParser).Or.....)
  • カンマ区切りで並べられている(parser.Token().DelimitedBy(Parse.Char(','))ものを
  • 配列として返す

配列の要素が配列だったりオブジェクトだったりするのでarrayParserの定義の中にarrayParserを使ってたり、まだ実装していないオブジェクトのパーサ(objParser)を使ってたりとちょっとややこしい

    public static Parser<IEnumerable<object>>? arrayContent(this Parser<object>? parser) => parser.Token().DelimitedBy(Parse.Char(',').Token());
    public static Parser<IEnumerable<object>>? arrayParser = from open in Parse.Char('[').Token()
                                                             from array in
                                                                 numParser
                                                                 .Or(strParser)
                                                                 .Or(boolParser)
                                                                 .Or(nullParser)
                                                                 .Or(arrayParser)
                                                                 .Or(objParser)
                                                                 .arrayContent()
                                                             from close in Parse.Char(']').Token()
                                                             select array;

KeyとValueの組をパース

値が全て(オブジェクトがまだだけど)パースできたので、次はキーと値の組をパースする

  • セミコロンで区切られた部分の(Parse.Char(':'))
  • 左側の文字列をKeyとし(from key in strParser)
  • 右側の任意の型の値をValueとした(from value in numParser.Or(strParser).Or.....)
  • KeyValuePairを返す
    public static Parser<KeyValuePair<string, object>>? keyValueParser = from key in strParser.Token()
                                                                         from separator in Parse.Char(':').Token()
                                                                         from value in
                                                                             numParser
                                                                             .Or(strParser)
                                                                             .Or(boolParser)
                                                                             .Or(nullParser)
                                                                             .Or(arrayParser)
                                                                             .Or(objParser).Token()
                                                                         select new KeyValuePair<string, object>((string)key, value);

オブジェクトをパース

ここまでやればJson全体をパースすることができる。

  • 中括弧で始まり(from open in Parse.Char('{'))
  • 中括弧で終わる(from close in Parse.Char('}'))部分に
  • カンマ区切りで列挙されているKeyとValueの組を要素とする(from keyValues in keyValueParser.Token().DelimitedBy(Parse.Char(','))
  • Dictionaryを返す
    public static Parser<Dictionary<string, object>>? objParser = from open in Parse.Char('{').Token()
                                                                  from keyValues in keyValueParser.Token().DelimitedBy(Parse.Char(',').Token())
                                                                  from close in Parse.Char('}').Token()
                                                                  select new Dictionary<string, object>(keyValues);

Jsonパーサの出来上がり

出来上がったオブジェクトのパーサ(objParser)がそのままJsonのパーサとなる。
あとはobjParserにパースしたいJson形式のテキストを食わせてやれば辞書型にパースしてくれる。

var obj = objParser.Parse(jsontext);

かなりしっかりとしたJsonのパーサがわずか50行程度(しかも体裁整えるための改行こみこみ)で出来上がってしまった。Sprache(というか構文解析ライブラリ?)めちゃくちゃ便利ですね。

補記

改行とか空白があるので要所要所でToken()するのを忘れないように。

自分がコレクションをいい感じに処理する目的でしかLinq構文を使ったことが無かったので混乱した(from a in arrayA from b in arrayB select (a,b)みたいな感じになると期待した)のだけど、

  • Parse関数を実行したときに文字列を1文字ずつ処理するオブジェクト(Inputクラス)が生成される
  • from * in *されるごとにその塊でパースした結果を変数に保持
  • パースした結果と文字列の処理状況を次のfrom * in *に渡す

という流れを実現するためのSelectManyの仕組みを使っている(ので字義通りにSelect"Many"しているわけではない)と理解したらしっくりときた。
Linqまじ強力だ。

Expressionで辞書に登録されていればそれを返すし、登録されていなければデフォルト値を登録したうえでそれを返すコード

Expression完全に理解した

            private static readonly ParameterExpression dicParameterExpression = Expression.Parameter(typeof(Dictionary<string, object>), "dic");
            private static readonly MethodInfo containsKeyMethod = typeof(Dictionary<string, object>).GetMethod("ContainsKey");
            private static readonly MethodInfo addMethod = typeof(Dictionary<string, object>).GetMethod("Add");
            private static readonly MethodInfo debugLogErrorMethod = typeof(Debug).GetMethod("LogError", new[] {typeof(object)});
            
            //(下記と同等のコード)
            // T value;
            // value = default(T);
            // if (dic.ContainsKey(key))
            //     value = dic[key];
            // else
            //     dic.Add(key, value);
            // return value;
            var keyExpression = Expression.Constant(statementString);
            var valueExpression = Expression.Parameter(typeof(T), $"{statementString}_value");
            var getValueExpression = Expression.Convert(Expression.Property(dicParameterExpression, "Item", keyExpression), typeof(T));
            expression = 
                Expression.Block(
                    typeof(T),
                    new[] { valueExpression },
                    Expression.Assign(valueExpression, Expression.Default(typeof(T))),
                    Expression.IfThenElse(
                        Expression.Call(dicParameterExpression, containsKeyMethod, keyExpression),
                        Expression.Assign(valueExpression, getValueExpression),
                        Expression.Block(
                            Expression.Call(dicParameterExpression, addMethod, keyExpression, Expression.Convert(valueExpression, typeof(object))),
                            Expression.Call(debugLogErrorMethod, Expression.Constant($"{statementString}が登録されていなかったためデフォルト値を登録して使用します")))
                    ),
                    valueExpression
                );

C#のExpression(式木)を使って文字列で与えられた条件文をラムダ式に変換する

やること

本体の関数はCreateExpressionで、下記のようなことを行っている

  • 一番外側のWhitespaceは除外
  • 式全体が括弧で囲まれている場合、不要な括弧なので除外
  • 一番優先度の低い演算子を探して、本体の演算子とする
    • 括弧で囲まれた位置にある演算子は無視(左辺、右辺のExpressionを作成するときに一番外側の括弧になってたときに初めて本体になれる)
  • 本体の演算子がなかった場合は単項演算子のため、単項演算子としてExpressionを作成する(CreateUnaryExpression)
  • 本体の演算子の左辺、右辺を切り取ってExpressionを作成する(再帰的にCreateExpressionを実行)
  • 本体の演算子、左辺、右辺を二項演算子の各項としてExpressionを作成する

何も見ずにやったので最適なやり方ではないと思うけど、割といい線行ってるんじゃないかなという感じ

補記

Spanを使ってなるべくstringを作成しないように気を付けていたが、
Unityだと.NET Standard 2.0を使っていて、int.Parse関数にSpanオーバーロードが無かったり(.NET 5.0だとある)、dictionaryから値を取るときにstringにしないといけなかったりしたため、結局stringになってしまっている。。。
Expression.***でExpressionを生成するときにallocationしている量に比べると少ない(Expression生成の1割程度)ので無視することにしよう
「((1 + 1) * 3 > a)」を1回Expressionにすると1kBぐらいallocationされてるが、多いのか少ないのか:thinking_face:

ソースコード

    private void Start()
    {
        var statement = "((1 + 1) * 3 > a)";
        var expression = CreateExpression(statement.AsSpan());
        Debug.Log(Expression.Lambda<Func<Dictionary<string, object>, bool>>(expression, dicParameterExpression).Compile().Invoke(dictionary));
    }

    private const char startbracket = '(';
    private const char endbracket = ')';
    private const char whitespace = ' ';

    //式木の中で使いたい変数はdictionaryに入れておく
    private Dictionary<string, object> dictionary = new Dictionary<string, object>()
    {
        {"a", 6 },
        {"b", 2 },
        {"c", 3 },
        {"flag", true },
    };

    private static readonly ParameterExpression dicParameterExpression = Expression.Parameter(typeof(Dictionary<string, object>), "dic");

    //使用できる演算子(優先度順に並べる)
    private static readonly List<(char[] operatorChars, ExpressionType type, Type parameterType)> sortedBinaryOperatorList = new List<(char[], ExpressionType, Type)>()
    {
        (new char[]{'*'}, ExpressionType.Multiply, typeof(int)),
        (new char[]{'+'}, ExpressionType.Add, typeof(int)),
        (new char[]{'>'}, ExpressionType.GreaterThan, typeof(int)),
        (new char[]{'&'}, ExpressionType.AndAlso, typeof(bool)),
        (new char[]{'|'}, ExpressionType.OrElse, typeof(bool)),
    };


    /// <summary>
    /// sourceStatementからExpressionを作成
    /// </summary>
    public Expression CreateExpression(ReadOnlySpan<char> sourceStatement)
    {
        var statement = sourceStatement;

        //statementの両端のWhitespaceと括弧は除外しておく
        statement = TrimWhiteSpace(statement);
        while(statement[0] == startbracket && statement[statement.Length - 1] == endbracket)
            statement = statement.Slice(1, statement.Length - 2);
        statement = TrimWhiteSpace(statement);

        //statementを1文字ずつ順番に見ていき、本体となる演算子を探す
        int bracketDepth = 0;
        var useBinaryOperatorIndex = -1;
        var operatorStartIndex = -1;
        for(var i = 0;i < statement.Length;i++)
        {
            var tmpStatement = statement.Slice(i, statement.Length - i);

            //括弧の深さを保持する
            if (tmpStatement[0] == startbracket)
            {
                bracketDepth++;
                continue;
            }
            else if (tmpStatement[0] == endbracket)
            {
                bracketDepth--;
                continue;
            }

            //括弧の中だった場合は飛ばす
            if (bracketDepth != 0)
                continue;
            //Whitespaceだった場合は飛ばす
            if (tmpStatement[0] == whitespace)
                continue;

            //次のWhitespaceまでで区切った区間を取得
            var whitespaceIndex = tmpStatement.IndexOf(whitespace);
            if (whitespaceIndex > 0)
                tmpStatement = tmpStatement.Slice(0, whitespaceIndex);

            //一番優先度の低い演算子がこのstatementの本体になる
            for(var operatorIndex = Math.Max(0, useBinaryOperatorIndex); operatorIndex < sortedBinaryOperatorList.Count; operatorIndex++)
            {
                var tmpOperator = sortedBinaryOperatorList[operatorIndex];
                if(tmpStatement.Length == tmpOperator.operatorChars.Length && tmpStatement.Contains(tmpOperator.operatorChars, StringComparison.Ordinal))
                {
                    useBinaryOperatorIndex = operatorIndex;
                    operatorStartIndex = i;
                }
            }
        }

        if (useBinaryOperatorIndex < 0 || operatorStartIndex < 0)
        {
            //単項だった場合
            return CreateUnaryExpression(statement);
        }

        //本体となる演算子の左右のExpressionを作成する
        var useOperator = sortedBinaryOperatorList[useBinaryOperatorIndex];
        var leftStatement = statement.Slice(0, operatorStartIndex);
        var leftExpression = CreateExpression(leftStatement);
        var rightStatement = statement.Slice(operatorStartIndex + useOperator.operatorChars.Length);
        var rightExpression = CreateExpression(rightStatement);

        //本体の演算子でExpressionを作成
        leftExpression = Expression.Convert(leftExpression, useOperator.parameterType);
        rightExpression = Expression.Convert(rightExpression, useOperator.parameterType);
        return Expression.MakeBinary(useOperator.type, leftExpression, rightExpression);
    }

    /// <summary>
    /// 単項式のExpressionを作成
    /// </summary>
    public Expression CreateUnaryExpression(ReadOnlySpan<char> sourceStatement)
    {
        var statement = TrimWhiteSpace(sourceStatement);

        //否定演算子の場合、後ろをExprsssionにしてから否定演算子を付ける
        const char notOperator = '!';
        var isNot = false;
        if (sourceStatement[0] == notOperator)
        {
            isNot = true;
            statement = sourceStatement.Slice(1);
        }

        //statementの中身に応じてExpressionを作成
        Expression expression = null;
        var statementString = statement.ToString();
        if (statement[0] == startbracket && statement[statement.Length - 1] == endbracket)
        {
            //statementが括弧で囲まれている場合は中身は式の可能性がある
            expression = CreateExpression(statement);
        }
        else if(int.TryParse(statementString, out var num))
        {
            //intにParseできた場合
            expression = Expression.Constant(num);
        }
        else
        {
            //それ以外の場合、statementの文字列をキーとしてdictionaryの要素を取得する
            expression = Expression.Property(dicParameterExpression, "Item", Expression.Constant(statementString));
        }

        //否定演算子がついていた場合はExpression.Notを足す
        if (isNot)
            expression = Expression.Not(Expression.Convert(expression, typeof(bool)));

        if (expression == null)
        {
            Debug.LogError($"Expression is null! statement:\"{statement.ToString()}\"");
        }

        return expression;
    }
    public ReadOnlySpan<char> TrimWhiteSpace(ReadOnlySpan<char> statement)
    {
        return statement.TrimStart(whitespace).TrimEnd(whitespace);
    }

UniRxでUniTaskをSubscribeしたい

あるイベントが発行されたときに、非同期的な処理を開始したかった(例:マウスがクリックされた時一定時間毎フレーム何か処理をするみたいな)

パターン①

    UniTask _task;
    private void Start()
    {
        _task = UniTask.Defer(() => HogeTask());
        this.UpdateAsObservable()
              .Where(u => Input.GetMouseButtonDown(0))
              .Subscribe(async u => await _task);
    }

    private async UniTask HogeTask()
    {
        for(var i = 0;i < 10;i++)
        {
            Debug.Log($"Hoge {i}");
            await UniTask.Delay(100);
        }
    }

パターン②

    private void Start()
    {
        System.Action<Unit> action = async u => await HogeTask("action1");
        action += async u => await HogeTask("action2");
        this.UpdateAsObservable().Where(u => Input.GetMouseButtonDown(1)).Subscribe(action);
    }

    private async UniTask HogeTask(string text)
    {
        for(var i = 0;i < 10;i++)
        {
            Debug.Log($"{text} {i}");
            await UniTask.Delay(100);
        }
    }

ちょっと説明

UniTaskは生成された瞬間から処理が開始されるみたいな感じになっている
なので、UniTaskを変数に保持しておこうと思ってあらかじめ生成しておきたくてもその時点から処理が始まってしまう

パターン①

そこでUniTaskでは「awaitするまで生成しない」ようにできる関数が用意されていて(UniTask.Defer)、そいつにUniTask(本命)を生成する関数を渡して返されたUniTask(義理)を使えば、UniTask(義理)をawaitした瞬間からUniTask(本命)が開始されてくれる

UniRxのあるイベントが発行された時にUniTask(本命)を開始したい場合は上に書いたソースコードのようにSubscribeにUniTask(義理)をawaitするラムダ式を渡してやればよい(ラムダ式にasyncつけれるの初めて知った)
上の例だと左クリックされた瞬間から「Hoge {i}」とログが出され始める

パターン②

遅延生成の肝はUniTaskを生成する関数を保持しておくことなことに気づいたので、
任意の数のUniTaskを任意のタイミングで開始したい場合、②のようにすればいい感じになった

補足

変数に保持しておかなくてもアクセスできる場所に本命のUniTask関数がある場合、Subscribeの中身は「async u => await _task」ではなく「u => HogeTask()」でもいい
外部から渡されたUniTaskを変数に保持しておいて任意のタイミングで開始したい場合に上のコードが必要になってくると思う

追記

めっちゃおなかすいた

うめぼしたべたい(わかる人だけでいいよ)

ハチミツでもクリスピーでも海苔の佃煮でもなく、梅干しなんですよね。
酸っぱくて素朴で小さく丸い食べ物。

知らない間に僕も悪者になってた
優しい言葉だけじゃ物足りない

甘いだけの関係性に甘えていたけれど、周りにいたのは実は身勝手な人間ばかりだったと気づいたのでしょう。
酸っぱい言葉をかましてくれるような、本気で自分のことを考えてくれるような人がたった一人だけいてくれればいいのにと。

長年にわたるスピッツ研究の結果、「うめぼし」は多分そういう歌だとわかりました。
(○首という説がまことしやかにささやかれているがそれは流石にかの宣言にとらわれすぎでは?)

榛名神社

GWが明けた8日土曜日、友人とレンタカーを借りて榛名へ向かった。
練馬ICから関越自動車道を通り渋川伊香保ICで降り、まずは榛名山(榛名富士)へ。

片道1時間半ほどかけて榛名富士を登った。天気も良く、気温もちょうどよい日だ。
頂上で榛名富士山神社に参拝。この時で11時くらいだったと思うが、寂しくもなく混んでもおらずで快適だった。

夏ほど空気が澄んではいないので遠くの景色が見えなかったのは残念。

下りは1時間ほどだったと思う。
下りて登山口を出たところで小さい花畑があるのを発見。すわカメラの練習台にしてやろうと思い、いろいろな角度から写真を撮ってみる。

やはり色が鮮やかな花は画面に映える。かなり近くまで寄って遠景と一緒に収めると上級な感じの写真が撮れた。

その後、榛名湖のほとりの広場で乗馬体験をさせてもらった。どこかの牧場からかと思ったが、聞くところによれば付近の住民は個人で馬を飼っていることが多いとのことで、乗せてもらった馬も個人所有のものらしい。

暴れたりしないかとも思ったが非常におとなしい馬で、特に危うげなく付近を一周することができた。競走馬ではなく性格の穏やかなハフリンガーとクォーターホースという品種の馬だそうだ。一人一回2000円也、安い。

昼をまわっていたため、駐車場の向かいにあった彩湖庵という店で昼食をとった。
注文したのはわかさぎ定食。そういえば榛名湖では冬に氷上でわかさぎ釣りができるらしい。

子持ちわかさぎのフライにとろろ飯がついていて味もボリュームも申し分無し。榛名湖を見つつゆっくり食事ができた。
ひとごこちもついたところで榛名神社へ向かう。

榛名神社は榛名富士や榛名湖から車で20分ほどかかる場所にあった。意外と遠い。

榛名富士が円錐形のきれいな山だったため、榛名富士を神体山としていると思っていたがどうやらそういうわけではないらしく、付近の山地全体が信仰の対象という印象だった。
13時半ごろに榛名神社へ到着。門前町には何某坊と看板に書かれた宿泊施設が立ち並んでいて、神仏習合の名残を色濃く感じた。

鳥居をくぐって境内へ入り、榛名川沿いに敷かれた石畳の山道を歩いて本殿へ向かう。ゆっくり歩いて20分ぐらいだっただろうか。よく整備はされているが豪奢というわけではなく、静かに参拝することのできる環境になっていた。
本殿付近には背の高い巨岩がいくつもそびえ立っていて、それらを縫うようにして石段を登っていくようになっていた。


中でもとりわけ背の高く頂上付近でだるま上になっている形の岩が神体である御姿岩で、本殿は御姿岩に埋まるような形で建てられていた。
本殿を含めた建物は一面が細かい木彫りの彫刻で装飾されていて見事だった。

神体は御姿岩ではあるが配置上は榛名富士・榛名湖のほうを向くように建てられていたため、元は神域を拝するような形になっていたんじゃないかと推察。
参拝した後、榛名川をさらにさかのぼった場所にあった砂防堰堤を眺めてから神社を出た。ついでに力こんにゃくなるものを買って小腹を満たす。七味をかけすぎてむせた。

榛名川を小鳥が飛んでいたので急いで望遠レンズに付け替えて写真を撮った。
手前のぼやけた葉っぱと後ろの岩の隙間の暗い部分がいい感じのコントラストになっている気がする。
肝心の鳥はよく見えない。

神社を出たあと、入り口付近にあった榛名歴史民俗資料館立ち寄った。
御師や地元の祭りに関するものが展示されていた。悲しいかな、文章を読むのに必死だったせいかあまり内容を覚えていない……。ふもとにいくつも摂末社があって、地域の信仰が厚いということは覚えている。

歩きっぱなしでくたくただったので榛名湖まで戻り、榛名湖温泉ゆうすげ元湯という温泉に入る。
伊香保に行こうか迷ったが、伊香保は別の機会に取っておくことにする。そういえば伊香保というのは厳峰のことで、榛名山のことを指す言葉らしい。
露天風呂は榛名湖を眺めることができる位置だったが、榛名湖沿いの道から丸見えの場所にあったので柵がしてあってじっくり眺めることはできなかった。
コーヒー牛乳は売り切れていたので代わりにミックスジュース的なものを飲んだ。
温泉の売店では「かける梅胡椒」というものを買った。意外にも榛名は南紀に次ぐ梅の生産地らしい。

リフレッシュもできたことなので帰路につく。
朝7時半に東京を出てゆっくり回った観光した後、夜の7時ごろ東京へ到着。
関越自動車道もあまり混まない広い道なので、榛名山は東京から気分転換しに行くのに本当にちょうどいい感じの場所だった。