📌

【C#】オレオレ過剰抽象化ライブラリでAtCoderでふざける

2022/12/13に公開約7,000字

概要

極度に抽象的なライブラリや用途が限定されるユーティリティ関数たち、あったら便利だなと思いつつ実プロダクトには中々導入しがたいものがありますよね...

しかし、AtCoderのようなプログラミングコンテストでだったら、どうせ自分一人しかコードを読み書きしないのだから思う存分オレオレ補助関数を突っ込めるではありませんか!
そこで、AtCoderの間違った遊び方として、オレオレ過剰抽象化関数を作成しまくって遊ぶという遊び方を提案します。

お断り

  • AtCoderでガチで好成績を狙いに行くのにはむいていないと思います。
    • 定数倍の実行速度悪化とかが起こるだろうし
  • これ抽象化の切り方変じゃない?っていうところがいっぱいあると思います。
    • ノリと勢いで問題の解答の手を進めるために筋の悪い関数の切り方をしている箇所が結構ある気がする

入力編

AtCoderでは、たとえば入力は次のようなトークン列の形で標準入力から渡されます。

3        // N... 配列Aの長さ
8 12 40  // A

出典(abc081 b)

4   // N ... 配列Aの長さ
10  // A1
8   // A2
8   // A3
6   // A4

出典(abc085 b)

これを素朴にパースしようとすると、上のどちらの形式で渡されているかによって以下のように書き分けることになるかと思われます。

int N = int.Parse(Console.ReadLine());
List<int> A = Console.ReadLine().Split().Select(int.Parse).ToList();
private static void Main()
{
    int N = int.Parse(Console.ReadLine());
    List<int> A = new List<int>();

    for (int i = 0; i < N; i++)
    {
        A.Add(int.Parse(Console.ReadLine()));
    }
}

cin の定義

上述の変換の面倒くささと、改行の入り方のバリエーションへの一律的な対応を図って、入力パースメソッドを用意します。

入力は有限長で、かつスペースか改行で区切られたトークン列なので、まずトークンを全て読み込みます。

public class Cin
{
    public Queue<string> Tokens { get; set; } = new Queue<string>();

    public Cin()
    {
        string line;
        while ((line = Console.ReadLine()) != null && line != "")
        {
            string[] tokens = line.Split();
            foreach (string token in tokens)
            {
                Tokens.Enqueue(token);
            }
        }
    }
}

それを欲しい型に変換した上で読み込む関数を定義します。

public int ReadInt()
{
    return int.Parse(Tokens.Dequeue());
}

public long ReadLong()
{
    return long.Parse(Tokens.Dequeue());
}

public string ReadString()
{
    return Tokens.Dequeue();
}

これにより、先ほど挙げたどちらのケースでも以下のコードで対応できるようになります。

private static void Main()
{
    Cin cin = new Cin();
    int N = cin.ReadInt();
    List<int> A = new List<int>();

    for (int i = 0; i < N; i++)
    {
        A.Add(cin.ReadInt());
    }
}

リストの扱いの改良

入力リストを得るのにfor文を回さなきゃならないのは面倒なので、ここを抽象化します。
Rubyにはある処理をn回繰り返すtimesというメソッドがるのでここを若干真似しつつ、以下の関数を用意します。

public static IEnumerable<T> Times<T>(this int n, Func<T> f)
{
    for (int i = 0; i < n; i++)
    {
        yield return f();
    }
}
public static IEnumerable<T> Times<T>(this long n, Func<T> f)
{
    for (long i = 0; i < n; i++)
    {
        yield return f();
    }
}

これにより、先ほどのmain関数は以下のように書き直せます。

private static void Main()
{
    Cin cin = new Cin();

    int N = cin.ReadInt();
    List<int> A = N.Times(cin.ReadInt).ToList();
}

さらに、このTimesを使ってリストを読み込む関数、行列を読み込む関数も定義してしまいます。

public List<int> ReadInts(int length)
{
    return length.Times(ReadInt).ToList();
}

public List<long> ReadLongs(int length)
{
    return length.Times(ReadLong).ToList();
}

public List<string> ReadStrings(int length)
{
    return length.Times(ReadString).ToList();
}

public List<List<int>> ReadIntMatrix(int columns, int rows)
{
    return columns.Times(() => rows.Times(ReadInt).ToList()).ToList();
}

public List<List<long>> ReadLongMatrix(int columns, int rows)
{
    return columns.Times(() => rows.Times(ReadLong).ToList()).ToList();
}

public List<List<string>> ReadStringMatrix(int columns, int rows)
{
    return columns.Times(() => rows.Times(ReadString).ToList()).ToList();
}

これにより、main関数は以下のようになりました。

private static void Main()
{
    Cin cin = new Cin();

    int N = cin.ReadInt();
    List<int> A = cin.ReadInts(N);
}

書く順序編

「aに対して、fをして、gをして、hをした結果を出力。」をするためには、次のような書き方になるのではないでしょうか?
Console.WriteLine(h(g(f(a))));
かっこが多くて理解が大変であることと、処理の時系列順とコード上に現れる順番が逆になっていて若干つらさがあります。
そこで、パイプライン演算子のような拡張メソッドを導入して、書く順序を時系列順と揃えます。

public static S Pipe<T, S>(this T value, Func<T, S> f)
{
    return f(value);
}

public static void Pipe<T>(this T value, Action<T> f)
{
    f(value);
}

これにより、Console.WriteLine(h(g(f(a))))

a.Pipe(f)
    .Pipe(g)
    .Pipe(h)
    .Pipe(Console.WriteLine);

と書けます。
(記述量は増えてしまっていますが...)

特に、「色々変換した後に最後に出力する」は頻出パターンなので、ここはショートハンドを用意します。

public static void WriteLine<T>(this T value)
{
    Console.WriteLine(value);
}

最終的に、


a.Pipe(f)
    .Pipe(g)
    .Pipe(h)
    .WriteLine();

と書けるようになりました。
個人的にはリスト操作をした最後に何か処理を加えるときに便利な気がしています。

listA.Select(foo)
    .Where(bar)
    .Pipe(f)
    .WriteLine();

デバッグ出力編

print デバッグをしやすくするために、次の関数を定義する

public static void DebugPrint(
        string message = "",
        [CallerLineNumber] int line = 0,
        [CallerMemberName] string name = "")
{
    $"[line{line}@{name}]: {message}".WriteLine();
}

リストや行列(二次元リスト)についてもC#だと中身をいい感じに表示してくれるように関数を用意する。

public static void DebugPrint<T>(
    this T value,
    string identifier = "",
    [CallerLineNumber] int line = 0,
    [CallerMemberName] string name = "")
{
    (identifier == ""
        ? $"[line{line}@{name}]: {value}"
        : $"[line{line}@{name}]: {identifier} = {value} ").WriteLine();
}



public static void DebugPrintEnumerable<T>(
    this IEnumerable<T> value,
    string identifier = "",
    [CallerLineNumber] int line = 0,
    [CallerMemberName] string name = "")
{
    value.ToStringPretty().DebugPrint(identifier, line, name);
}

public static void DebugPrintMatrix<T>(
    this IEnumerable<IEnumerable<T>> value,
    string identifier = "",
    [CallerLineNumber] int line = 0,
    [CallerMemberName] string name = "")
{
    ("\n" + value.ToStringPrettyMatrix()).DebugPrint(identifier, line, name);
}

たとえば、以下のmain を実行すると以下のような出力を得る

private static void Main()
{
    List<int> A = new List<int> { 1, 2, 3, 4, 5 };

    A.DebugPrintEnumerable();
}
[line12@Main]: 1 2 3 4 5

その他

2次元, 3次元リストの初期化が地味に面倒なのでこれも関数化する。

public static List<List<T>> NewTwoDimensionalList<T>(T value, int m, int n)
{
    return m.Times(() => Enumerable.Repeat(value, n).ToList()).ToList();
}

public static List<List<List<T>>> NewThreeDimensionalList<T>(T value, int l, int m, int n)
{
    return l.Times(() => NewTwoDimensionalList(value, m, n)).ToList();
}

どこで終わるかわからない処理を途中でぶった切って使う用途に、無限イテレータを用意しておく


/// 自然数を(intの範囲で)0から数え上げていく
public static IEnumerable<int> Natural()
{
    for (int i = 0; i < int.MaxValue; i++)
    {
        yield return i;
    }
}

/// 同じ値を無限に吐き続ける
public static IEnumerable<T> Infinite<T>(this T value)
{
    while (true)
    {
        yield return value;
    }
}


/// init, f(init), f(f(init)), f(f(f(init))), ... を返す
public static IEnumerable<T> Iterate<T>(T init, Func<T, T> f)
{
    T value = init;
    while (true)
    {
        yield return value;
        value = f(value);
    }
}
public static IEnumerable<ACC> Scan<T, ACC>(this IEnumerable<T> source, ACC init, Func<T, ACC, ACC> f)
{
    ACC acc = init;
    foreach (T v in source)
    {
        yield return acc;
        acc = f(v, acc);
    }
}

まとめ

Haskell とかだとこういう極度に抽象的な関数ありがちなんですけれど、C#にはあまりありませんよね...
こういう変な遊び方をして楽しむ楽しみ方もあると思います!(本来のAtCoderの楽しみ方ではなさそうですけれど...)

GitHubで編集を提案

Discussion

ログインするとコメントできます