はじめに
先日、Ruby でパターンマッチを試し、複雑なロジックを簡潔で明確なコードで表現できる強力なツールになり得ることを知りました。
実践的なプログラムでもどんどん試していきます!
パターンマッチをどこでどうつかうか
パターンマッチの使いみちはいくつか考えられますが、個人的に特に氣に入ったのは深い入れ子構造の Hash や Array の値からの取得です。
API レスポンスとして返ってきた JSON から値を取り出す
API によってはレスポンスの JSON が複雑な構造をしている場合があります。
例えば、PocketSign APIには以下のように入れ子の深いレスポンスが返るものがあります。
response = <<~RESPONSE
{
"results": [
{
"digitalSignature": {
"result": {
"verification": {
"id": "80375018-3502-4eb7-b422-0898dcf140b5",
"result": "RESULT_OK",
"hashAlgorithm": "HASH_ALGORITHM_SHA256",
"digest": "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
"signature": "n4bVa46x6/Ud44p7+zHMPpsTjZG7yqtnqc2Wlcr...",
"createdAt": "2023-09-02T20:15:27.613966Z"
},
"certificate": {
"id": "1845533e-42ae-42db-9ed5-862dece5a4e3",
"type": "TYPE_JPKI_CARD_DIGITAL_SIGNATURE",
"createdAt": "2023-07-31T11:00:11.506906Z"
},
"certificateStatus": {
"id": "b3ee5560-206b-4871-aa67-a47389fbafd6",
"status": "STATUS_GOOD",
"checkMethod": "CHECK_METHOD_CRL",
"checkPurpose": "CHECK_PURPOSE_SIGNATURE_VERIFICATION",
"sourceUpdatedAt": "0001-01-01T00:00:00Z",
"createdAt": "2023-09-02T20:15:27.613966Z"
},
"certificateContent": {
"subject": "OU=P8N for digital signature+OU=PocketSign Inc.,O=P8N-MOCK,C=JP",
"validity": {
"notBefore": "2022-06-06T04:56:54Z",
"notAfter": "2027-06-05T04:56:54Z"
},
"crlDistributionPoint": "CN=City-0 CRLDP,OU=Prefecture-0,OU=CRL Distribution Points,OU=P8N for digital signature,O=P8N-MOCK,C=JP",
"jpkiCardDigitalSignatureContent": {
"commonName": "ナカモト サトシ",
"substituteCharacterOfCommonName": "0000",
"gender": "2",
"dateOfBirth": "118991201",
"address": "福岡県飯塚市幸袋576-14 e-ZUKAトライバレーセンター",
"substituteCharacterOfAddress": "0000000000000000000000000"
}
}
}
}
}
]
}
RESPONSE
Enumerable モジュールのメソッドや dig メソッドを使ったやり方
まずはパターンパターンマッチを使わない場合のやり方を考えてみようと思います。
パターンマッチを使わない場合は、Enumerableモジュールのメソッドやdigメソッドを用いて狙った値を取得することができます。
require 'json'
parsed_body = JSON.parse(response)
digital_signature_result =
parsed_body
.fetch('results')
.find { |result| result.key?('digitalSignature') }
.fetch('digitalSignature')
verification_result = digital_signature_result.dig("result", "verification", "result")
#=> "RESULT_OK"
require 'json'
parsed_body = JSON.parse(response)
verification_result =
parsed_body.dig("results", 0, "digitalSignature", "result", "verification", "result")
#=> "RESULT_OK"
パターンマッチを使ったやり方
パターンをマッチを使うと、以下のようにデータ構造を明示した上で深い入れ子構造の中の値を変数に代入することができます。
データ構造が見て取れるのでうまく行けばドキュメントとしての機能も果たせるのではと期待しています。
require 'json'
parsed_body = JSON.parse(response, symbolize_names: true)
parsed_body => {
results: [
{
digitalSignature: {
result: {
verification: {
result: verification_result
}
}
}
},
*_other_results
]
}
verification_result
#=> "RESULT_OK"
ハッシュのパターンマッチで使えるキーはシンボルのみなので注意してください。
異常系のレスポンスにも対応したい場合は、正常系と異常系の両方のパターンを指定します。
get_verification_result =
lambda do |results|
case results
# 署名が正常に作成された場合
in [
{
digitalSignature: {
result: {
verification: {
result: verification_result
}
}
}
},
*_
]
verification_result
# 署名が正常に作成されなかった場合
in [
{
digitalSignature: {
error: _
}
},
*_
]
"署名が正常に作成されませんでした"
end
end
require 'json'
parsed_body = JSON.parse(response, symbolize_names: true)
get_verification_result.call(parsed_body.fetch(:results))
#=> "RESULT_OK"
get_verification_result.call([{
digitalSignature: {
error: {
code: 9,
message: "The request cannot proceed. Please refer to the documentation for more details."
}
}
}])
#=> "署名が正常に作成されませんでした"
APIから想定外のレスポンスが返って来てパターンにマッチしない場合は、エラー(NoMatchingPatternError
)となります。このエラーを回避したい場合は「その他の条件」としてelse
節を指定します。
一つ氣になること
シンボルってガベージ・コレクションされるのでしたっけ?
ハッシュのパターンマッチで使えるキーはシンボルのみなので、でっかいJSONをまるごとシンボルキーに変更したら楽なのですが、果たしてそれで大丈夫なのでしょうか?
Symbol GC
公式ドキュメントによるとシンボルはガベージ・コレクションされるようです。
2.2.0 以降においては、テーブルに記録された情報は Ruby によって GC されます。すなわち、ある使わなくなったシンボルのテーブル上の情報はGCによって削除されます。
とはいえ、一時的でも知らないところで要らないシンボルが生成されるのは氣持ち悪いのでHashのシンボルキー化のスコープはなるべく必要最低限に狭めたほうが良さそうな氣がしています。
さいごに
パターンマッチの活用方法の一つとしてでっかいJSONから値を取り出すことをやってみました。
同僚が Elixir や Rust のパターンマッチを利用してスマートに課題解決しているのをみたことがあります。
これからも引き続きいろんな場面でパターンマッチを試していこうと思います。