ブロック構文はいいぞ〜


ブロックを受け取る関数を使って一般化された問題を解いておくと捗るかもな話@Meguro.rb

お前誰よ(自己紹介)


  • 名前と連絡先
    • 名前はsxarpでオナシャス
    • 最近始めたツイッター@_sxarp
  • 趣味とか
    • いつもは4chanという海外の画像掲示板にいます
    • ちな、ごちうさスレによく張り付いてます(クッソどうでもいい情報)

技術的興味/バックグラウンド


  • 前までどんなことやってたか
    • 元はPythonばかり書いてました
    • 機械学習系の研究室にいたので
  • 今どんなことやってるか
    • Elixirに釣られて今の会社にジョイン
    • でもRailsはちゃんとマスターしたいので今はRuby書いてます
  • 最近のハマってること
    • 某Hな本でHaskellを勉強中
    • そろそろmonadが何なのか分かりそう(願望)

今日の発表の大まかな流れ


ブロックを受け取る関数を作ったり使ったりすると便利だよという話をします

  • ブロック構文の簡単な説明
    • Rubyでの使い方/作り方
  • ブロック構文/ブロックを受け取る関数について考察
    • 普通の関数との違い
    • どんなときに使う/作るのが良いか?
  • 仕事でブロックを受け取る関数を自作したらわりと捗った話

スライド: https://sxarp.github.io/slides/ruby_block

ブロック構文とは


ブロック構文は普段からよく使ってるかと思います、例えばArray#eachとか:

[1, 2, 3].each do |item|
   puts item
end

ブロック構文の作り方


逆にブロックを受け取るeachみたいなメソッドは以下のように作れます。引数の定義にProcオブジェクトを使うだけOKです。

def each(&do_something_block)
    for index in 0..(self.length)
       item = self[index]
       do_something_block.call(item)
    end
end

で、ブロック構文って結局何なのさ?


なんの役に立つ?

  • 共通ロジックを取り出すことでDRYにできる
    • 例えばArray#eachは配列の各要素に個別に処理をするというロジックを抽出したもの
    • eachを使えば各要素への処理だけ書けば良く、イタレーション部分のロジックは書かなくて良い

では、普通の関数とは何が違うのか? 共通ロジックの抽出という目的は関数も同じだけれど...

ブロックを受け取る関数と普通の関数との違い

違いとしては、普通の関数とブロックを受け取る関数では、抽出するロジックの階層が異なります

ここでいう階層とは以下のような入れ子構造のことです:

上の階層のロジック {
      下の階層のロジック
}

上の階層のロジックによって下の階層のロジックが呼び出される関係にあります

階層構造の具体例

たとえば以下の例だと:

if x > 100
    puts x+1
end

上の階層のロジックは:

if x > 100
    ....
end

下の階層のロジックは:

puts x+1

ブロックを受け取る関数と普通の関数との違い

下の階層を抽出するのが普通の関数

def puts_x_plus_one(x)
    puts x+1
end

上の階層を抽出するのがブロックを受け取る関数

def do_something_when_over_100(x,  &block)
    block.call(x) if x > 100
end

上の階層を抽出すると何がうれしい?


  • 目の前の問題に固有なロジックに集中できる
    • 下の階層のロジックだけ書けばよく、共通ロジック(上の階層)を書かなくてよい
    • 簡易的なDSLと呼べるかも
  • コードの抽象度を高められる
    • 下の階層のロジックを切り替えることで色んな問題に適用できる
    • 仕様の変更や追加に強い

最近仕事でブロック構文を使った話

やりたいこと

  • 目的としては2つのバラバラに管理されていたモデルを一対一に対応付けたい
  • 具体的には、キャラクターのモデルと図鑑のページのモデルを論理的に紐付けたい

要件

  • できるだけ自動化したい
  • そのために色んな条件:名前や生成された時期等で対応ペアを抽出したい

色んな条件で? $\rightarrow$ ブロック構文が使えそう

対応条件をブロックで渡せる関数

2つの配列から、ブロックで渡した条件で対応ペアを抽出できるメソッド(#match)を作りました

渡されたブロックがtruthyな値を返すとき、対応ペアとなります:

pairs  =  ours.match(theirs) do |our_item, their_item|
    specified_condition(our_item, their_item)
end

なお、続けていくつも条件を適用したいので、マッチしたアイテムは逐次にpopしていく仕様です

実装コードはこんな感じです

実際にどんなふうに使ったか

まずは名前で対応付け:

pairs  =  characters.match(pages)  do  |chara,  page|
    chara.name  ==  page.title
end

名前でダメなやつは時期で対応付け:

pairs  =  characters.match(pages)  do  |chara,  page|
    same_month(chara.created_at,  page.created_at)
end

それでもダメなやつはidを直接指定して対応付け:

pairs  =  characters.match(pages)  do  |chara,  page|
    chara.id == 50 && page.id == 100
end

#matchを使うと何が良かった?


何らかの条件でペアを抽出するという汎用的な(ブロックを受け取る)関数を作った結果、どんな対応条件でも後から簡単に試したり追加したりできた


未確定な/変動しうる仕様をうまくカバーできる一般的な問題を見つけて解いた結果

まとめ:ブロック構文はいいぞ


ブロックを受け取る関数を使って、良い感じに一般的な問題を解いておくと、些細な仕様の変更や追加には余裕で対応できるコードが書けていいゾ~これ





おわり