読者です 読者をやめる 読者になる 読者になる

技術memo

関数型ゴースト

業務プログラマーのための不完全にしてあまり意義のないプログラミングガイド

概ね、「比較的冗長な構文を持ち、ガベージコレクションを行い、シングルディスパッチを使う、クラスベースで静的型付けのオブジェクト指向言語で、実装を単一継承しインタフェースを多重継承する」ようなプログラミング言語の話です。

なお、以下のコード例は全部C#です。

目次

  • 言語仕様がつらい
  • 作法・定型パターンに頼るところが多すぎてつらい
  • デザインパターンがつらい
  • 既にあるコードがつらい
  • 本を読みましょう
  • 現実を見ましょう

言語仕様がつらい

レコード型

不変な複数の名前付きフィールドを持ったデータを定義する場合、こんな感じに書きます

    public sealed class Person
    {
        private readonly string name;
        public string Name { get { return name; } }

        private readonly int age;
        public int Age { get { return age; } }

        private readonly string work;
        public string Work { get { return work; } }

        public Person(string name, int age, string work)
        {
            this.name = name;
            this.age = age;
            this.work = work;
        }
    }

Builderパターンやファクトリメソッド(※デザインパターンのそれとは等価ではない)を付け足すとこんな感じです

public sealed class Person
{
    private readonly string name;
    public string Name { get { return name; } }

    private readonly int age;
    public int Age { get { return age; } }

    private readonly string work;
    public string Work { get { return work; } }

    private Person(string name, int age, string work)
    {
        this.name = name;
        this.age = age;
        this.work = work;
    }

    public static Person New(string name, int age, string work)
    {
        return new Person(name, age, work);
    }
    public sealed class Builder
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public string Work { get; set; }
        public Person Build()
        {
            return new Person(Name, Age, Work);
        }
    }
}

この程度ならT4テンプレート化しておくといい感じですね(参考: neue cc - T4による不変オブジェクト生成のためのテンプレート)

多相性

「同じような性質を持ったオブジェクトを、共通に扱いたい」といった場合は、インターフェースを定義します。

    public interface INamed
    {
        public string Name { get; }
    }

    public sealed class Person : INamed
    {
        private readonly string name;
        public string Name { get { return name; } }

        private readonly int age;
        public int Age { get { return age; } }

        private readonly string work;
        public string Work { get { return work; } }

        public Person(string name, int age, string work)
        {
            this.name = name;
            this.age = age;
            this.work = work;
        }
    }

    public sealed class Pet : INamed
    {
        private readonly string name;
        public string Name { get { return name; } }

        private readonly int age;
        public int Age { get { return age; } }

        public Pet(string name, int age)
        {
            this.name = name;
            this.age = age;
        }
    }

    public static class Program
    {
        public static void Main(string[] args)
        {
            var family = new INamed[]
            {
                new Person("今日子", 28, "流通業"),
                new Person("二郎", 30, "飲食業"),
                new Pet("タマ", 5)
            };
            foreach (var x in family)
            {
                Console.WriteLine("名前:" + x.Name);
            }
        }
    }

クラス継承の話は忘れるべきです。人類が扱うには早すぎた技術です。

委譲

同じような処理を使いまわしたい場合に使います。間違えても継承を使おうなどとは考えるべきではありません。

    public sealed class Wallet
    {
        public decimal Money { private set; get; }
        public void Add(decimal money)
        {
            Money += money;
        }
        public void Remove(decimal money)
        {
            Money -= money;
        }
        public bool MoreThanEqual(decimal other)
        {
            return other <= Money;
        }
    }
    public sealed class Person
    {
        private Wallet wallet = new Wallet();
        public bool HasMoney(decimal threshold)
        {
            return wallet.MoreThanEqual(threshold);
        }
        //TODO 収入を得るメソッドが必要
    }

処理の一部を書き換えたい場合は、デリゲート関数(あるいはそれを複数まとめたオブジェクト)を引数に取るようにしましょう。Strategyパターンについても参照。

全部言語仕様のせいだ

より安全なプログラムを書くための指針に従うと、実装がやたらに面倒なのは、言語の設計が悪いせいです。高々この程度の機能実現の為に、むやみにタイプ数を増やさなければならないのが悪いのです。

安全のためのタイピングをサボる人は、コード自動生成やスニペットを用いずに、無駄なクラス継承関係を作ったり、不変にしてよいフィールドを可変のままで扱ったりします。その結果、酷いコードが世の中に満たされるわけです。

使う道具は選びましょう。選んだ、あるいは選ばされた道具は研ぎましょう。それだけの話です。

作法・定型パターンに頼るところが多すぎてつらい

関数・オブジェクトに関する指針

  • オブジェクトのフィールドを書き換えるメソッドは、必ずインスタンスメソッドから提供すること。外部で計算した値をセットしにいくのは、たいてい悪手です。
  • 関数は小さく保つこと。抽象化による切り出しと組み合わせの手間を惜しまないこと。
  • ガードや関数分割を使って「分岐構造の入れ子」をなんとしても避けること。
  • 変数の再代入を避けること。変数は原則「特定の値に名前を付けて、複数回参照するため」だけに用います。
  • 値を書き換えるついでに結果を返すような関数は、たいていダメ。(Command-Query分離の原則)
  • 結果を返すために、関数の外部のフィールドを書き換えたり、out引数を使うのは最悪。Tupleなり上記のようなレコード型オブジェクトを返すこと。
    • Haskellで言うところのMaybeやEitherのような型も、参考になります。

型の扱いに関する指針

  • フラグは列挙型(enum)を定義して使うこと。int 1の場合は○○、2の場合は○○、みたいなパターンは最悪です。
  • ジェネリックコレクションを使うこと。(List, Dictionary<TKey, TValue>)
  • 抽象的な型はジェネリッククラスとして作ること。利用側で型情報を失いたくない場合は。
  • 抽象的な関数はジェネリック関数として作ること。利用側で型情報を失いたくない場合は。
  • ダウンキャストをしないこと。インスタンスの型をチェックする処理を手動で書かないこと。
  • デリゲートはSystem.Func/Actionを使うこと。個別のデリゲートを定義する意義はさほどありません。
  • 時には妥協も必要だということ。型検査を頼るにしても、そのためにコードが複雑になりすぎるのもまた困ります。

命名に関する作法

  • コーディング頻出単語を、用法とセットで覚えること。詳しくは検索しましょう。
    • 例:
      • validate: 検証。条件を満たすならtrue
      • refresh: 最新の状態を再取得する
      • register: (データベースなどに)対象を登録する
      • 他: set, get, update, insert, add, remove, find, exists, required, enumerate, display, show, enabled, confirm, notice, is, as, has, contains, create, build 等...
  • 英単語を略さないこと。略すにしても関数内の変数など、狭いスコープのものに限ること。
    • Databaseをdbと書いたりするのは、「ややこしくならない」「読みづらくならない」から例外なんでしょうか。
  • ひとつのクラス名やメソッド名が3単語を超えたら、その機能のまとまりが大きすぎないか、設計を見直すこと。
    • 関数名なら「動詞」や、せいぜい「動詞+目的語」程度で済むのが理想です。
    • 「○○に対して△△を使って××する処理」を「○○.××(△△)」のメソッド呼び出し形式で書ける程度のシンプルさを。
      • △△も無い方がいい

設計に関する指針

  • 可変なフィールド変数を可能な限り少なくすること。(オブジェクト指向-カプセル化/隠蔽)
  • クラスや関数は小さく保つこと。適切な(それができれば苦労は無い)モジュール化を行うこと。
  • 単一の値をオブジェクトでラップするパターンは、手間が非常にかかるけれど、型安全性や、凝集度の為にはそれなりに効果があるように思います。取捨選択の基準も難しいですが……。

    • 例:
      • Money(decimal)
      • StartDate/EndDate(DateTime)
      • UserId(int)

デザインパターンがつらい

有名なGoFデザインパターンデザインパターン (ソフトウェア) - Wikipediaについて。 現代的なプログラミング言語でもまだ「使える」、あるいは「使いどころが多い」ものは、さほど多くないような気がします。

生成に関するパターン

  • Abstract Factory
    • 一連のオブジェクトの生成方法として。interfaceを使う方法でも可。
  • Builder
  • Factory Method
    • オブジェクトの生成方法は、オブジェクト自体とは切り分けておく。特に、それが複雑になりうる場合は。
  • Singletone
    • どちらかといえば「グローバルアクセスを提供するための必要悪」として。
    • シングルトンオブジェクトのメソッドも、interface越しにアクセスするようにした方が、後々ラクです。
    • オブジェクトが単一かどうかそれ自体は、さほど重要でもないような気がします。

構造に関するパターン

  • Bridge
  • Composite
    • あまりパターン名に結び付けて考えることは無い気がします。空気のようなもの。
  • Decorator
    • 特にinterfaceの実装について

振る舞いに関するパターン

  • Iterator
    • リスト処理。.NetならIEnumerable/IEnumeratorのことと言ってもいいです。
  • Observer
    • Functional Reactive Programmingのことは、よくわかりません。
  • Strategy
    • 高階関数があればパターンとして記述するほどのものではありません。

既にあるコードがつらい

  • ユーザ定義型を書いてください。
  • その巨大関数を何とかしてください。
  • LINQ to objectsを使ってください。
    • その巨大なfor文はどうやって読む/直すんですか。
    • 初めはSelect, Where, Any, FirstOrDefault, ToArrayだけ覚えればいいですから。
  • if文の多重入れ子を書かないでください。
    • 多重入れ子した上で変数を何回再代入したら気が済むんですか。
  • 無駄に可変なデータ構造を作らないでください。
  • どうしてそんな重要な手続きがクラスの利用側のほうに書かれているんですか。
  • 無駄にフラグを増やさないでください、フラグを関数に渡して内部で分岐させるなんて馬鹿げてます。
    • デリゲートを使うなり関数を切り分けるなりしてください。
    • 何でフラグが整数なんですか死んでください。
  • 何でもかんでも文字列にしないでください。
  • とにかくユーザ定義型を書いてください。

...つらい

本を読みましょう

私が読んで良いと思ったのはこのあたり。一部読みかけも含みますが……。

現実を見ましょう

上記を全て忘れて、

  • 「利口なビュー」アンチパターンで、
  • 僅かな抽象化を「共通モジュール」として行いながら、

日々をやり過ごしましょう。現実はそう甘くありません。 技術投資して生きるか、技術に投資しないで生きるか、道は二つに一つです。 それはビジネスモデルまで一直線な二者択一です。