Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rubyパターンマッチに闇の力が備わり最強に見える

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for kokuyouwind kokuyouwind
February 01, 2020

 Rubyパターンマッチに闇の力が備わり最強に見える

Burikaigi2020の発表資料です

Avatar for kokuyouwind

kokuyouwind

February 01, 2020
Tweet

More Decks by kokuyouwind

Other Decks in Programming

Transcript

  1. 全サンプルコードは以下リポジトリにあります https://github.com/kokuyouwind/pattern_match_demo $ git clone https://github.com/kokuyouwind/pattern_mat $ 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 1 2 3 4 5 6 7 8
  2. Array Pattern # 配列の要素ごとにパターンマッチできる case [1, 2, 3] in [2,

    _, _] fail # マッチしない in [1, x, y] puts "x: #{x}, y: #{y}" # => x: 2, y: 3 end 1 2 3 4 5 6 7 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/01_array.rb
  3. Array Pattern # * を使って残りとマッチできる case [1, 2, 3] in

    [x, *y] puts "x: #{x}, y: #{y}" # => x: 1, y: [2, end # 1 ⾏でin を使ってパターンマッチできる [1, 2, 3] in [x, _, _] puts "x: #{x}" # => x: 1 1 2 3 4 5 6 7 8 9 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/01_array.rb
  4. Array Pattern # destruct を定義してればなんでもマッチできる class Tester def self.deconstruct [1,

    2, 3] end end case Tester in [_, x, _] puts "x: #{x}" # => x: 2 end 1 2 3 4 5 6 7 8 9 10 11 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/01_array.rb
  5. Hash Pattern # ハッシュのキーごとにパターンマッチできる case { first: "Hotaru", last: "Shiragiku"

    } in { first: "Kako", last: _ } fail # マッチしない in { first: "Hotaru", last: name } puts "name: #{name}" # => name: Shiragiku end 1 2 3 4 5 6 7 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/02_hash.rb
  6. Hash Pattern # 1 ⾏でin を使ってマッチできる # ** を使って残りとマッチできる {

    first: "Hotaru", last: "Shiragiku" } \ in { first: first, **rest } puts "first: #{first}, rest: #{rest}" # => first: Hotaru, rest: {:last=>"Shiragik 1 2 3 4 5 6 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/02_hash.rb
  7. 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 1 2 3 4 5 6 7 8 9 10 11 12 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/02_hash.rb
  8. Constant Pattern # 定数を指定してマッチできる case 1 in String fail #

    マッチしない in Integer puts "1 is Integer" end 1 2 3 4 5 6 7 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/03_constant.rb
  9. 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 1 2 3 4 5 6 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/03_constant.rb
  10. 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 1 2 3 4 5 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/03_constant.rb
  11. Constant Pattern # === を定義すればマッチ条件を変えられる class Tester def self.===(other) other.nil?

    end end # case ではなく in ( パターン側) が使われる case nil in Tester puts "nil matched" end 1 2 3 4 5 6 7 8 9 10 11 12 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/03_constant.rb
  12. 実⽤例: 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 } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/04_example_response.rb
  13. 実⽤例: 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 } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/04_example_response.rb
  14. 実⽤例: 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] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/04_example_response.rb
  15. 実⽤例: 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: :+, RubyVM::AbstractSyntaxTree::NODE(type: LIST, children RubyVM::AbstractSyntaxTree::NODE(type: LIT, childre nil ]) ]) ]) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/05_ast.rb
  16. 実⽤例: 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, in [:LIST, [h, t]] puts 'cons'; print_tree(h, indent + 1); print_tree(t, ind in [:LIT, [lit]] puts lit.to_s in [nil, _] puts 'nil' end end 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/05_ast.rb
  17. 実⽤例: AST print_tree(RubyVM::AbstractSyntaxTree.parse(' # scope # | + # |

    | 1 # | | cons # | | | 2 # | | | nil 1 2 3 4 5 6 7 8 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/05_ast.rb
  18. 実⽤例: AST print_tree(RubyVM::AbstractSyntaxTree.parse('1 + 2 * 3 - # scope

    # | - # | | + # | | | 1 # | | | cons # | | | | * # | | | | | 2 # | | | | | cons # | | | | | | 3 # | | | | | | nil # | | | | nil # | | cons # | | | 4 # | | | nil 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/05_ast.rb
  19. 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 1 2 3 4 5 6 7 8 9 10 11 12 13 https://docs.microsoft.com/ja-jp/dotnet/fsharp/language-reference/active-patterns
  20. 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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/10_active_pattern_examples.rb
  21. 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 let point = { X = 3.0; Y = 4.0; } printPolar(point) // (5.000000, 0.9272952) 1 2 3 4 5 6 7 8 9 10 11 12 13
  22. 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 2 3 4 5 6 7 8 9 10 11 12
  23. こうすればできる( 闇) # 1. === で作った配列をグローバル変数に⼊れる Polar = Module.new do

    def self.===(point) $TMP = [Math.sqrt(point.x ** 2 + point.y ** Math.atan2(point.y, point.x)] end # 2. deconstruct をprepend で書き換えて # グローバル変数から返す Point.prepend(Module.new do def deconstruct $TMP || super end end) 1 2 3 4 5 6 7 8 9 10 11 12 13 14
  24. Coordinates with Active Pattern module Coordinates extend ActivePattern::Context[Point] Cartesian =

    pattern { [x, y] } Polar = pattern { [Math.sqrt(x ** 2 + y ** 2), Math.atan 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.927295218001612 1 2 3 4 5 6 7 8 9 10 11 12 13 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/10_active_pattern_examples.rb
  25. Active Pattern gem F# のActive Pattern っぽいものを書ける Const = pattern

    { ... } を連ねる pattern で返す値によって挙動が変わる true/false を返すと定数マッチングのみ Array を返すとArray Pattern になる Hash を返すとHash Pattern になる https://rubygems.org/gems/active_pattern
  26. JSON レスポンスの分解 # status でok とng を返すAPI ok_response = {

    status: "ok", body: { id: 1, # ... } } ng_response = { status: "ng", message: "Oops, something went wrong!" } 1 2 3 4 5 6 7 8 9 10 11 12 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/11_active_pattern_response.rb
  27. 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) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/11_active_pattern_response.rb
  28. 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: puts "owner: #{owner}, assignee: #{assignee}" #=> owner: octocat, assignee: kokuyou 1 2 3 4 5 6 7 8 9 10 11 12 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/11_active_pattern_response.rb
  29. 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)&.ca } end 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/14_active_pattern_routes.rb
  30. 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: #{ 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: parse_route('/users/765/posts/315/') #=> user post path(user_id: 765, post_id: 315) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/14_active_pattern_routes.rb
  31. 抽象構⽂⽊の実⾏ module Node extend ActivePattern::Context[RubyVM::AbstractSyntaxTre 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)); MinusOp = pattern { self in OpCall(x, :-, List(y, nil)) MulOp = pattern { self in OpCall(x, :*, List(y, nil)); DivOp = pattern { self in OpCall(x, :/, List(y, nil)); end 1 2 3 4 5 6 7 8 9 10 11 12 13 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/13_active_pattern_eval.rb
  32. 抽象構⽂⽊の実⾏ 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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/13_active_pattern_eval.rb
  33. 抽象構⽂⽊の実⾏ puts eval_tree(RubyVM::AbstractSyntaxTree.parse('1 + 2 # => 3 puts eval_tree(

    RubyVM::AbstractSyntaxTree.parse('1 + 2 * 3 - 4 / 2' # => 5 1 2 3 4 5 6 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/13_active_pattern_eval.rb
  34. Presenter( 闇) Video = Struct.new(:type, :status) module Presenter extend ActivePattern::Context[Video]

    Type = pattern do case self.type in :official; [' 公式'] in :user; [' ユーザ'] end end # ... 1 2 3 4 5 6 7 8 9 10 11 12 13 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/15_active_pattern_presenter.rb
  35. 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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/15_active_pattern_presenter.rb
  36. 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}" #=> ユーザ番組 放送中 1 2 3 4 5 6 7 8 9 https://github.com/kokuyouwind/pattern_match_demo/blob/master/src/15_active_pattern_presenter.rb
  37. 今後の展望 include Context[XXX] の部分が Nominal になっていて微妙 今の戦略だとprepend する対象が要る case v

    in C[x, y] みたいに書いたときに、 C.deconstruct(v) が呼ばれるようにしたい TracePoint でなんとかなる? C コード書き換えないとだめかも