UniRx: Observable.ReturnUnit のソースを読む

Observable.ReturnUnit のサンプルコード

using System;
using UniRx;
using UnityEngine;

public class Main : MonoBehaviour
{   
    void Start()
    {
        var s = Observable.ReturnUnit();
        s.Subscribe(e => Debug.Log("e: " + e), () => Debug.Log("OnCompleted"));
    }
}

実行結果です。

e: ()
OnCompleted

Observable.ReturnUnit のソースを読む

Observable.ReturnUnit()Subscribe() するとき、内部でどのような処理が行われるのか眺めてみます。

まずは Observable.ReturnUnit() の内部を見てみます。

/// <summary>
/// Same as Observable.Return(Unit.Default); but no allocate memory.
/// </summary>
public static IObservable<Unit> ReturnUnit()
{
    return ImmutableReturnUnitObservable.Instance;
}

ImmutableReturnUnitObservable.Instance インスタンスが使い回されているようです。

次に ImmutableReturnUnitObservable クラスの実装を眺めてみます。

internal class ImmutableReturnUnitObservable : IObservable<Unit>, IOptimizedObservable<Unit>
{
    internal static ImmutableReturnUnitObservable Instance = new ImmutableReturnUnitObservable();

    ImmutableReturnUnitObservable()
    {

    }

    public bool IsRequiredSubscribeOnCurrentThread()
    {
        return false;
    }

    public IDisposable Subscribe(IObserver<Unit> observer)
    {
        observer.OnNext(Unit.Default);
        observer.OnCompleted();
        return Disposable.Empty;
    }
}

ふむふむ。Subscribe() するとすぐに OnNext() と OnCompleted() を順に呼び出しています。

ふと疑問。サンプルコードで呼び出している Subscribe() は 2 引数なのですが、上の定義では 1 引数の定義しかありませんね。 2 引数を受け取る Subscribe() はどこで定義されているのでしょうか。

コードを読むと、以下の拡張メソッドで定義されていました。

public static IDisposable Subscribe<T>(this IObservable<T> source, Action<T> onNext, Action onCompleted)
{
    return source.Subscribe(Observer.CreateSubscribeObserver(onNext, Stubs.Throw, onCompleted));
}

ImmutableReturnUnitObservable クラスの Subscribe() の引数には、Observer.CreateSubscribeObserver() の戻り値が渡されます。

Observer.CreateSubscribeObserver() の内部では、引数で渡した onNext, onCompleted を呼び出す Subject<T>インスタンスを生成しています。

// same as AnonymousObserver...
class Subscribe<T> : IObserver<T>
{
    readonly Action<T> onNext;
    readonly Action<Exception> onError;
    readonly Action onCompleted;

    int isStopped = 0;

    public Subscribe(Action<T> onNext, Action<Exception> onError, Action onCompleted)
    {
        this.onNext = onNext;
        this.onError = onError;
        this.onCompleted = onCompleted;
    }

    public void OnNext(T value)
    {
        if (isStopped == 0)
        {
            onNext(value);
        }
    }

    public void OnCompleted()
    {
        if (Interlocked.Increment(ref isStopped) == 1)
        {
            onCompleted();
        }
    }
    
    // (コード一部省略・・・)
}

以上で、Observable.ReturnUnit()Subscribe() するときの処理の流れがわかりました。

ポイントは、ImmutableReturnUnitObservable.Subscribe() の処理でした(再掲)。

internal class ImmutableReturnUnitObservable : IObservable<Unit>, IOptimizedObservable<Unit>
{
    public IDisposable Subscribe(IObserver<Unit> observer)
    {
        observer.OnNext(Unit.Default);
        observer.OnCompleted();
        return Disposable.Empty;
    }
}

Subscribe() すると即座に OnNext(), OnCompleted() を呼び出します。 引数として渡された observer の参照を掴んでいませんので、サンプルコード上で生成しているクロージャ(onNext, onCompleted)はリークしていないことも確認できました。

参考

UniRx: Start

Observable.Start メソッドは、引数で指定した Func<T> 関数から生成したデータを通知します。

using System;
using UniRx;
using UnityEngine;

public class Main : MonoBehaviour
{
    private IObservable<DateTime> _observable;

    void Start()
    {
        _observable = Observable.Start(() => DateTime.Now);
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            // スペースキーを押すたびに Subscribe する
            _observable.Subscribe(e => Debug.Log(e),
                                  () => Debug.Log("OnCompleted"));
        }
    }
}

実行結果です。スペースキーを押すたびに出力される時刻が更新されていますね。

2018/08/20 21:18:20
OnCompleted
2018/08/20 21:18:22
OnCompleted
2018/08/20 21:18:24
OnCompleted

参考

UniRx: Return

    void Start()
    {
        var s = Observable.Return(100);
        s.Subscribe(e => Debug.Log(e), () => Debug.Log("OnCompleted"));
    }

実行結果です。

100
OnCompleted

UniRx の Return の実装を読む。パフォーマンスを上げるために、通知する値の型に応じて実装をもっているようだ。 int の小さな値は内部でキャッシュしている。

参考

ToLookup 拡張メソッド

以前に Partition 関数 - C#練習日記 を作りましたが、 Linq には ToLookup 拡張メソッド があり、このメソッドを使うことで同様のグルーピングを行うことができます。

using System;
using System.Linq;

class Program
{    
    static void Main()
    {
        var xs = new[] { 1, 2, 3, 4, 5 };

        // 偶数と奇数でグループ分けする
        var d = xs.ToLookup(e => e % 2 == 0);
        Console.WriteLine(string.Join(", ", d[true]));  // 2, 4
        Console.WriteLine(string.Join(", ", d[false])); // 1, 3, 5
        
        // 正の整数とそれ以外でグループ分けする
        var d2 = xs.ToLookup(e => e > 0);
        Console.WriteLine(d2[true].Count());  // 5 
        Console.WriteLine(d2[false].Count()); // 0
    }
}

ToLookup の戻り値は、ILookup<TKey, TElement> です。 各キーに対する要素は IEnumerable<TElement> で取得できます。

プロパティへの代入がコンストラクタ内のみなら、そのプロパティに private set を定義する必要はない

using System;

class Person
{
    public string Name { get; } // 代入はコンストラクタでのみ。private set; は不要
    public int Age { get; }

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

class Program
{    
    public static void Main()
    {
        var p = new Person("Bob", 23);
        Console.WriteLine($"{p.Name} {p.Age}"); // Bob 23

        // コンパイルエラー: The Property 'Person.Name' has no setter
        // p.Name = "Alice";
    }
}

N 番目の要素を O(1) で取得する Cycle クラスを作る

数列 a = { 0, 1, 0, -1, 0, 1, 0, -1, 0, 1, 0, -1, ... } のような 4 周期で繰り返す数列で、かつ N 番目の要素を O(1) で取得する Cycle クラスを作ってみました。

// 0, 1, ... を繰り返す数列
var zeroOne = new Cycle<int>(new[] { 0, 1 });
Console.WriteLine(string.Join(", ", zeroOne.Take(5)));
//=> 0, 1, 0, 1, 0
Console.WriteLine(string.Join(", ", new[] { 0, 1, 2, 3, 5 }.Select(e => zeroOne[e])));
//=> 0, 1, 0, 1, 1

// 0, 1, 0, -1, ... を繰り返す数列
var seq = new Cycle<int>(new[] { 0, 1, 0, -1 });
Console.WriteLine(string.Join(", ", seq.Take(8)));
//=> 0, 1, 0, -1, 0, 1, 0, -1
Console.WriteLine(string.Join(", ", new[] { 0, 1, 2, 3, 5, 7 }.Select(e => seq[e])));
//=> 0, 1, 0, -1, 1, -1

Cycle クラス:

class Cycle<T> : IEnumerable<T>
{
    private readonly T[] _array;
    
    public Cycle(T[] array)
    {
        if (array == null) throw new ArgumentNullException(nameof(array));
        if (array.Length == 0) throw new ArgumentException(nameof(array));

        _array = array;
    }
    
    public IEnumerator<T> GetEnumerator()
    {
        while (true)
        {
            foreach (var x in _array)
                yield return x;
        }        
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    public T this[int index] => _array[index % _array.Length];
}

エラトステネスの篩

haskell.g.hatena.ne.jp

上の記事をみて、エラトステネスの篩を C# で書いてみました。

// 2, 3, 4, 5, ...
static IEnumerable<int> Nums()
{
    int n = 2;
    while (true)
        yield return n++;
}

static Func<int, bool> MakePrimeFilter(int prime, Func<int, bool> isPrime)
{
    // x はこれまで登場した素数で割りきれない、かつ prime でも割り切れないならば、x は素数である
    return x => isPrime(x) && x % prime != 0;
}

static IEnumerable<int> Primes()
{
    Func<int, bool> isPrime = n => true;
    foreach (var x in Nums())
    {
        if (isPrime(x))
        {
            yield return x;
            isPrime = MakePrimeFilter(x, isPrime);
        }
    }
}

お試しコード。

public static void Main(string[] args)
{
    Console.WriteLine(string.Join(", ", Primes().Take(10)));    
    // => 2, 3, 5, 7, 11, 13, 17, 19, 23, 29

    Console.WriteLine(Primes().ElementAt(1000)); // 7927
    
    // 10,000 より大きな最小の素数は?
    Console.WriteLine(Primes().SkipWhile(e => e <= 10000).First()); // 10007
}

※ このコードは効率はよくありません。関数を重ねるコードの書き方の練習用です。

双子素数も求めてみます。

// 双子素数を求める
static IEnumerable<Tuple<int, int>> Twins()
{
    foreach (var pair in Primes().Zip(Primes().Skip(1), (a, b) => Tuple.Create(a, b)))
    {
        if (pair.Item1 + 2 == pair.Item2)
            yield return pair;
    }
}

public static void Main(string[] args)
{
    var ts = Twins().Take(10);
    Console.WriteLine(string.Join(", ", ts.Select(e => $"({e.Item1}, {e.Item2})")));
    // => (3, 5), (5, 7), (11, 13), (17, 19), (29, 31), (41, 43), (59, 61), (71, 73), (101, 103), (107, 109)
}

元コード:

twins = filter ((2==) . uncurry subtract) $ s zip tail primes

上の ((2==) . uncurry subtract) のところがよく分からなかったので分解してみます。

subtract は、subtract a b で b - a を計算します。

Prelude> :t subtract
subtract :: Num a => a -> a -> a
Prelude> subtract 1 5
4

uncurry は、uncurry (+) (a, b)(+) a b を計算します。

Prelude> :t uncurry
uncurry :: (a -> b -> c) -> (a, b) -> c
Prelude> uncurry (+) (1, 3)
4

(2==) は、2 と等しいかを判定する関数です。

Prelude> :t (2==)
(2==) :: (Num a, Eq a) => a -> Bool
Prelude> (2==) 2
True
Prelude> (2==) 3
False

よって、(2==) . uncurry subtract は、タプル (a, b) を受け取って substract a b すなわち b - a が 2 と等しいかを判定する関数、ということになります。

Prelude> a = (2==) . uncurry subtract
Prelude> :t a
a :: (Eq a, Num a) => (a, a) -> Bool
Prelude> a (1, 3)
True
Prelude> a (1, 4)
False