Rubyパターンマッチに
闇の力が備わり最強に見える
283プロ/346プロ アイドルプロデューサー
黒曜
(@kokuyouwind)
自己紹介
-
黒曜(@kokuyouwind)
-
アイマスプロデューサー
(283/346兼務) -
担当アイドル
櫻木真乃(283プロ)
白菊ほたる(346プロ) -
副業でWebエンジニア@Misoca
担当アイドル紹介-櫻木真乃
ほんわかした癒やし系アイドル。
illumination STARSという
3人ユニットのセンター。
心優しい性格だが、
センターという役割に悩む
責任感の強い一面も。
イルミネはエモの塊
担当アイドル紹介-白菊ほたる
所属事務所が3連続で倒産した
不運な少女。思考が後ろ向き。
それでもアイドルを目指すのは、
不幸を撒き散らした自分でも
誰かを幸せにしたいから。
かこほたは運命
Rubyの
パターンマッチの
話をします
Rubyのパターンマッチ
-
条件分岐とデータ分解を兼ねる機能
-
Ruby 2.7.0 で実験的に導入された
-
関数型プログラミング言語からの輸入
-
-
Rubyらしさを壊さないように仕様策定
-
n月刊ラムダノートに詳しい話がある
-
dack typingなどに配慮して設計
-
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コード書き換えないとだめかも
-