Rubyパターンマッチに

闇の力が備わり最強に見える

283プロ/346プロ アイドルプロデューサー

黒曜
(@kokuyouwind)

自己紹介

  • 黒曜(@kokuyouwind)

  • アイマスプロデューサー
    (283/346兼務)

  • 担当アイドル
     櫻木真乃(283プロ)
     白菊ほたる(346プロ)

  • 副業でWebエンジニア@Misoca

担当アイドル紹介-櫻木真乃

ほんわかした癒やし系アイドル。
illumination STARSという
3人ユニットのセンター。
心優しい性格だが、
センターという役割に悩む
責任感の強い一面も。

イルミネはエモの塊

担当アイドル紹介-白菊ほたる

所属事務所が3連続で倒産した
不運な少女。思考が後ろ向き。

それでもアイドルを目指すのは、
不幸を撒き散らした自分でも
誰かを幸せにしたいから。

かこほたは運命

Rubyの
パターンマッチの
話をします

Rubyのパターンマッチ

  • 条件分岐とデータ分解を兼ねる機能

  • Ruby 2.7.0 で実験的に導入された

    • 関数型プログラミング言語からの輸入

  • Rubyらしさを壊さないように仕様策定

Rubyらしさ

instance_eval

TracePoint

Binding

Flip Flop

prepend

Rubyパターンマッチに

闇の力が備わり最強に見える

アジェンダ

  • Rubyパターンマッチの概要

  • 闇の力で強化する

  • 闇の力でできることを考える

  • まとめ

全サンプルコードは以下リポジトリにあります

$ git clone https://github.com/kokuyouwind/pattern_match_demo
$ cd pattern_match_demo
$ bundle install

$ bundle exec ruby -W:no-experimental src/01_array.rb
# or
$ export RUBYOPT=-W:no-experimental
$ bundle exec ruby src/01_array.rb

アジェンダ

  • Rubyパターンマッチの概要

  • 闇の力で強化する

  • 闇の力でできることを考える

  • まとめ

パターンマッチ

  • case文の分岐で in pattern を使える

  • pattern 部分にはいろんなパターンを
    組み合わせて書ける

    • Variable Pattern

    • Array Pattern

    • Hash Pattern

    • etc

正確なパターン定義は
辻元さんのスライド
見よう!

パターン概要
Array Pattern

Array Pattern

# 配列の要素ごとにパターンマッチできる
case [1, 2, 3]
in [2, _, _]
  fail # マッチしない
in [1, x, y]
  puts "x: #{x}, y: #{y}" # => x: 2, y: 3
end

Array Pattern

# * を使って残りとマッチできる
case [1, 2, 3]
in [x, *y]
  puts "x: #{x}, y: #{y}" # => x: 1, y: [2, 3]
end

# 1行でinを使ってパターンマッチできる
[1, 2, 3] in [x, _, _]
puts "x: #{x}" # => x: 1

Array Pattern

# destructを定義してればなんでもマッチできる
class Tester
  def self.deconstruct
    [1, 2, 3]
  end
end

case Tester
in [_, x, _]
  puts "x: #{x}" # => x: 2
end

パターン概要
Hash Pattern

Hash Pattern

# ハッシュのキーごとにパターンマッチできる
case { first: "Hotaru", last: "Shiragiku" }
in { first: "Kako", last: _ }
  fail # マッチしない
in { first: "Hotaru", last: name }
  puts "name: #{name}" # => name: Shiragiku
end

Hash Pattern

# 1行でinを使ってマッチできる
# ** を使って残りとマッチできる
{ first: "Hotaru", last: "Shiragiku" } \
   in { first: first, **rest }
puts "first: #{first}, rest: #{rest}" 
# => first: Hotaru, rest: {:last=>"Shiragiku"}

Hash Pattern

# deconstruct_keysを定義すれば何でもマッチできる
# 引数には「マッチしようとしたキー名」が配列で渡る
class Tester
  def self.deconstruct_keys(_)
    { first: "Hotaru", last: "Shiragiku" }
  end
end

case Tester
in { first: "Hotaru", last: name }
  puts "name: #{name}" # => name: Shiragiku
end

パターン概要
Constant Pattern + α

Constant Pattern

# 定数を指定してマッチできる
case 1
in String
  fail # マッチしない
in Integer
  puts "1 is Integer"
end

Array Pattern with Const.

# 定数と配列を合わせて指定できる
Point = Struct.new(:x, :y)
case Point.new(1, 2)
in Point[x, y]
  puts "x: #{x}, y: #{y}" # => x: 1, y: 2
end

Hash Pattern with Const.

# 定数とハッシュを合わせて指定できる
case Point.new(3, 4)
in Point(x: x, y: y)
  puts "x: #{x}, y: #{y}" # => x: 3, y: 4
end

Constant Pattern

# ===を定義すればマッチ条件を変えられる
class Tester
  def self.===(other)
    other.nil?
  end
end

# case ではなく in (パターン側)が使われる
case nil
in Tester
  puts "nil matched"
end

パターンマッチ
実用例

実用例: JSON Response

response = {
  status: "ok",
  body: {
    id: 1,
    url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347",
    user: {
      login: "octocat",
      id: 2,
      type: "User",
      site_admin: false
    },
    assignee: {
      login: "kokuyou",
      id: 3,
      type: "User",
      site_admin: true
    }
  }
}

実用例: JSON Response

response = {
  status: "ok",
  body: {
    id: 1,
    url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347",
    user: {
      login: "octocat",
      id: 2,
      type: "User",
      site_admin: false
    },
    assignee: {
      login: "kokuyou",
      id: 3,
      type: "User",
      site_admin: true
    }
  }
}

実用例: JSON Response

# 全体のidと、userとassigneeのlogin nameを取り出したい
# digを使う場合
p [response.dig(:body, :id), 
   response.dig(:body, :user, :login), 
   response.dig(:body, :assignee, :login)]

# パターンマッチを使う場合
response in { 
  body: { 
    id: id, 
    user: { login: name1 }, 
    assignee: { login: name2 } 
  }
}
p [id, name1, name2]

実用例: AST

tree = RubyVM::AbstractSyntaxTree.parse('1 + 2')

# tree は以下のようなオブジェクトになる
RubyVM::AbstractSyntaxTree::NODE(type: :SCOPE, children: [
  ...,
  RubyVM::AbstractSyntaxTree::NODE(type: OPCALL, children: [
    RubyVM::AbstractSyntaxTree::NODE(type: LIT, children: [1]),
    :+,
    RubyVM::AbstractSyntaxTree::NODE(type: LIST, children: [
      RubyVM::AbstractSyntaxTree::NODE(type: LIT, children: [2]),
      nil
    ])
  ])
])

実用例: AST

def print_tree(node, indent = 0)
  print '| ' * indent

  case [node&.type, node&.children]
  in [:SCOPE, [_, _, n1]]
    puts 'scope'; print_tree(n1, indent + 1)
  in [:OPCALL, [n1, op, n2]]
    puts op.to_s; print_tree(n1, indent + 1); print_tree(n2, indent + 1)
  in [:LIST, [h, t]]
    puts 'cons'; print_tree(h, indent + 1); print_tree(t, indent + 1)
  in [:LIT, [lit]]
    puts lit.to_s
  in [nil, _]
    puts 'nil'
  end
end

実用例: AST

print_tree(RubyVM::AbstractSyntaxTree.parse('1 + 2'))

# scope
# | +
# | | 1
# | | cons
# | | | 2
# | | | nil

実用例: AST

print_tree(RubyVM::AbstractSyntaxTree.parse('1 + 2 * 3 - 4'))

# scope
# | -
# | | +
# | | | 1
# | | | cons
# | | | | *
# | | | | | 2
# | | | | | cons
# | | | | | | 3
# | | | | | | nil
# | | | | nil
# | | cons
# | | | 4
# | | | nil

アジェンダ

  • Rubyパターンマッチの概要

  • 闇の力で強化する

  • 闇の力でできることを考える

  • まとめ

Active Pattern in F#

// マッチ対象と独立してパターンを定義
let (|Even|Odd|) input = 
  if input % 2 = 0 then Even else Odd

// パターンマッチに使える
let TestNumber input =
   match input with
   | Even -> printfn "%d is even" input
   | Odd -> printfn "%d is odd" input

TestNumber 7  // 7 is odd
TestNumber 11 // 11 is odd
TestNumber 32 // 32 is even

やりたい!!!!

作った!!!!!!!

Parity Check with Active Pattern

module Parity
  extend ActivePattern::Context[Integer]
  Even = pattern { self % 2 == 0 }
  Odd = pattern { self % 2 != 0 }
end

def test_number(input)
  case input
  in Parity::Even; puts "#{input} is even"
  in Parity::Odd;  puts "#{input} is odd"
  end
end

test_number 7  # =>  7 is odd
test_number 11 # => 11 is odd
test_number 32 # => 32 is even

これだけなら

===を定義すればできる

Active Pattern in F# (2)

type Point = { X: float; Y: float; }

let (|Polar|) (p : Point) =
    ( sqrt <| p.X ** 2. + p.Y ** 2.
    , Math.Atan2(p.Y, p.X)
    )

let printPolar (p : Point) =
   match p with
   | Polar(r, theta) -> printf "(%f, %f)" r theta

let point = { X = 3.0; Y = 4.0; }
printPolar(point) // (5.000000, 0.9272952)

Rubyでもこうしたい(願望)

Point = Struct.new(:x, :y)
Polar = # なんらかの定義

point = Point.new(3, 4)
point in Polar[r, theta]
puts "Polar: (#{r}, #{theta})"

# しかしここで呼ばれるのは
#   1. Polar.===(point)
#   2. point.deconstruct (Point#deconstruct)
# の2つ
# Polarをどう定義してもPoint#deconstructは変わらない!

こうすればできる(闇)

# 1. === で作った配列をグローバル変数に入れる
Polar = Module.new do
  def self.===(point)
    $TMP = [Math.sqrt(point.x ** 2 + point.y ** 2), 
            Math.atan2(point.y, point.x)]
end

# 2. deconstructをprependで書き換えて
#    グローバル変数から返す
Point.prepend(Module.new do
  def deconstruct
    $TMP || super
  end
end)

Coordinates with Active Pattern

module Coordinates
  extend ActivePattern::Context[Point]
  Cartesian = pattern { [x, y] }
  Polar = pattern { [Math.sqrt(x ** 2 + y ** 2), Math.atan2(y, x)] }
end

point = Point.new(3, 4)

point in Coordinates::Cartesian[x, y]
puts "Catesian: (#{x}, #{y})" #=> (3, 4)

point in Coordinates::Polar[r, theta]
puts "Polar: (#{r}, #{theta})" #=> (5.0, 0.9272952180016122)

Active Pattern gem

  • F#のActive Patternっぽいものを書ける

    • Const = pattern { ... } を連ねる​

  • patternで返す値によって挙動が変わる

    • ​true/falseを返すと定数マッチングのみ
    • Arrayを返すとArray Patternになる

    • Hashを返すとHash Patternになる

アジェンダ

  • Rubyパターンマッチの概要

  • 闇の力で強化する

  • 闇の力でできることを考える

  • まとめ

JSONレスポンスの分解

JSONレスポンスの分解

# statusでokとngを返すAPI
ok_response = {
  status: "ok",
  body: {
    id: 1,
    # ...
  }
}
ng_response = {
  status: "ng",
  message: "Oops, something went wrong!"
}

JSONレスポンスの分解

module Response
  extend ActivePattern::Context[Hash]
  OK = pattern { self[:status] == 'ok' && [self[:body]] }
  NG = pattern { self[:status] == 'ng' && [self[:message]] }
end

def print_response(response)
  case response
  in Response::OK[body]
    puts 'OK! body: ' + body.to_s
  in Response::NG[message]
    puts 'NG! message: ' + message
  end
end

print_response(ok_response)
print_response(ng_response)

JSONレスポンスの分解

# bodyからownerとassigneeのlogin nameだけ取り出したい
module PullRequest
  extend ActivePattern::Context[Hash]
  Users = pattern { {
    owner: dig(:body, :user, :login),
    assignee: dig(:body, :assignee, :login) }
  }
end

ok_response in PullRequest::Users(owner: owner, assignee: assignee)
puts "owner: #{owner}, assignee: #{assignee}"
#=> owner: octocat, assignee: kokuyou

URLを振り分け

URLを振り分け

module Route
  extend ActivePattern::Context[String]

  Root = pattern { self == '/' }
  Users = pattern { self == '/users/' }
  User = pattern { 
    %r|^/users/([0-9]+)/$|.match(self)&.captures
  }
  UserPosts = pattern { 
    %r|^/users/([0-9]+)/posts/$|.match(self)&.captures
  }
  UserPost = pattern { 
    %r|^/users/([0-9]+)/posts/([0-9]+)/$|.match(self)&.captures
  }
end

URLを振り分け

def parse_route(path)
  case path
  in Route::Root; puts 'root path'
  in Route::Users; puts 'users path'
  in Route::User[uid]; puts "user path(user_id: #{uid})"
  in Route::UserPosts[uid]; puts "user posts path(user_id: #{uid})"
  in Route::UserPost[uid, pid]
    puts "user post path(user_id: #{uid}, post_id: #{pid})"
  end
end

parse_route('/') #=> root path
parse_route('/users/') #=> users path
parse_route('/users/765/') #=> user path(user_id: 765)
parse_route('/users/765/posts/') #=> user posts path(user_id: 765)
parse_route('/users/765/posts/315/') 
#=> user post path(user_id: 765, post_id: 315)

抽象構文木の実行

抽象構文木の実行

module Node
  extend ActivePattern::Context[RubyVM::AbstractSyntaxTree::Node]

  Scope = pattern { type == :SCOPE && children }
  OpCall = pattern { type == :OPCALL && children }
  List = pattern { type == :LIST && children }
  Literal = pattern { type == :LIT && children }

  PlusOp = pattern { self in OpCall(x, :+, List(y, nil)); [x, y] }
  MinusOp = pattern { self in OpCall(x, :-, List(y, nil)); [x, y] }
  MulOp = pattern { self in OpCall(x, :*, List(y, nil)); [x, y] }
  DivOp = pattern { self in OpCall(x, :/, List(y, nil)); [x, y] }
end

抽象構文木の実行

def eval_tree(tree)
  case tree
  in Node::Scope[_, _, n1]
    eval_tree(n1)
  in Node::Literal[n]
    n
  in Node::PlusOp[l, r]
    eval_tree(l) + eval_tree(r)
  in Node::MinusOp[l, r]
    eval_tree(l) - eval_tree(r)
  in Node::MulOp[l, r]
    eval_tree(l) * eval_tree(r)
  in Node::DivOp[l, r]
    eval_tree(l) / eval_tree(r)
  end
end

抽象構文木の実行

puts eval_tree(RubyVM::AbstractSyntaxTree.parse('1 + 2'))
# => 3

puts eval_tree(
  RubyVM::AbstractSyntaxTree.parse('1 + 2 * 3 - 4 / 2'))
# => 5

Presenter(闇)

Presenter(闇)

Video = Struct.new(:type, :status)

module Presenter
  extend ActivePattern::Context[Video]

  Type = pattern do
    case self.type
    in :official; ['公式']
    in :user; ['ユーザ']
    end
  end

  # ...

Presenter(闇)

  # ...

  Status = pattern do
    case self.status
    in :prepare; ['準備中']
    in :onair; ['放送中']
    in :closed; ['放送済み']
    end
  end

  All = pattern { 
    self in Type[type]; self in Status[status]; 
    { type: type, status: status }
  }
end

Presenter(闇)

v1 = Video.new(:official, :prepare)
v1 in Presenter::Type[type]
v1 in Presenter::Status[status]
puts "#{type}番組 #{status}" #=> 公式番組 準備中

v2 = Video.new(:user, :onair)
v2 in Presenter::All(type: type, status: status)
puts "#{type}番組 #{status}" #=> ユーザ番組 放送中

アジェンダ

  • Rubyパターンマッチの概要

  • 闇の力で強化する

  • 闇の力でできることを考える

  • まとめ

まとめ

  • 楽しい!!!!!!!!!!

  • 割と可能性を感じる

    • 複雑なパターンや定数を閉じ込められる

    • データの解釈を外から与えられる

今後の展望

  • include Context[XXX]の部分が
    Nominalになっていて微妙

    • 今の戦略だとprependする対象が要る

  • case v in C[x, y] みたいに書いたときに、
    C.deconstruct(v) が呼ばれるようにしたい

    • TracePointでなんとかなる?

    • Cコード書き換えないとだめかも