自分のクラスに`#to_a`を実装すべきじゃない(翻訳)|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社エンジニアの「?」を「!」に。原著者の許諾を得て翻訳・公開いたします。名サイト「rubyreferences.github.io」の作者でもあります。自分のクラスに#to_aを実装すべきじゃない(翻訳)えっと、このブログをときどき読んでる人なら言葉のキツさをご存知と思います。挨拶代わりに、Rubyの便利なイディオムのひとつであるKernel#Arrayメソッドについて軽くおさらいしておきましょう。名を「知らんけど、んなもんArrayに決まってんだろ!」というやつです。あるデータを用いて作業しているときに、1個の値か値の配列のどちらをも受け取るまたは返す可能性のあるメソッドが使えるとしましょう。その次に、両者の区別をなくして確実に1つの配列として扱う必要が生じたとしたら、次のように書けるでしょう。ま、わかるっちゃわかるこのステキなコード片の挙動は簡単に見落とされがちですが、ありがたいものですし、ほとんどの場合直感を裏切りません。Array(nil)はここでは割りといい感じですし、[nil]を返すのと比べて少々論理的ではないとはいえ、現場で欲しいものとしては十分過ぎるぐらいでしょう。直感はときに裏切られます。"EET"]な、何だこれは?Arrayメソッドは最初にval#to_aryを呼ぼうとし、応答がなければ最終的にTimeが最終的に実装するval#to_aを呼びます。#to_aryと#to_aの違いがなぜ重要かについてはこちらの記事をご覧ください。ここから貴重な教訓を得られました。Objectを表す場合は)。一体全体どうして...?逆に皆さんにお尋ねします。オブジェクトがコレクションでない場合、#to_aにはいったいどんな意味があるんですか?ほとんどの場合(Timeの場合とか)、このメソッドの存在によって引き起こされる問題は、Rubyでは「配列とタプル」を区別しないという事実が原因なので、ここでは「有意義な内部値を含むタプルに変換する」ということです。ここで「タプル」が指すものが今ひとつはっきりわからなかったので、社内Slackで相談しました。この変換を行う場合、専門に特化した名前を用いる場合と比べて、「標準的な」名前を用いるメリットはありません。少なくとも私が想像する限りでは、種類の異なるさまざまなオブジェクトを含む配列が1個あってarray.map(&:to_a)を呼んだときに、そのうちのいずれかがTimeのインスタンスであれば、「timeを構成する要素のリスト」が返されることを期待するものです。それでは何らかのバグが発生するということになります(配列が返されるのを期待しているところにTimeが返されるなど)。#to_aのような一般的過ぎる名前ではなく)もっと特化したメソッド名を選ぶほうが常に設計がクリーンになります。gemのデータ型で最近これと同じ問題が起きたのです。Array()のイディオムとどうにも相性が合わないので、メソッド名を#to_aから#latlngに変更することにしました。順序を入れ替えた)#lnglatメソッドが存在することの察しが付きますし、実際、座標をこの順序で渡すことを期待するAPIもあったりするのです。:lnglat)みたいな方法で順序を入れ替える手もありますが、(#to_aなんかではなく)メソッドを2つにする方が最も明快かつ見つけやすいように思えます。個人的には、せめて#lat_lngや#lng_latのようにアンダースコアを入れて欲しい気がしました。他の#to_<x>系メソッドはどうよ?「#to_aがそんなに邪悪なら、#to_hとか#to_sも捨てないとダメなの?」ということですか。そちらは捨てるべきではありません。理由は以下のとおりです。他の#to_<x>系メソッドはセマンティクス(意味)が違いますし、疑問の余地もさほどありません。#to_sは、オブジェクトをputsや文字列の式展開で表現する方法です。#to_hをJSON形式の時刻で使う場合、「意味を明確に保ったまま、オブジェクト全体を基本的なデータ型にダンプする」便利なイディオムとして使えます。実際、「オブジェクト全体を何らかのタプルにダンプする」適切かつ現代的な代替メソッドになっています。Array()と比べて)ずっと制約が強くなっていますし、変換しようのない値オブジェクトがある可能性が考えられる場合に何でもかんでも値をハッシュに変換するとはちょっと想像しにくいことです。#to_aがTimeに実装されている理由って?歴史的な理由であろうと信じています。暗黒時代に設計されたコアクラスたちは、(Timeクラスがそうであるように)多数の値を構成できるようにするときには、このような長ったらしい値のリストをコンストラクタで受けられるようにするしかなかったのです。Timeには未だにnewだのgmだのmktimeだのlocalだのさまざまなやり方があります。それというのも、位置で表現される引数だけでは、値の個数が1個から10個までありうる場合に正しいコンストラクタを設計するのが困難だったからです。したわけで、コンストラクタがあるなら、その逆の操作を簡単に行える「デコンストラクタ」的なメソッドも使える方が筋がとおっているように思えます。+0200ここで妙なのは、現在の日時を表現するためのキーワード引数コンストラクタもなければ、#to_hメソッドすらないことです。StringIO]既に言及したTimeの流れで言うと、特筆すべきはStructです。Objectの基礎となる言語のコアです。ご多分に漏れず、これも「これは配列なのか?」という疑問に陥っています。Objectを念頭に置いていただけるなら)Structはさらに変です。#to_aを実装しているだけではなく、#eachも実装してEnumerable全体をインクルードしているのです!。Objectというよりはむしろ、ドット記法が使える自由なハッシュである」と考えることでしょう。ここからもうひとつ、疑問の余地が残るルールが生まれます。(コレクションでないものに)#eachを実装すべきではない理由は同じです。Xがコレクションでない場合に「Xのそれぞれの値」を問う意味がどこにあるのでしょうか?相当曖昧でありながら、一部のコアクラスは未だにこの機能を誇示しています。端的に言えば、IO#eachが何を列挙するのかという話です。ほとんどの皆さんはもう答えをご存知でしょう($\または$OUTPUT_FIELD_SEPARATOR、あるいは文字列セパレータとして渡されたもので区切られた行が列挙されます)。これは直感的に言えば実装も不可能ですし、一番欲しいものにもならないでしょう。「スクリプト」とプレーンテキストの時代ならそのとおりでしたが、もうそんな時代ではないことをしかと認めましょう。今ならI/Oストリームやeach(ナントカ)はバイトだったりUnicodeコードポイントだったりバッファのチャンクだったりそうでなかったりします。IO#each_byteやIO#each_line(separator)はどちらも柔軟性と可読性が高くなっています。Stringには#eachがあり、Enumerableでした(I/Oで言うのと同じ意味で)が、この考え方は改められて現在は#each_byte/#each_char/#each_codepoint/#each_lineとなり、むき出しの#eachはなくなりました。instead」をお読みいただき、メソッド名をちゃんと意味のある#each_ナントカにしましょう。Enumerableは使わないでください。対象がコレクションでないのであれば、です。Enumerableしましょう。すれば#to_aがタダで手に入りますし、必要であれば効率よく再実装することもできます。「自分のコレクション」というコンテキストでうまく動く#to_aryもおそらく実装されるでしょう。大胆に申し上げると、私はHash#eachや#to_aをこれっぽっちもよいと思いません。Enumeratorの方がましなコードになるように思えます。Astronauts」みたいにのぼせ上がってるのかもしれませんね。Software」の記事に登場する、実装や設計をやらずに現実離れしたアーキテクチャばかり提唱する人を揶揄した言葉から来ています。「空飛ぶアーキテクト」とか「浮世離れのアーキテクト」とでも呼びましょうか。コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。チュートリアル第2版の監修および半分程度を翻訳、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れて更新翻訳中。enno.jpを運営。実は最近Go言語が好きで、Goで書かれたRubyライクなGoby言語のメンテナーでもある。仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。Storageを使ってはいけない(翻訳)#Tags#会社紹介#自社サービス#勉強会#Rails#PHP#デザイン#実績#週刊RailsウオッチCONTACTお気軽にご相談ください。(翻訳)CONTACTTechRachoでは、パートナーシップをご検討いただける方からのご連絡をお待ちしております。お気軽にご意見・ご相談ください。