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

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

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

Burikaigi2020の発表資料です

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 コード書き換えないとだめかも