SEAN_BLOG

プログラミング etc.

日本の携帯電話解約後、カナダで今まで利用していた LINE アカウントを利用する方法

対象読者

日本の携帯電話を解約・カナダの携帯電話を契約したうえで LINE のアカウントの移行を試みている人。

TL;DR

  • Apple ID もしくは、Google アカウントのいずれか 1 つ以上が登録されている状況であれば、今まで通り利用できる。
  • 上記いずれかのアカウントの登録が済んでいれば、特に移行の作業は不要。

Apple ID/Google アカウントの連携 についてはこちら。

※ 上記は 2024/02 時点のもので、将来の対応方法を保証するものではない。

詳細

以下、LINE 公式サポートとのやりとり。

#1

Q.
日本の番号からカナダの番号に変更を試みています。
しかし、該当の(5)のステップにて `電話番号でログイン` が表示されず、
Apple ID でのログインしか確認できません。

現状は、日本の番号を維持しているので当面は移行せずとも問題がないと推測しますが、
将来的には日本の番号の解約を考えています。
この場合、どのようにカナダの番号に移行し、今まで日本で利用していたアカウントと紐づけることが可能でしょうか?
ご多用のところ恐縮ですが、ご確認お願いします。

https://help.line.me/line/android/categoryId/20007877/3/pc?lang=ja

A.
お問い合わせありがとうございます。
LINEカスタマーサポートです。

恐れ入りますが、現在、日本・韓国・台湾・香港・タイ以外の国・地域の電話番号への変更はできません。

お客さまのご期待にそう回答とならず心苦しいかぎりでございますが、何卒ご理解ご了承くださいますようお願い申し上げます。

なお、登録電話番号を解約された場合でも、Apple IDまたはGoogleアカウントが連携されていれば、LINEは引き続きご利用いただけますのでご安心ください。
※LINEは、電話番号/Apple ID/Googleアカウントのいずれか1つ以上が登録されている状況であれば、有効なままご利用を続けることができます
※Apple ID/Googleアカウントの連携状況は、LINEアプリ内よりご確認ください

#2

Q.
前回上記の通り回答をいただきました。

カナダの番号に紐づけることはできないとのことで理解しました。
また、Apple/Google との連携がされている状態であれば、サインインできることも承知・確認しました。

その上で一点質問なのですが、日本の電話番号のプロバイダー契約を解除したとします。
ただ、LINE のアプリ上では電話番号を空欄とすることはできないように思います。
この場合、解約した日本の電話番号が LINE 上の自分の電話番号となったままとなってしまうかと思います。
こちらは LINE を使用する上で問題はないのでしょうか?
あるいは、何らかの方法で電話番号を空欄に更新する方法があるのでしょうか?

A.
お問い合わせありがとうございます。
LINEカスタマーサポートです。

恐れ入りますが、登録した電話番号の解除方法についてはセキュリティ上の理由からご案内しておりません。
何卒ご了承ください。

お客さまが今後電話番号を解約された場合でも、LINEアカウントにApple ID/Googleアカウントのいずれかが連携されている状態であれば、LINEアカウントの利用に影響することはありませんのでご安心ください。
※Apple ID/Googleアカウントの連携状況は、LINEアプリ内よりご確認ください

平成Ruby会議01 CfP

面白そうな Advent Calendar を発見したので、せっかくなので残しておこうと思います。

adventar.org

ありがたいことに、こちらの CfP は採択されました。

厳正なる選考の結果、「Play with Ruby」の内容でSho ITO様にご登壇いただきたいです。

heiseirb.github.io

発表タイトル(※本情報は当選時にLPに掲載されます)

Play with Ruby

発表の概要(※本情報は審査にのみ利用します)

エンジニアとしての経験が1年半程の私が、 Ruby本体 への右代入の実装を通して学んだこと・気づきを発表します。

この発表を聴いた人が何を持ち帰れるか(※本情報は審査にのみ利用します)

今年の春に開催されたRubyKaigi 2019 のDay 3 に "Play with local vars" というTalk がありました。 そこではRuby 本体のlocal vars を修正するlive coding を通じて、 "Play" しながら新たなことを学習することの重要性が説かれていました。

そのTalk を聴く前まで私自身Ruby 本体を修正することは、 非常にハードルが高く難しいものだと考えておりました。 もっと言えば、C 言語の経験も全く無かった私には不可能にすら思えました。

しかし"Play with local vars" を聴講し、

"Play" しながら学習が可能であること、 Ruby 本体の修正は想像よりかは難しくないこと、

を学びました。 これにより大きく勇気付けられた気がしました。

この度のLT では私自身が"Play with local vars" を拝聴した時に得た、

実際に手を動かし楽しみ(Play)ながら学ぶことの可能性、 Ruby 本体の修正は想像よりかは難しくないこと、

この2点を参加者の皆さんに共有したいと思います。

私がRubyKaigi 2019 で勇気を得たように、 1人でも多くのRubist に勇気を与えることができれば光栄に思います。

cf. https://rubykaigi.org/2019/presentations/ujm.html#apr20

当日の資料

speakerdeck.com

binding.pry がなぜ動くのか?今更ながら調べてみた

TL;DR

  • binding.pry は、Kernel#bindingObject#pry に分けられる。
  • Kernel#binding はコンテキストの環境情報を束縛したBinding オブジェクトを返す。
  • Object#pry はREPL プログラム。

そもそも、binding.pry とは?

Ruby を使っている方なら、一度はbinding.pry を利用したことがあるでしょう。

そう、debug するときにお世話になるあれです。

しかしながら、なぜあのような挙動になるのか知っている人は少ないと思います。 コード中にbinding.pry を記述し当該のコードを実行するだけで*1、 なぜかプロンプトが出現します。

     9: def name
 => 10:   binding.pry
    11:   puts @name
    12: end

[1] pry(#<Sample>)>

なぜでしょう。

wakaranai...

ということなので、今更ながら調べてみました。

そもそも、pry とは何なのか。

Pry is a runtime developer console and IRB alternative with powerful introspection capabilities. Pry aims to be more than an IRB replacement. It is an attempt to bring REPL driven programming to the Ruby language.

cf. https://github.com/pry/pry#introduction

要はruntime developer console だと。 そして、REPL driven programming をRuby に導入することを目指すものらしい。

でつ。oO(はて。REPL とは何だろう。。。難しそう。。。)*2

REPL (Read-Eval-Print Loop) とは、入力・評価・出力のループのこと。 主にインタプリタ言語において、ユーザーとインタプリタが対話的にコードを実行する。

cf. https://ja.wikipedia.org/wiki/REPL

とのことです。

今回はこちらのREPL の処理を中心に、pry を紐解いていこうと思います。

と、その前に今回はbinding.pry がなぜ動くのか?という事なので、 Kernel#Binding メソッドについても調査していきます。

Kernel#binding

まずは、binding.pry の前半分の、binding

これは、gem pry に依存するものではなく、Ruby 組み込みライブラリKernel モジュールのメソッドです。 変数やメソッドなどの環境の情報を束縛した、Binding クラスのオブジェクトを返します。

def sample
  name = 'John'
  binding
end

eval('puts name', sample)
#=> John

cf. Binding

Kernel#eval の第二引数に渡すと、そのコンテキストで第一引数の文字列をRuby プログラムとして評価してくれます。

つまり、binding.pry を実行したときに必要な情報が取得できるのは、この人のおかげという事ですね。

でつ。oO(すてき。。。)

Object#pry

さて、binding.pry の残りの部分pry の部分を深掘りしていきます。

肝心の pry メソッドですが、gem pry のなかで定義されています。

class Object
  ...
  def pry(object = nil, hash = {})
    if object.nil? || Hash === object # rubocop:disable Style/CaseEquality
      Pry.start(self, object || {})
    else
      Pry.start(object, hash)
    end
  end
  ...

Open class を用いてObject クラスに、pry メソッドを定義しています。 Object クラスは全てのクラスのスーパークラス*3なので、Object#pry を定義することで、 全てのオブジェクトでpry メソッドが利用できるようになります。

実際にModule#method_defined? を使って確かめてみると。

p Object.method_defined?(:pry)
#=> false

require 'pry'

p Object.method_defined?(:pry)
#=> true

require 'pry' の前では Object クラスに pry が定義されていなにのに対し、 require 'pry' の後では Object#pry が定義されていることが確認できます。

さて、ここからはbinding.pry がコード中で実際に呼ばれたとき、何が起こるのか見ていきます。

コード中でbinding.pry を呼ぶと、 Object#pry にデフォルト引数object = nil が渡されます。 そして、Object#pry の内部で、Pry.start(self, object || {}) が呼ばれます。

  def self.start(target = nil, options = {})
    ...
    options[:target] = Pry.binding_for(target || toplevel_binding)
    ...
    driver = options[:driver] || Pry::REPL

    # Enter the matrix
    driver.start(options)
    ...
   end

Pry.start は全体で30行程あります。 長くなるのでメインの処理の関係する箇所のみ抜粋しました。

Pry.binding_for が呼ばれています。target が存在すれば target を、 無ければトップレベルスコープの binding が引数として渡されます。

binding.pry のケースでは、self(=binding)Pry.binding_for の第一引数として渡されます。

  def self.binding_for(target)
    return target if Binding === target # rubocop:disable Style/CaseEquality
    return TOPLEVEL_BINDING if Pry.main == target

    target.__binding__
  end

こちらのメソッドは渡されたtargetbinding を返します。 binding.pry のケースでは、self(=binding) が戻り値となります。

    options[:target] = Pry.binding_for(target || toplevel_binding)

その結果、options[:target]self(=binding)アサインされます。

Pry.start では続いてdriver に Pry::REPLアサインされ、Pry::REPL.start が呼ばれます。 options が引数として渡されるので、先ほどoptions[:target] としてアサインした束縛情報ももれなく渡されています。

    def self.start(options)
      new(Pry.new(options)).start
    end

    ...

    def start
      prologue
      Pry::InputLock.for(:all).with_ownership { repl }
    ensure
      epilogue
    end

Pry::REPLインスタンスが作成され、Pry::REPL#start が呼ばれます。 Pryインスタンスが生成され引数として渡されています。 また、この際Pryインスタンス変数@binding_stackoptions[:target] の中身がアサインされています。

やっと、主要の処理にたどり着きました。Pry::REPL#repl です。

感の良い方はもうお気付きですね!(感が鈍くても気がつきますね。。。)

そう、Pry::REPL#repl は、先ほど説明したRead-Eval-Print Loop です!

    def repl
      loop do
        case val = read
        when :control_c
          output.puts ""
          pry.reset_eval_string
        when :no_more_input
          output.puts "" if output.tty?
          break
        else
          output.puts "" if val.nil? && output.tty?
          return pry.exit_value unless pry.eval(val)
        end
      end
    end

コードを見てみると、何だか色々やってそうですが、 重要なのはPry::REPL#readPry#evalKernel#loop です。

でつ。oO(あれ、print が無い。。。)

read

Pry::REPL#read > Pry::REPL#read_line > Pry::REPL#input_readline という順で見ていきます。

    def input_readline(*args)
      Pry::InputLock.for(:all).interruptible_region do
        input.readline(*args)
      end
    end

input@pry 経由でPry::Config からdelegate されています。

  class Config
    ...
    def initialize
      merge!(
        input: MemoizedValue.new { lazy_readline },
        ...
    def lazy_readline
      require 'readline'
      ::Readline
      ...
    end

長くなりそうなので中間の処理を省きます。

上記の通りinput には::Readlineアサインされるので、 Readline.readline が呼ばれ、ユーザからの入力を取得します。cf. Readline

これがコード実行中にプロンプトが表示されて、入力待機状態になる現象の実態ですね。 無事ユーザからの入力部分は理解できました。

続いて、評価部分のeval を見ていきます。

eval

前出のPry::REPL#repl を見るとpry.eval(val) となっており、先ほど見たread の戻り値をPry#eval に渡しています。

ちなみに、ここで呼ばれているeval は、Pry クラスでoverride されており、Kernel#eval とは異なります。

Pry#eval > Pry#handle_line > Pry#evaluate_ruby と読み進めていきます。

  def evaluate_ruby(code)
    inject_sticky_locals!
    exec_hook :before_eval, code, self

    result = current_binding.eval(code, Pry.eval_path, Pry.current_line)
    set_last_result(result, code)
  ensure
    update_input_history(code)
    exec_hook :after_eval, result, self
  end

こちらも途中の処理をだいぶ省きました。

Pry#evaluate_ruby の中で、current_binding.eval が呼ばれています。 Pry#current_binding は、binding_stack に格納されている現状のBinding オブジェクトを返します。 Pry#current_binding の戻り値のコンテキストで、文字列codeRuby のコードとして評価します。 ちなみに、多少処理がなされていますがcode の中身は、先ほどのPry::REPL#read の返り値です。

結局のところ、対象のbinding に対して、binding#eval を呼び出しているだけですね。

さて、ここまででreadeval までの処理が完了しましたね。

次は表示print の部分です。

print

実は先ほど見たPry#eval の中にprint の処理も含まれています。

Pry#eval > Pry#handle_line > Pry#show_result と掘り進めます。

  def handle_line(line, options)
    ...
      Pry.critical_section do
        show_result(result)
      end
    ...
  end
  def show_result(result)
    if last_result_is_exception?
      exception_handler.call(output, result, self)
    elsif should_print?
      print.call(output, result, self)
    end
    ...
  end

print の中身は、Pry::ColorPrinter.method(:default) の戻り値Method オブジェクトです。 そのMethod オブジェクトにcall メソッドを呼んでメソッドを実行しています。

require 'pp'
...
class Pry
  class ColorPrinter < ::PP
    ...
    def self.default(_output, value, pry_instance)
      pry_instance.pager.open do |pager|
        pager.print pry_instance.config.output_prefix
        pp(value, pager, pry_instance.output.width - 1)
      end
    end
    ...
  end
end

Pry::ColorPrinter.default の中で、pry_instance.pager.open によってfile を開きます。 ここで開くfile に関しては、設定によって変わります。

ブロックの中で、Pry::ColorPrinter.pp を呼び出します。

    ...
    def self.pp(obj, output = $DEFAULT_OUTPUT, max_width = 79)
      queue = ColorPrinter.new(output, max_width, "\n")
      queue.guard_inspect_key { queue.pp(obj) }
      queue.flush
      output << "\n"
    end

そして、ColorPrinter#pp を呼び出します。

    ...
    def pp(object)
      return super unless object.is_a?(String)

      text(object.inspect)
    rescue StandardError => exception
      raise if exception.is_a?(Pry::Pager::StopPaging)

      text(highlight_object_literal(inspect_object(object)))
    end
    ...

引数のobjectString 以外の時は、 キーワードsuper を呼び出し親クラスPPpp メソッドを呼び出しています。 それ以外のケースでは、ColorPrinter#text を呼び出します。

    ...
    def text(str, max_width = str.length)
      if str.include?("\e[")
        super("#{str}\e[0m", max_width)
      elsif str.start_with?('#<') || %w[= >].include?(str)
        super(highlight_object_literal(str), max_width)
      else
        super(SyntaxHighlighter.highlight(str), max_width)
      end
    end
    ...

表示する文字列str の内容に応じて処理を変更しています。 基本的にはこちらもキーワードsuper により、親クラスのPP#text を呼び出しています。

この一連の処理によって、結果が出力されます。

loop

あとはこれを外側のループで回すだけです。

でつ。oO(loop は特に書くことが無い。。。)

ここまでの一連の流れがbinding.pry ないしは、pry メソッドの正体です。 pry というgem 全体で見ると処理も多く、コード数もそれなりにあります(20,000 行強*4)。

ただ、REPL に関する処理は、意外とシンプルなようでした。

所感

今まで何となく使っていたbinding.pry ですが、実際に調べてみて、 ブラックボックス化されていたことを明らかにすることができました。

# 今まで
binding.pry == 便利な魔法の呪文

# 今
binding.pry == REPL プログラム

頭の中でbinding.pry の単語をアップデートすることができました*5

最後まで読んでいただきありがとうございます🙇‍♂️

参考

*1:gem のインストールが必要。

*2:でつ: 社のSlack でご活躍のスヌーピーさん。

*3:実際にはBasicObject クラス(Ruby 1.9 以降)という、Object クラスの親にあたるクラスが存在します。

*4:コード量計測には'AlDanial/cloc' を使用。

*5:pry は様々なコマンドが利用出来たり、ハイライトが効いたりと単純なREPL プログラム以上のものです。

Ruby 2.7 の新機能(Pipeline operator |> 編) - チケットとコミットログを読んでみて

さて、今回はRuby 2.7 の新機能、Pipeline operator |> についてのエントリーです。 RubyKaigi 2019 のRuby Committers vs the World で取り上げられていたあれです。

6月13日にtrunk にmerge されたようですが、それ以降Twitter を中心に議論が盛り上がっていますね。 他の機能に対しても言えることですが、今後も機能が大きく変わる可能性がありそうです。

Pipeline operator |>

TL;DR

  • dot の代わりに、|> を利用することが可能。
  • |> はdot に比べて優先順位が低く、Range の括弧を省略可能。(e.g. 1..10 |> each { p @1 })
  • |> の後に他のoperator を続けることは不可。(e.g. 10 |>+ 10)

使用例

$ ruby -v
ruby 2.7.0dev (2019-06-16T05:46:28Z trunk 2fb1564c02) [x86_64-linux]
# w/o pipeline operator
(1..).take(10).map { @1 * 2 }
#=>[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

# w/ pipeline operator
1..
  |> take 10
  |> map { @1 * 2 }
#=>[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

基本的にはmethod 呼び出しのdot と同じ動きをします。 Pipeline operator |> は、dot に比べ優先順位が低いです。 そのため、上述のコードのようにRange の括弧を省略することが可能です。

Numbered parameters @1 については、こちらをご覧ください。

Ruby 2.7 の新機能(Numbered parameters 編) - チケットとコミットログを読んでみて - SEAN_BLOG

チケット

bugs.ruby-lang.org

全体を通りしてPipeline operator の追加に否定的な意見が多かったように見受けられました。 主な理由としてあげられていたのが、以下の2つ。

  • 関数型のPipe operator をRuby に持ち込むのは如何なものか。
  • ElixirやF#、Elm のPipe operator とRuby 2.7 の"Pipeline operator"の挙動は異なる。
# w/o pipe operator
foo(bar(baz(new_function(other_function()))))

# w/ pipe operator
other_function() |> new_function() |> baz() |> bar() |> foo()

上記は、Elixir のPipe operator のコード例です。

記述法はRuby 2.7 のPipeline operator と変わりません。

ただ、Elixir のPipe operator では、前のfunction の結果を次のfunction の第一引数としてとります*1。 つまり、上記の例ではother_function() の戻り値が、new_function() の引数として渡されることになります。 (baz() 以下も同様。)

一方で、Ruby 2.7 のPipeline operator では、前のmethod の戻り値が次のmethod の引数としては渡されません。

コミットログ

github.com

今回もテストコードを中心に見ていきます。

dot の代わりに、|> を用いていますね。

assert_equal("121", eval('x = 12 |> pow(2) |> to_s 11'))

dot を用いるとこんな感じですかね。

eval('x = 12.pow(2).to_s 11')

ただ、dot の場合、変数x12.pow(2).to_s 11 の戻り値である121 が代入されます。 それに対して、Pipeline operator ではx = 12 になっているようですね🤔

優先順位がことなるからですね。

assert_syntax_error('a|>-b', /unexpected '-'/)

こちらは、6月14日に追加されていたのですが、|> の後に別のoperator を用いることができなくなっています。 この点はdot と異なるようですね。

所感

全体的にPipeline operator の導入には否定的な意見が多かったように思います。 確かに、実際のユースケースを考えて見ると、dot ではなくPipe operator を利用するケースが思い付かない😇 果たしてこれは、僕の経験が無いからなのか、そもそも使い道がないからなのか。。。

といったところで、今回は失礼します。

最後まで読んで頂き、ありがとうございます🙇

参考

*1:F#、Elm の|>では、function の結果を次のfunction の引数の最後にappend する仕様のようです。

Ruby 2.7 の新機能(Method reference operator .: 編) - チケットとコミットログを読んでみて

さて、今回はRuby 2.7 の新機能、Method reference operator .: についてまとめていきます。 Ruby 2.7 の新機能に関しては、前回のNumbered parameters に続き今回で3本目のエントリーです🎉

なんだか、連載のような感じになってきてしまいました。

最初は1つのエントリーに全てまとめるつもりだったのに。。。

Method reference operator .:

TL;DR

  • Object#method の代わりに、Method reference operator .: を利用可能
  • Dir["*/*.c"].map(&File.:basename) といった書き方が可能
  • Numbered parameters は便利

使用例

# w/ Object#method
m = 100.method(:to_s)
#=> #<Method: Integer#to_s>
m.call
#=> "100"

m = 100.method(:to_hash)
#=> NameError (undefined method `to_hash' for class `Integer')

引数で指定したメソッドの名前をオブジェクト化した、Method オブジェクトを返します。 もし、その名前のメソッドが存在しない場合は、NameError を返します。 メソッドをオブジェクト化したことにより、クロージャーとして利用することが可能になります。

これを、.: を用いて下記のように書くことができるようになりました。

# w/ .:
m = 100.:to_s
#=> #<Method: Integer#to_s>
m.call
#=> "100"

m = 100.:to_hash
#=> NameError (undefined method `to_hash' for class `Integer')

挙動は Object#method と同じです。

チケット

今回もチケットから導入の経緯、導入までの一連の流れを見ていきましょう。

bugs.ruby-lang.org

bugs.ruby-lang.org

今回の出発点は、下記のような処理をしたい時に、

  • ブロックパラメターを利用する方法
w/o Object#method
Dir["*/*.c"].map { |f| File.basename(f) }
  • Object#method を用いる方法
w/ Object#method
Dir["*/*.c"].map(&File.method(:basename))

どちらもあまりいけてないよね?というところのようです。

そこで、lambda で使われている-> を用いて、下記のような方法を用いるのはどうかと提案されています。

Dir["*/*.c"].map(&File->basename)

これを受けて、Matzさんは「アイディアは好きだけど、-> はちょっと。」と言った反応を示されておりました。

I like the idea of short hand notation for Object#method(), but I don't think -> is a good idea.

Matz.

チケットの中では.><..>&>->>=>>+>$>:>\.Object[.method]: や、 Elixir の|> を用いたらどうかという代替案が出されていました。

また、メッセージセンドの:: を地上げし、再利用するという提案もありました。 ちなみに、Java のMethod reference には :: が用いられているようです。

そんな中、Matzさん、

「候補の中で.: がベスト、ついで :::。...」とのこと。

..., .: looks best to me (followed by :::)....

Matz.

結果、.: の形に収まったようです。

その他、RubyPerl に近づいていくことを危惧する意見や、これ以上記号をRuby に導入しないで欲しいといった意見などの Method reference operator の導入しネガティブな意見も散見されました。

コミットログ

github.com

今回もテストを中心に、読み解いていこうと思います。

基本的には、Object#method と同様の挙動になるようテストが書かれています。

m = 1.:succ
assert_equal(1.method(:succ), m)
assert_equal(2, m.())

m = 1.:+
assert_equal(1.method(:+), m)
assert_equal(42, m.(41))

m = 1.:-@
assert_equal(1.method(:-@), m)
assert_equal(-1, m.())

o = Object.new
def o.foo; 42; end
m = o.method(:foo)
assert_equal(m, o.:foo)

def o.method(m); nil; end
assert_equal(m, o.:foo)
assert_nil(o.method(:foo))

通常のメソッド同様、改行にも対応してます。

assert_valid_syntax("a\n.:foo")

所感

Ruby 2.7 のMethod reference operator はなんとなく、Pattern matching とNumbered parameters の陰に身を潜めてしまっている感があります。 僕自身もPattern matching、Numbered parameters を先にまとめているので、何にも言えませんが。。。

w/o .:
Dir["*/*.c"].map { |f| File.basename(f) }

w/ Object#method
Dir["*/*.c"].map(&File.method(:basename))

w/ .:
Dir["*/*.c"].map(&File.:basename)

.: の導入により、上記のように書くことが可能になりました。 こう比べてみるとスッキリしましたね。

そういえば、Numbered parameters を用いてもかけますね。

w/ Numbered parameters
Dir["*/*.c"].map { File.basename(@1) }

少しだけ、Method reference operator .: の方が記述量少ないのか🤔 にしても、Numbered parameters も便利ですね。

これまでそもそも Object#method 知らなかったので、 これを機にガシガシ Object#method、Method reference operator .: 使っていきたいと思います。

では、今回はこの辺で。

最後まで読んで頂き、ありがとうございます🙇

参考

Ruby 2.7 の新機能(Numbered parameters 編) - チケットとコミットログを読んでみて

5月30日(木)、Ruby 2.7.0 preview1 がリリースされましたね。

パチパチパチ👏🎉👏

さて、前回のパターンマッチング編に引き続きRuby 2.7 の新機能、Numbered parameters についてまとめていきます。

Numbered Parameters

TL;DR

  • ブロックパラメターに @1, @2 という新しい記法を用いることが可能。
  • @n の記法はインスタンス変数を想起させ見慣れないが、時が解決してくれる(きっと)。
  • Hash でNumbered parameters を用いると著しく可読性が下がる(気がする)。

使用例

#w/o Numbered parameters
[1, 2, 3].map { |n| n + 2 }

#w/ Numbered parameters
[1, 2, 3].map { @1 + 2 }

#w/o Numbered parameters
{ key1: 'foo', key2: 'bar' }.map { |key, val| [key, val] }

#w/ Numbered parameters
{ key1: 'foo', key2: 'bar' }.map { [@1, @2] }

今まではこんな感じでブロックパラメターに名前をつける必要がありました。 しかし、Ruby 2.7 では @ を用いたデフォルト値を使うことができます。 便利そうですが、Ruby@ というとインスタンス変数を想起してしまいます。。。

チケット

今回もチケットの内容を読み解いてみることにします。

bugs.ruby-lang.org

@my_sons.each { |s| s.sell_to_someone }

Account.all.each { |a| my_account << a.money }

そもそもNumbered parameters という概念の始まりは、

ブロック中の変数の名前、そこまで重要でない時もありますよね。 もしかして、デフォルト値作ったら便利じゃないですか?

というところのようです。 ちなみに、チケットでは下記のようなサンプルコードも挙げられています。

Account.all.each { my_account << it.money }

お、it が。。。 これはRSpec の方々が黙ってなさそう。 と思ったらやはり、RSpec 等の既存のコードとの互換性の問題で it は却下されていました。

現状trunk にマージされたNumbered parameters は @n という形を取っています。

Matzさん曰く、「@ の見た目は、最初慣れないけどそのうち見慣れるでしょ!」とのことです。

I still feel weird when I see @ and @1 etc. Maybe I will get used to it after a while. I need time.

議論の中では先ほど挙げた it 、そして意味的にそれに近い this。 記号系では、\n$n&n. なんかも候補に挙げられてました。 あと、`(backtick)? この辺の記号を地上げして使えば良いみたいな少し過激な意見もありました。

そしてこのNumbered parameters ですが、非常に議論が盛り上がっているようで、 trunk にマージされた後も新規でチケットが起票されていました。

bugs.ruby-lang.org

その他個人的に印象に残った意見が、数文字のタイプを減らすために、ここまで可読性を犠牲にするのはどうかと言ったものです。 さらには、可読性が落ちることで初学者の学習コストが上がると言ったところ言及されており、 機能追加の段階でここまでの議論がなされているものなのかと驚きました。

また、Numbered parameters を利用した実例として、ネガティブな反応が多かったのがHash を用いたケース。

h = Hash.new { |hash, key| hash[key] = "Go Fish: #{key}" }

#vs

h = Hash.new { @1[@2] = "Go Fish: #{@2}" }

確かにこれは、Numbered parameters を使うとわかりづらいかも。。。

これを受けて、Matzさん

So use numbered parameters with care (just like other features in Ruby)

「(他の機能もそうだけど、)注意して使いましょうね。」とのこと。

時と場合によりけりといったところでしょうか。

コミットログ

github.com

さて、お待ちかねのコミットログです。 今回も前回同様、テストコード中心に読み進めていきます。

まずこちら、

assert_syntax_error('proc {|| @1}', /ordinary parameter is defined/)
assert_syntax_error('proc {|x| @1}', /ordinary parameter is defined/)

|x| といった現状のブロック変数との併用はできないようです。

assert_syntax_error('proc {@1 = nil}', /Can't assign to numbered parameter @1/)

ブロック中でNumbered parameter に値をアサインすることも出来ないようです。

assert_syntax_error('proc {@01}', /leading zero/)
assert_syntax_error('proc {@1_}', /unexpected/)

0 から始めるのもエラーのようですね。

アンダースコア用いることも出来ないようです。 と言っても、あんまり利用例も考えつきませんが。。。

assert_syntax_error('proc {@9999999999999999}', /too large/)

これは。。。too largeです。。。笑

assert_syntax_error('@1', /outside block/)

もちろん、ブロックの外でNumbered parameter を呼ぶのもエラーが出ます。

所感

RubyKaigi 2019 期間中初めて、Numbered parameters の存在を知ったのですが、その時は便利そうだなーと思いました。 ただ、今回チケット・コミットログを読んでみて、Hash のケース等は可読性が著しく下がってしまい、一概に便利ということではないようですね。 Matzさんもおっしゃる通り、節度を持って利用したいと思います。

Ruby 2.7 では Enumerable#filter_map というメソッドが新しく導入されています。 この辺とNumbered parameters 一緒に使ってみたいですね。 気が向いたら、Enumerable#filter_map についても記事まとめます!

では、今回はこの辺で。

最後まで読んで頂きありがとうございます🙇

参考

Ruby 2.7 の新機能(Pattern matching 編) - チケットとコミットログを読んでみて

今回はRubyKaigi 2019 に参加して俄然モチベーションが上がったので、 NEWS for Ruby 2.7.0 の中から気になった点をいくつかピックアップしてまとめていきたいと思います。*1

パターンマッチング

使用例

def pattern_match(arg)
  case arg
  in [a]
    p a
  in { key: a }
    p a
  end
rescue NoMatchingPatternError => e
  p e
end

pattern_match([1])
#=>1
pattern_match({key: 'value'})
#=>"value"
pattern_match('string')
#=>#<NoMatchingPatternError: string>

挙動としてはこのように、パターンが一致すれば値をキャプチャすることができます。 また、全てのパターンと一致しない場合は NoMatchingPatternError の例外を投げます。

ちなみに、現状Experimental なので、パターンマッチングを実行すると、 下記のようなwarning が出ます。

pattern_match/sample.rb:2: warning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!

チケット

bugs.ruby-lang.org

元々k-tsj/pattern-match というgem で開発されていたようです。 gem のリリースは2012年ということなので、遡ること7年ほど。

お恥ずかしながらこちらのgem の存在を初めて知りました。 正直、詳しいことはわかりませんが、README を見る限りRuby trunk のパターンマッチングとはシンタックス等大きく変わっているようです。

チケット内で下記の2点がデザイン・ポリシーとして挙げられています。

  • Keep compatibility
  • Be Ruby-ish

"compatibility" に関しては、RubyKaigi 2019 のなかでMatzさんを始め、 他のRuby comitter の方々も言及していたように記憶しています。

k-tsj/pattern-match の方では Object#match という形で、パターンマッチングが表現されていました。 しかし、match を導入すると、String#match や、Regexp#match 等の既存のコードを壊してしまう恐れがある。 そういった理由でmatch の利用は見送られ、case 文を拡張する形で実装が進められたようです。

"Be Ruby-ish" という点に関しては、Ruby の動的型づけ言語としての強みを活かすよう意識されたようです。

"Keep compatibility" と"Be Ruby-ish"。 言葉で言うのは簡単ですけど、実現するのは本当に大変そうです。

こういったRuby committer の方々の苦労に支えられているのですね。いつも本当にありがとうございます。 自分も少しずつRuby community に恩返ししていきたいです。

コミットログ

github.com

さて、ここからは実際のコミットログを見て少しだけ深掘りしていきます。 C言語が全く分かりませんので、今回はテストコードをヒントに紐解いていきます。

assert_block do
  case 0
  in a if a != 0
  else
    true
  end
end

このようにguard exp を置くこともできます。

assert_block do
  case 0
  in 0 => a
    a == 0
  end
end

as pattern を用いることで、このように変数に代入することも可能です。

assert_block do
  [0, 1].all? do |i|
    case i
    in 0 | 1
      true
    end
  end
end

| を用いて、or を表現することも可能です。

assert_block do
  a = /a/
  case 'abc'
  in ^a
    true
  end
end

^ を使うと、既に定義されている変数と比較することも可能のようです。 下記の通り、^ を利用するとa = 0 がパターンマッチに利用されています。

a = 0
case 1
in a
  p a
end
#=>1

a = 0
case 1
in ^a
  p a
end
#=>NoMatchingPatternError

所感

パターンマッチングは、API とかでJSON をparse するときに用いたら便利そうだなと。 強い方々がRuby 2.6 のASTxPattern matching とかよく耳にするので、そちらもやってみたいです。 まずは、AST を調べるところから始めたいと思います。

今回RubyKaigi 2019 で感化されて、初めてRubyredmine のチケットやコミットログとか見てみましたが面白かったです。

普段Ruby committer の方々がどういった意識でRuby の開発に取り組まれているのかを垣間見ることができました。

正直なところコミットログに関しては、C言語ができないと言う理由で距離を置いていました。 しかし、実際に見てみるとテストコードから基本的な挙動を理解することができました。

自分みたいなC言語できない人も安心です。

「テストコードはドキュメントの代わり」という言葉の意味をやっと理解し、テストコードの重要性を再認識しました。

とはいえ、C言語を習得してコミットログちゃんと理解できるようにしたいなぁ、、、今後の課題です。

参考

*1:まとめ始めたら長くなってしまったので、いくつかの記事に分けたいと思います。(気が向いたベースで書きます。)