他プログラミング言語と仲良くなるための心得
はじめに
この記事は SmartHR Advent Calendar 2024 シリーズ2 の24日目の記事です。
こんにちは、株式会社SmartHR でプロダクトエンジニアをしているN9tE9です。
突然ですが、他プログラミング言語を学んだり、業務で利用経験が浅いプログラミング言語を利用するときハードルが高いと感じることはないでしょうか?
僕は前職では golang をメインで使っていましたが、転職を機に Ruby(Ruby on Rails) をメインで使うようになりました。
Ruby を学ぶ過程で感じた新しい言語を習得する際のハードルがありましたが、業務上で支障がない程度で Ruby を書くことができるようになりました。
今回はこの経験を通して、新しいプログラミング言語を学ぶ心得に気付けたので、それをお伝えできたらと思います。
どのようなハードルがあったのか?
僕は、主に以下の2つの観点からハードルを感じました。
- コーディング時のハードル
- デバッグ時のハードル
コーディング時に感じたハードル
Ruby の表現の豊かさ
Ruby は、非常に表現が豊かな言語で、同じ処理を書く場合でも複数の表現方法があります。
この表現方法を覚えるという部分で若干苦労しました。
例を挙げると numbered parameter や キーワード引数の省略、スプラット演算子を使った配列の展開 といったもの具合のものです。
魔術
動的型付け言語のコードジャンプが一意に定まらない場合にロジックを見失ってしまうケースが最初の方に多々ありました。
Rails は、便利なメソッドを生やしてくれる一方で、自分が認知していない多くのメソッドが自動的に実装されます。
自分が認知していないメソッドが呼び出されている場所は、公式ドキュメントや技術ブログから挙動を読み解く必要がありました。
メタプログラミング
Ruby は、 __send__
や public_send
といったメソッドを利用してメタプログラミングができます。
メタプログラミングが利用されている場所ではどのメソッドが呼び出されているかコードジャンプができないので、コードベース全体に対して grep をして追う必要がありました。
デバッグ時に感じたハードル
インタプリタ言語の特性
ruby は、インタプリタ言語なのでランタイム時に呼び出すメソッドやクラスが決まります。
デバッグ時は、binding.pry
をコード中に仕込んで実行し、コンソールでデバッグをするという具合です。コンソールでは .inspect
といったメソッドで中に入っている値を確認しつつ、デバッグを進めます。
このデバッグ方法は、処理や値を追いやすく開発者体験としてもとても良いですし、業務でもよく利用します。
一方で、ループ中に case 文の分岐がある上で __send__
や public_send
などのメタプログラミングが実装されていた場所はかなりデバッグが難しいです。
このループ中の分岐で NoMethodError のエラーが返されるとどのメソッドを想定してたのかがわからず途端に処理が追いづらくなります。
ハードルの高さをポジティブに捉える
新しいプログラミング言語を学ぶことは、かなりの労力を使います。
Ruby でこういう表現を読み解けるようになったということを1つ1つハードルとして考えると精神的にしんどいです。
Ruby の表現の豊かさや柔軟性は、ハードルではなく言語の個性であるポジティブに捉えるようにしました。
表現の豊かさを面白さとして捉える
実装に複数の表現方法があることは、メンバーにより好みの表記が違います。
シンプルなところだとブロックの { }
を利用した表現と do end
の表現があります。
{ }
を使った方がワンライナーで表現できるというメリットがあります。
一方で、1行が長くなってしまうと処理が複雑になってしまうので do end の複数行に分解します。
PR のレビューコメントを見るとレビュワーによってワンライナーで表現する許容する複雑さの上限が異なっている印象を受けました。
ここは実装者の好みなので、それを取り入れるかどうかは実装者に委ねられる点は、golang を書いている時は見られなかった光景で新鮮でした。
このような光景が新鮮だった要因は、 golang 言語仕様がシンプルなことに起因していると考えます。そのため、誰が書いても同じような実装になってしまうという言語設計の背景があると思っています。
実体験としては、今のチームにジョインした初期にオブジェクトの値を map で回して特定の値の配列を取得したいときに do end を利用して配列を取得していました。
& や { }
のブロックで表現できるというコメントをもらった時に、短く書くことが Ruby らしい書き方なのかなと思いをはせました。
メタプログラミングは Ruby の言語仕様の背景から考えるきっかけになった
メタプログラミングが許容され推奨されていることを考えるきっかけになりました。
現状、以下のような自分なりの回答を持ちました。
- データを受け取る変数や定数に型が存在しない
- シンボルやメソッドをコード上で動的に生成できる柔軟性がある
-
respond_to?
のような存在確認のメソッドが充実している
Ruby は、データを受け取る変数や定数に型が存在しません。そのため、メタプログラミングを利用したメソッドコールにより値が返ってきたとしても柔軟に受け取れます。
また、シンボルやメソッドの実装を動的に生成できる点でメタプログラミングをしやすい土壌が整っていると感じました。
例えば、"one", "two", "three" の文字列を持つ配列からこの文字列のアッパーケースを返すメソッドを動的に実装できます。
class Hoge
["one", "two", "three"].each {|x| define_method(x) { x.__send__(:upcase) }}
end
hoge = Hoge.new
pp hoge.one # ONE
pp hoge.two # TWO
pp hoge.three # THREE
デバッグが困難なコードはよりデバッグがしやすいコードを書くきっかけになる
先ほど、ループ中の分岐で、メタプログラミングでメソッドが呼ばれている場合はデバッグが難しいと上げました。
コード例を上げると以下のような具合です。
class Processor
def initialize(data)
@data = data
end
def process
@data.each do |item|
case item[:type]
when :a
send("handle_a", item)
when :b
send("handle_b", item)
else
send("handle_default", item)
end
end
end
private
def handle_a(item)
puts "Handling type A: #{item[:value]}"
end
def handle_b(item)
puts "Handling type B: #{item[:value]}"
end
def handle_default(item)
puts "Handling default: #{item[:value]}"
end
end
data = [
{ type: :a, value: "foo" },
{ type: :b, value: "bar" },
{ type: :c, value: "baz" }
]
Processor.new(data).process
以下のような方針でリファクタリングをするきっかけです。
- 分岐の条件を述語メソッドとして実装しガード節で実装する
- メソッドをハッシュで呼び出せるようにする
ガード節を利用してリファクタリングは、以下のような形です。
class Processor
# 中略...
def process
@data.each do |item|
return handle_a(item) if type_a?(item)
return handle_b(item) if type_b?(item)
handle_default(item)
end
end
# 中略...
end
Processor.new(data).process
メソッドをハッシュで持つリファクタリングは、以下のような実装を目指します。
class Processor
handlers = {
a: method(:handle_a),
b: method(:handle_b)
}
def process
@data.each do |item|
(handlers[item[:type]] || method(:handle_default)).call(item)
end
end
# 中略
end
実際のコードではこのように簡単にリファクタリングすることは難しいです。
ただし、もっと開発者体験の良い形で実装できる方法はないかというきっかけを与えてくれます。
Ruby や Ruby on Rails を習得するプロセス
Ruby を書く上のマインドセットで慣れない部分をポジティブに捉えながら進めました。
一方で、抽象的なスキルを意識しながらRubyやRuby on Railsを書くことで効率良く身につけることができることを実感しました。
抽象的なスキルからアプローチする
前職は、さまざまなアプリケーションアーキテクチャに触れる機会がありました。
具体的には、 MVC、DDD+レイヤードアーキテクチャ、クリーンアーキテクチャ といった具合です。
Ruby を利用してweb開発する場合は、Ruby on Rails が採用される場合が大半です。
Ruby on Rails で開発する場合、 MVC の設計に沿います。
僕は以下を考えながら Ruby を書くことを意識していて効率的に身につけました。
- Rails Way を守りながら実装する場合の Ruby の適切な書き方はどのようなものなのか?
- MVC で Ruby の柔軟性をどのように生かせば良いのか?
これらの考え方は、ブロックの書き方やメタプログラミングといったものよりもっと広範囲をカバーするものです。
そのため、実装時に以下のような疑問は極力排除しつつ実装を進めることができました。
- この層はどのような責務を持っているのか?
- この処理に関するメソッドをどこに実装すべきなのか?
- どのクラスを継承すべきなのか?
複数の分からないことを並行して進めると一層複雑に見えます。
一般的な設計プラクティスが頭に入った状態で業務のプロダクトの機能を実装できたことは、プログラミング言語習得を効率的に行えたと思っています。
新しいプログラミング言語を学ぶ心得(まとめ)
いかがだったでしょうか?
今回、ruby を学んだ経験を例に新しいプログラミング言語を学ぶうえでのマインドセットや効率的な習得のアプローチを話しました。
この心得を一言で表現すると、郷【Go】にいれば郷【Go】に従え(ボケのためにルビ(Ruby)を振っています)です。
それぞれのプログラミング言語に設計思想があります。
golang には golang の書き心地がありますし、Ruby には Ruby の書き心地があります。
そのため、使い慣れている言語で推奨されている方法を他の言語でも推奨すべきという押し付けはプログラミング言語の習得を阻害すると思っています。自分がその言語に適用するという形でプログラミング言語を習得するというマインドセットを持つことが重要だと考えます。
アプリケーションアーキテクチャの知識といった抽象的なスキルは、プログラミング言語の習得に大きく寄与します。
Discussion