こんにちは、マネーフォワードでスマホアプリの開発を担当しています高地です。
ちょっと釣り気味なタイトルになっていますが、Swiftはマルチパラダイム言語と言われており、関数型のエッセンスも持っています。 簡易的な文法の言語ですので、関数型を学ぶきっかけづくりには、丁度よい言語だと思います。今回は、Swiftを使ってimmutableについてピックアップしてみました。
記載してありますサンプルに不備がありましたので、修正を行いました。ご指摘下さった方ありがとうございました。 (2015/03/24)
immutable(イミュータブル)
そもそもimmutableとは何でしょうか?wikipediaによると
オブジェクト指向プログラミングにおいて、イミュータブル(immutable)なオブジェクトとは、作成後にその状態を変えることのできないオブジェクトのことである。対義語はミュータブル(mutable)なオブジェクトで、作成後も状態を変えることができる。...イミュータブルなオブジェクトを使うと、複製や比較のための操作を省けるため、コードが単純になり、また性能の改善にもつながる。
一度作成された、オブジェクトは不変であるということですね、これをSwiftで説明していきます。
immutable変数
まずはimmutableな変数の宣言は
let str: String = "家計簿"
変数の前にletというキーワードを定義することで、immutableな変数になります。次のように再代入を行おうとしますと
// "家計簿"から"無料家計簿"という文字列に変更したい str = "無料家計簿"
Cannot assign to 'let' value 'str'とコンパイルエラーになりますので、wikipediaに書かれていた、「作成後に状態を変えることができない」という条件を満たしています。初期化された変数を変更したい場合は、letではなくmutableなvarでの宣言に切り替える必要があります。
// 家計簿から無料家計簿 文字列に上書きする var str: String = "家計簿" str = "無料家計簿"
immutableクラス
同じくletをつけることでimmutableにすることができます。
class Kakeibo { var input: Int = 0 } let kakeiboObj = Kakeibo() kakeiboObj = AnyObject // ←コンパイルエラーになる
AnyObjectを代入しようとするとCannot assign to 'let'...と変数と同じくコンパイルエラーになり、狙った動作になります。 ですが、注意点としまして、プロパティとして定義されている、inputの定義がvarになっているので、プロパティに対してはmutableとして扱えます。
let kakeiboObj = Kakeibo() kakeiboObj = AnyObject // ←コンパイルエラーになる kakeiboObj.input = 1000 // ←OK kakeiboObj.input = 5000 // ←OK
プロパティの値も初期代入以外は許可しないのであれば、letで宣言しておいたほうがよいでしょう。
それでは、immutableなオブジェクトを再代入した場合、どのような事がおきるのでしょうか?immutableに保つことはできるのでしょうか?Kakeiboクラスのオブジェクトを再代入するサンプルを書いてみます。
let kakeiboObj = Kakeibo() let otherObj = kakeiboObj kakeiboObj.input = 1000 otherObj.input = 300
kakeiboObj変数の中にあるKakeiboオブジェクトをotherObj変数に代入し異なる値をinputプロパティに代入してみました。kakeiboObj変数とotherObj変数の状態を確認してみます
kakeiboObj // {input: 300} otherObj // {input: 300}
kakeiboObj変数へ初期代入した{input 1,000}値は上書きされてしまいました。 Swiftにおけるclassのオブジェクトの代入は参照渡しになってしまうので、otherObj変数への変更はkakeiboObj変数まで伝搬してしまいます。このような症状を副作用があると表現します。副作用があるコードは開発者の意図していない箇所でbugを発生させる原因にもなりますので、副作用が少ないコードが理想です。 この問題を解決するにはSwiftではclassを使わず、struct構造体で(※C言語でいう構造体とは機能面で異なります)次のように記述します。
struct Kakeibo { var input: Int = 0 } let kakeiboObj = Kakeibo(input: 1000) var otherObj = kakeiboObj otherObj.input = 300
structはclassとはinputプロパティの初期化方法が異なります。それではkakeiboObj変数とotherObj変数の内部にあるKakeiboオブジェクトの変化をみてみたいと思います。
kakeiboObj // {input: 1,000} otherObj // {input: 300}
otherObjに代入した値がkakeiboObjectに伝搬していないのが分かります。Swiftにおいてclassとstructの大きな違いは
classは参照渡し、structは値渡し
という特性の違いがあります。immutableなオブジェクトを意識するのであれば、classよりはstructをメインで使う方が向いています。最近の海外のblogを見ていましても、classよりもstructによる定義を進めている記事が多いように感じます。
上記のstructを使ったサンプルですが、otherObj変数がvar(mutable)というのがちょっと気になります。 せっかく元をstructとして宣言しても再代入先がmutableというのはいまいちなので、otherObj変数への代入もletで宣言してみます。ただ、そうすると、再代入先のotherObjでの使い回しが悪くなるので、ちょっとした細工を行います。
struct Kakeibo { let input: Int let output: Int func input(genkin: Int) -> Kakeibo { return Kakeibo(input: genkin, output: output) } } let kakeiboObj = Kakeibo(input: 1000, output: 5000)// {input: 1000, output: 5000} let otherObj = kakeiboObj.input(300) // {input: 300, output: 5000} outputの値も複製されている
inputというメソッドを用意してあげて、その中で、Kakeiboオブジェクトを返すようにします。 そうすることですでに生成されているkakeiboObj変数のoutputプロパティ(値:5000)はちゃんと複製されつつ、新たにinputプロパティに値300をセットしたKakeiboオブジェクトがotherObj変数に生成されました。 この方法を使えば、副作用の少ない、immutableなオブジェクトを生成できます。
immutable変数を改変できるか
letで宣言したimmutable変数にも例外があり、初期代入以外は改変はできませんが、extensionをつかった機能拡張の場合は、いくらでも変更できてしまいます。
それではimmutableなArray変数を使って、改変できるか試してみます。
let arr = [1, 2, 3, 4, 5] arr.append(6)
これは、当然ながらImmutable value of type only has mutating members....ということでimmutableエラーになります。これを
extension Array { func add(obj: T) -> Array { var mutable = self mutable.append(obj) return mutable } } let arr = [1, 2, 3, 4, 5] let arrAdded = arr.add(6) arr // [1, 2, 3, 4, 5] arrAdded // [1, 2, 3, 4, 5, 6]
immutableなarr変数であっても、mutableにすることができます。addメソッド内で、mutable変数をつかって、Arrayデーターを変更してそれを返してるので、immutableな変数でも更新されているかのように見えているわけです。
とすることで、extensionのなかで、selfの複製先であるmutable変数を改変し、それを返すことで値の拡張を行っています。immutable変数であるarrの改変はできなかったのですが、extensionを使って、それっぽい振る舞いにはなんとかできました。
まとめ
immutableは副作用の少ないコードを書くのには欠かせないものです。とくにアプリを作成していると、threadを使った並列処理を書くケースも多くなります。自分で作成したオブジェクトが意図しない箇所で書きかえらてしまうクリティカルな問題を引き起こすケースもでてきます。それを防ぐ一番手軽な手段はletを使ってimmutableにしておくことです。
個人的に、メリットだと感じるところとしまして、他人が書いたコード解析するときにletが定義されていれば、初期化の箇所だけを意識をすればよいので、コード解析の効率性も高まりました。
最後に
マネーフォワードでは、Swiftが大好きなエンジニアを募集しています! みなさまのご応募お待ちしております!