Blog株式会社マネーフォワード公式開発者向けブログです。技術や開発手法、イベント登壇などを発信します。サービスに関するご質問は、各サービス窓口までご連絡ください。型の難しさと、そのちょっとした緩和策についてこんにちは。最近、お寿司を食べるとお寿司を食べたくなることに気が付きました。今日はString#scanの型の話をします。String#scanの挙動さて、まずはString#scanの挙動についておさらいしましょう。String#scanはパターンを受け取り、受け取ったパターンにマッチした文字列の配列を返すメソッドです。次に簡単な例を示します。"world"end文字列中からマッチした部分文字列を列挙しています。文字列処理を書く際に便利そうですね。ブロックを渡して、マッチした文字列を引数にブロックを実行することもできます。String#scanはパターンとして文字列も受け付けます。["Hello"]String#scanに渡す正規表現がグルーピングを含む場合、戻り値の配列のそれぞれの要素は、グルーピングの数の長さの配列になります。詳しく知りたい場合には、公式ドキュメントを参照すると良いでしょう。(英語ドキュメント)String#scanの型このメソッドの型は以下のRBSで表されます。selfend型の解説RBSに不慣れな読者のために、簡単に解説をします。RBSに詳しい場合は、次の「この型を扱う際の問題点」まで読み飛ばして構いません。endはクラス定義を表します。これはRubyと同じです。...の部分がscanメソッドの定義を表します。3行目の先頭にある|がメソッドのオーバーロードを表します。このメソッドは、次の2つの定義に分けられます。これはそれぞれブロックを受け取らない場合と、受け取る場合に対応しています。self今回はブロックの有無は大きな争点ではないので、ここでは前者のブロックを受け取らない場合を解説します。pattern)は、引数の型を表します。これは、patternという名前の引数を1つ受け取り、その型はRegexpもしくはstringであることを表します。2そして->の右側はこのメソッドの戻り値です。Array[String]]は、戻り値はArrayであることを表し、そのArrayの要素はStringかArray[Srting](文字列を要素に持つ配列)であることを表します。この型を扱う際の問題点この型定義を使ってString#scanを使ったコードの型検査を行おうとすると、問題が起きます。次のコードをSteepを使って検査してみましょう。単語をscanして、それを大文字にして出力するだけのコードです。"WORLD"]Steepで解析するために、以下のSteepfileを置きます。checkコマンドを実行すると、次のように出力されます。~~~~~~おや、Rubyコードとしては実行できるのに、型検査は失敗してしまいましたね。失敗の原因は、引数の正規表現にグルーピングが含まれているかどうかで、戻り値の型が変わることにあります。このケースでは戻り値の型がArray[Array[String]]になることはありません。Steepは正規表現のグルーピングの情報を知らないため、その可能性を排除できません。そのため、Array[String]に対してupcaseメソッドを呼び出せないというエラーが発生してしまいます。ワークアラウンド型検査を通したい場合、次のワークアラウンドが有効です。"WORLD"]この例ではwordをw変数に代入しています。@typeコメントでStringと宣言しています。3なお_変数に代入しているのは型のキャストです。Steepは_に代入された値をuntypedとして扱います。wordとしてしまうとArray[String]をString型の変数に代入できずエラーになってしまうため、_を使ってuntypedを介して代入する必要があります。4この方法の欠点は安全ではないことです。wが本当にArray[String]ではなくStringであることをSteepは保証せず、プログラマが気をつける必要があります。正規表現を後からグルーピングありのものに変えた場合、実際の型とSteepが想定する型に違いが出てしまいます。この欠点は型検査の利点を損なってしまいます。次のように書いて型エラーを回避することもできます。"WORLD"]wordがStringのインスタンスであることをcaseを使って明示的にチェックしています。これなら安全に型検査を行えますが、少し冗長になってしまいますね。ここでwordがString以外になることはないので、これは本来不要なチェックのはずです。Steepを使って検査しようとすると、問題が生じます。した緩和策このように扱いづらいString#scanですが、今回ちょっとした緩和策を実装してみました。github.comこのPRがマージされると、引数が文字列の場合にこの問題が発生しなくなります。以下のコードは現在は型エラーが発生してしまいますが、このPRが適用されるとエラーにならなくなります。["HELLO"]このPRでは、引数が文字列の場合にはArray[Array[String]]が返る可能性を排除して、Array[String]だけが返るように変更しました。引数が文字列の場合はグルーピングが存在することはないため、Array[Array[String]]を排除できます。いえ正規表現を引数に渡した場合には、この記事で説明した問題が依然として発生してしまいます。5最後にString#scanの型と、それによって起こる問題を紹介しました。その問題に対応するPRも紹介しました。String#scanやRBSに少しでも興味を持っていただければ嬉しいです。RBSについて詳しく知りたい場合は、RBSの公式ドキュメントを参照すると良いでしょう。↩stringのSが小文字であることが気になるかも知れませんが、今回はあまり気にしなくて良いです。Stringと、暗黙にStringに変換できるなにかが該当します。word).upcaseのように書くこともできます。実際、Stringを引数に渡すケースは少ないような気がしています。カスタムルールを作るこんにちは。文字列の生成や検証エンジニアの澤田です。読者になるこのブログについてはてなブログをはじめよう!moneyforwardDevさんは、はてなブログを使っています。ブログを報告する引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。