CLOVER🍀

That was when it all began.

Scalaで学ぶイミュータブルなプログラミング

Scalaを勉強してそこそこ経ちますが、Scalaを勉強することになったきっかけは、かの有名な「コップ本」を購入してみたことです。それまでスクリプト言語もあまり学んできていなかったので、中心のプログラミング言語はほぼJava一色だったのですが、Scalaを勉強した時にかなり大きな衝撃を受けたのを覚えています。

特に、以下の2つはかなりインパクトがあったのを覚えています。

  • クロージャ、関数渡しをはじめとした高階関数
  • 不変なクラス、変数を基本とした、イミュータブルなプログラミング

前者は、以前に軽くエントリを書いているので、今回は後者の方をちょっと書いてみたいと思います。

例えば、以下のような何の変哲もないJavaのクラス。

public class Person {
    private String firstName;
    private String lastName;

    public Person() { }

    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
}

フィールドと対応するsetter/getterが付いた、普通のJavaBeansとしてのクラスです。Javaプログラマなら、こういうクラスに特に違和感はないでしょう。もちろん、他のプログラミング言語を学んでいる人からは冗長感がたっぷりあるとか、アクセスする時にset…とかget…とか面倒だとかいろいろ言いたくなることはあるでしょうけどね。

で、コップ本を読んだり、関数型言語の勉強をしているとだんだん上記のようなクラスに違和感を覚えるようになります。懐疑的になってくるは、そのsetterは本当に必要なのか?ということですかね。

フィールドを直接公開するな、setter/getter経由でアクセスしなさいというのがカプセル化の教えであり、それはそれでいいと思うのですが、変更できる必要はあるんですかね〜?というところはちょっと考えたいところだと思います。

Javaでは普通にクラスのインスタンスをメソッドの引数に渡すことができるので、以下のようなコードを普通に書くことができます。

public void addPersons(List<Person> persons) {
    persons.add(new Person("Jiro", "Tanaka"));
    persons.add(new Person("Taro", "Suzuki"));
}

pubilc void marry(Person man, Person wife) {
    wife.setFirstName(man.getFirstName());
}

ちょっと極端な例ですが、この手の引数に対して変更を加えてしまうようなメソッドは、Javaではたまに書いてしまうのではないでしょうか?こういう例はけっこうあるような気がしていて、例えば…

  • Commandのようなクラスを実行すると、実行メソッド呼び出し時に状態が変わってしまい再度実行すると結果がおかしくなる(再度実行する前には、初期化が必要とか…)
  • Collectionのソートを行うと、対象のCollection自身が変更される

などなど。

インスタンスが状態を持つこと自体は悪くないと思いますが簡単に変更できるような構造だと、時に見通しの悪いコードになってしまうことがあるような気がします。ローカル変数についても、同じことが言えます。スコープを最低限に絞って宣言するのがルールだと思いますが、再代入を安易にしているプログラムはちょっと読みにくい気がします。

個人的には、Javaではローカル変数は宣言と同時に初期値を設定し、以後はできる限り変更しないようにしています。初期値が条件によって変わるようであれば、以下のように書いています。

        boolean condition = ....;

        String value;
        if (condition) {
            value = "It's Ok!";
        } else {
            value = "Not enough...";  // この行を削除するとコンパイルエラー
        }

        // ローカル変数valueを使う何らかの処理
        System.out.println(value);

上記コードだと、ローカル変数valueの値はif文の全てのケースで値を決めなければコンパイルエラーとなるため、初期値の代入は1回になります。さらに宣言時にfinalを付けるとさらに良いのですが、実際のJavaのコードではあんまりローカル変数にfinalが付きまくっているコードは見かけることがないので、他と足並みを揃えるという意味では、そこまでやっていません。

また、戻り値がない(voidな)メソッドって、けっこう注意しないといけないと思うんです。コンソールに出力するとかならまだいいんですが、戻り値がないメソッドは、たいていインスタンスの状態を変更するものが多いので、setter以外だと外からは何をやっているのかが非常に推測しにくく、結局中の実装を覗きに行くことが多くなりがちなような気がします。

まとめると、クラスのインスタンスはコンストラクション時にほぼ値を決めてしまい、以降は状態はできる限り変更しないようにするとコードの見通しがよくなるんじゃないかなぁと思います。状態と操作をひとまとめにしたのがオブジェクトですが、あまり状態をコロコロ変えるとわかりにくくなると思うんですよ。そしてメソッドは極力、インスタンスの状態に応じた値を値を返すようにして、関数的に使えるように考えると良いのではないかな、と。

状態が変わらないとオブジェクト指向じゃない!!とかいうのは、ちょっと違うと思います。

Scalaだと、どちらかというと不変なクラスが基本です。例えば、先ほどのPersonクラスだと…

class Person(val firstName: String, val lastName: String)

のように書きます。ただ、Java版とは異なり、このクラスはイミュータブルです。また、Javaのfinalをvalキーワードでごく自然に使うことができ、かつScalaのifは文ではなく式なので、先ほどのローカル変数の例は以下のように書けます。

    val condition = ...

    val value = if (condition) "It's OK!" else "Not enough"
    println(value)

まあ、Scalaの場合はif式よりmatch式を使うことの方が多い気がしますが。

Collectionもイミュータブルなものが基本になっており、ソートや要素の追加を行っても処理を行ったインスタンス自身は変更されず、新しいCollectionが生成されるようになっています。

もちろん、イミュータブルなクラスはいいことばかりではありません。状態を変えようとするとインスタンスの数が増えてしまうので、パフォーマンスに劣りがちだったり、状態を変更できないとどうしてもやりづらい処理とかもあったりするでしょう。

そういう時まで無理にイミュータブルなクラスを使うことはないかもしれませんが、ちょっと意識を変えて変更不可を基本にコードを書いてみると今までと違った世界が見えるんじゃないでしょうか?コードの雰囲気が変わって面白いので、少しトライしてみてはいかが?