Ruby の Warning[:experimental] = false は実行時に反映されないケースがある

某所でそういう話が出ていたのでいろいろと調べてまとめてみました。

Warning[:deprecated] = true / false で実行時に非推奨な警告を制御できる

Warning[:deprecated]true / false を割り当てることで実行時に非推奨な警告の出力を制御することができます。
例えば Dir.exists?Ruby 3.0 以降では非推奨なので Warning[:deprecated] が真だと警告文が出力されます。
ちなみに Ruby 2.7 系で試す場合は Enumerator.new([], :each) など使うと警告が出ます。

# Ruby 2.7.2 以降だとデフォルトでは false
pp Warning[:deprecated]
# => false

# no wanirng
Dir.exists?("")

# これに true を設定するとそれ以降に呼ばれたメソッドなどで警告が出るようになる
Warning[:deprecated] = true

# warning: Dir.exists? is deprecated; use Dir.exist? instead
Dir.exists?("")

こんな感じで実行時に警告文の出力を制御することができます。

Warning[:experimental] = true / false の場合は?

Ruby 3.0 ではいくつかの機能が実験的に導入されています。
例えば RactorRuby 3.0 時点ではまだ実験的な機能になります。
さて、このような実験的な機能を使用すると警告文が出力されることがあります。

# warning: Ractor is experimental, and the behavior may change in future versions of Ruby! Also there are many implementation issues.
Ractor.new {}

この警告文の出力ですが Warning[:experimental] を用いることで『実験的な機能に対する』警告文の出力を制御することができます。

# デフォルトでは true
pp Warning[:experimental]
# => true

# 警告が出る
# warning: Ractor is experimental, and the behavior may change in future versions of Ruby! Also there are many implementation issues.
Ractor.new {}

# 警告の出力を無効にする
Warning[:experimental] = false

# 警告が出ない
# no warning
Ractor.new {}

このように Warning[:experimental] = false にする事で警告文が出力されなくなります。

Warning[:experimental] = false で制御できないケースもある

Ruby 3.0 ではパターンマッチの in を 1行でかける構文も実装されました。
これも Ractor 同様にまだ実験的な機能なので使用すると警告文が出ます。

user = { name: "homu", age: 14 }

# warning: One-line pattern matching is experimental, and the behavior may change in future versions of Ruby!
if user in { age: 1..20 }
  # ...
end

ではこの警告文を Warning[:experimental] で制御することができるのでしょうか?結論から言うとできませんでした。

user = { name: "homu", age: 14 }

# 実験的な警告を無効にする
Warning[:experimental] = false

# 実験的な警告を無効にしているのに警告が出てしまう
# warning: One-line pattern matching is experimental, and the behavior may change in future versions of Ruby!
if user in { age: 1..20 }
  # ...
end

これはなぜかというと in を使ったときの警告文の出力が『コンパイル時』に制御されているためです。
なので次のように Ractor.new が実行時に呼ばれないと警告文を出さないのに対して、 in の場合は実行時に呼ばれていなくても警告文がでます。

def foo
  # これは実行時に foo メソッドが呼ばれていないので警告はでない
  Ractor.new
end

def hoge
  # これは実行時に hoge メソッドが呼ばれていなくても警告が出る
  42 in n
end

一方で Warning[:experimental] = false というコードは『実行時』に制御を行っているため、コンパイル時には反映されません。
なので例えば eval などを使って実行時に Ruby のコードを実行する場合は Warning[:experimental] の設定が反映されます。

user = { name: "homu", age: 14 }

Warning[:experimental] = true

# warning: One-line pattern matching is experimental, and the behavior may change in future versions of Ruby!
eval <<~EOS
if user in { age: 1..20 }
  # ...
end
EOS

Warning[:experimental] = false

# no warning
eval <<~EOS
if user in { age: 1..20 }
  # ...
end
EOS

このように Warning[:experimental] で警告文の出力を制御する場合は反映される機能とそうでない機能があるので注意する必要があります。

実験的な機能の警告を無効にするには?

Ruby のコマンドオプションに -W:no-experimental を付けることで警告文を無効にすることができます。

$ cat main.rb
user = { name: "homu", age: 14 }

if user in { age: 1..20 }
  # ...
end
# 警告が出る
$ ruby main.rb
main.rb:3: warning: One-line pattern matching is experimental, and the behavior may change in future versions of Ruby
# -W:no-experimental を付けると警告がでない
$ ruby -W:no-experimental main.rb
$

実験的な機能の警告文を無効にしたい場合はコマンドオプションで制御する方がよさそうですね。

おれの考えた最強の denite.nvim の設定

年明けぐらいからちょこちょこ denite.nvim を使い始めて設定を追加したりプラグイン側でいろいろと対応してもらったりしていたのですがだいぶいい感じになってきたので一旦現状の設定を書き溜めておきます。

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" denite のバッファの設定
augroup my_denite
    autocmd!
    autocmd FileType denite call s:denite_my_settings()
    autocmd FileType denite-filter call s:denite_filter_my_settings()
augroup END


"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" denite.nvim のバッファの設定
function! s:denite_my_settings()
    nnoremap <silent><buffer><expr> <CR>
   \ denite#do_map('do_action')
    nnoremap <silent><buffer><expr> d
   \ denite#do_map('do_action', 'delete')
    nnoremap <silent><buffer><expr> p
   \ denite#do_map('do_action', 'preview')
    nnoremap <silent><buffer><expr> q
   \ denite#do_map('quit')
    nnoremap <silent><buffer><expr> i
   \ denite#do_map('open_filter_buffer')
    nnoremap <silent><buffer><expr> <Space>
   \ denite#do_map('toggle_select').'j'
    nnoremap <silent><buffer><expr> <C-Space>
   \ denite#do_map('toggle_select').'j'
    nnoremap <silent><buffer><expr><nowait> t
   \ denite#do_map('do_action', 'tabswitch')
    nnoremap <silent><buffer><expr> a
   \ denite#do_map('choose_action')
    nnoremap <silent><buffer><expr> <C-g>
   \ denite#do_map('echo')
endfunction


"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" denite.nvim のフィルタバッファの設定
function! s:denite_filter_my_settings() abort
    augroup ftplugin-my-denite
        autocmd! * <buffer>
       " denite-filter 用のキーマッピング
       " NOTE: このタイミングじゃないとキーマッピングが反映されない
       " フィルタリング中に Enter を押すと選択されている候補のデフォルトアクションを実行する
        autocmd InsertEnter <buffer> imap <silent><buffer> <CR> <ESC><CR><CR>
       " インサートを抜けた時に自動的にフィルタウィンドウを閉じる
        autocmd InsertEnter <buffer> inoremap <silent><buffer> <Esc> <Esc><C-w><C-q>:<C-u>call denite#move_to_parent()<CR>
    augroup END

   " フィルタバッファでは自動補完を無効にしておく
    call deoplete#custom#buffer_option('auto_complete', v:false)

   " ステータスラインに file/rec(10/100) のような候補数を表示させる
    setlocal statusline=%!denite#get_status('sources')

   " カーソルキーで候補の選択を移動させる
    inoremap <silent><buffer> <Down> <Esc>
       \:call denite#move_to_parent()<CR>
       \:call cursor(line('.')+1,0)<CR>
       \:call denite#move_to_filter()<CR>A
    inoremap <silent><buffer> <Up> <Esc>
       \:call denite#move_to_parent()<CR>
       \:call cursor(line('.')-1,0)<CR>
       \:call denite#move_to_filter()<CR>A
   " 同様のことを <C-j><C-k> で
    inoremap <silent><buffer> <C-j> <Esc>
       \:call denite#move_to_parent()<CR>
       \:call cursor(line('.')+1,0)<CR>
       \:call denite#move_to_filter()<CR>A
    inoremap <silent><buffer> <C-k> <Esc>
       \:call denite#move_to_parent()<CR>
       \:call cursor(line('.')-1,0)<CR>
       \:call denite#move_to_filter()<CR>A
   " 同様のことを <C-j><C-k> で
    inoremap <silent><buffer> <C-n> <Esc>
       \:call denite#move_to_parent()<CR>
       \:call cursor(line('.')+1,0)<CR>
       \:call denite#move_to_filter()<CR>A
    inoremap <silent><buffer> <C-p> <Esc>
       \:call denite#move_to_parent()<CR>
       \:call cursor(line('.')-1,0)<CR>
       \:call denite#move_to_filter()<CR>A

    return

   " インサートを抜けた時に自動的に候補のバッファに移動する
    imap <silent><buffer> <Esc> <Esc>:call denite#move_to_parent()<CR>
    imap <silent><buffer> <C-[> <C-[>:call denite#move_to_parent()<CR>

   " フィルタバッファで <CR> すると候補を実行する
    inoremap <silent><buffer> <CR> <Esc>
       \:call denite#move_to_parent()<CR>
       \<CR>
   " 別キーで実装
    inoremap <silent><buffer> <C-CR> <Esc>
       \:call denite#move_to_parent()<CR>
       \<CR>
    inoremap <silent><buffer> <C-m> <Esc>
       \:call denite#move_to_parent()<CR>
       \<CR>

endfunction



"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" :Denite のデフォルトの設定
let s:denite_default_options = {}


" 絞り込んだワードをハイライトする
call extend(s:denite_default_options, {
\   'highlight_matched_char': 'None',
\   'highlight_matched_range': 'Search',
\   'match_highlight': v:true,
\})

" denite を上に持っていく
call extend(s:denite_default_options, {
\   'direction': "top",
\   'filter_split_direction': "top",
\})

" フィルタのプロンプトを設定
call extend(s:denite_default_options, {
\   'prompt': '> ',
\})

" 大文字小文字を区別してフィルタする
call extend(s:denite_default_options, {
\   'smartcase': v:true,
\})

" ステータスラインに入力を表示しないようにする
" call extend(s:denite_default_options, {
"\    'statusline': v:true,
"\})


" デフォルトで絞り込みウィンドウを開く
" call extend(s:denite_default_options, {
" \   'start_filter': v:true,
" \})

call denite#custom#option('default', s:denite_default_options)



"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" kind の設定

" ファイルを開く際のデフォルトアクションを tabswitch にする
call denite#custom#kind('file', 'default_action', 'tabswitch')



"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" file の設定
" call denite#custom#source('file', 'matchers', ['matcher/regexp'])
if &rtp =~ "devicons"
    call denite#custom#source('file', 'converters', ['devicons_denite_converter', 'converter/abbr_word'])
else
    call denite#custom#source('file', 'converters', ['converter/abbr_word'])
endif


"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" file/rec の設定
call denite#custom#source("file/rec", "max_candidates", 100)
if &rtp =~ "devicons"
    call denite#custom#source('file/rec', 'converters', ['devicons_denite_converter', 'converter/abbr_word'])
else
    call denite#custom#source('file/rec', 'converters', ['converter/abbr_word'])
endif


" プロジェクト直下のファイル一覧を表示する + 新規ファイル作成
nnoremap <Space>uff :DeniteProjectDir file/rec<CR>


"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" quickrun_config の設定

" denite-quickrun_config の並び順を単語順にする
call denite#custom#source('quickrun_config', 'sorters', ['sorter/word'])


"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" grep の設定
" ripgrep で grep
if executable("rg")
    call denite#custom#var('file/rec', 'command',
   \ ['rg', '--files', '--glob', '!.git', '--color', 'never'])
    call denite#custom#var('grep', {
   \ 'command': ['rg'],
   \ 'default_opts': ['-i', '--vimgrep', '--no-heading'],
   \ 'recursive_opts': [],
   \ 'pattern_opt': ['--regexp'],
   \ 'separator': ['--'],
   \ 'final_opts': [],
   \ })
endif


call denite#custom#source("grep", "max_candidates", 300)
" ファイル名を含めて絞り込めるようにする converter/abbr_word を追加する
" devicons_denite_converter を使うと絞り込みがおかしくなるので一旦無効にする
" call denite#custom#source('grep', 'converters', ['converter/devicons_denite_converter', 'converter/abbr_word'])
call denite#custom#source('grep', 'converters', ['converter/abbr_word'])
nnoremap <Space>gr :DeniteProjectDir grep<CR>




"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" gitto の設定

command! GitBranch Denite gitto/branch

denite-grep 周りでプラグイン間の競合があり、かなり設定に手間取っていたんですがだいぶ理想の挙動になりました。
設定を覚えるのは難しいですが、設定すればだいたいのことを実現することができるので便利ですねえ。

2021/05/06 今週の気になった bugs.ruby のチケット

内容は適当です。
今週と言っても今週みかけたチケットなだけでチケット自体は昔からあるやつもあります。
あくまでも『わたしが気になったチケット』で全ての bugs.ruby のチケットを載せているわけではありません。

[Bug #17838] Set#intersect? and enumerables

  • 現状だと Set#intersect? には Set しか渡せないんですがそれを Enumerable を渡せるようにしようという提案っぽい
[1, 2, 3].intersect?(Set[2, 3, 4]) # => true
Set[2, 3, 4].intersection([1, 2, 3]) # => Set[2, 3]
# これを渡せるようにしたいぽい?
Set[2, 3, 4].intersect?([1, 2, 3]) # => ArgumentError

2021/04/29 今週の気になった bugs.ruby のチケット

内容は適当です。
今週と言っても今週みかけたチケットなだけでチケット自体は昔からあるやつもあります。
あくまでも『わたしが気になったチケット』で全ての bugs.ruby のチケットを載せているわけではありません。

[Feature #17830] Add Integer#previous and Integer#prev

  • Integer#next の逆の Integer#previous を追加する提案
  • 現状は Integer#nextエイリアスとして Integer#succ があり、それの逆の Integer#pred がある
  • Integer#succ <-> Integer#pred はわかりやすいが、 Integer#next <-> Integer#pred は分かりづらいので Integer#next <-> Integer#previous を追加しよう、というモチベーションらしい
  • Integer#pred 自体使ったことなかったけど実際どういうケースで使うんですかね?
  • コメントには (number - 1).times { ... })number.pred.times { ... }) みたいに書くとはかかれていた

[Feature #17837] Add support for Regexp timeouts

# この処理はいつまで経っても終わらない…
/A(B|C+)+D/ =~ "A" + "C" * 100 + "X"

2021/04/22 今週の気になった bugs.ruby のチケット

内容は適当です。
今週と言っても今週みかけたチケットなだけでチケット自体は昔からあるやつもあります。
あくまでも『わたしが気になったチケット』で全ての bugs.ruby のチケットを載せているわけではありません。

[Bug #9542] Delegator does not delegate protected methods

  • 以下のコードが Ruby 2.0 と 2.1 で差異がある、というバグ報告
require "delegate"
class Cow
  def unprotected_moo
    "mooooo!"
  end

  protected

  def protected_moo
    "guarded mooooo!"
  end
end
my_cow = Cow.new
puts SimpleDelegator.new(my_cow).unprotected_moo
# => "mooooo!"

# protected_moo は protected なのに呼び出すことができる
puts SimpleDelegator.new(my_cow).protected_moo
# 2.0      => "guarded mooooo!"
# 2.1 以降 => undefined method `protected_moo' (NoMethodError)

puts my_cow.unprotected_moo
# => "mooooo!"
puts my_cow.protected_moo
# 2.0      => "guarded mooooo!"
# 2.1 以降 => protected method `protected_moo' called (NoMethodError)
  • これ自体は期待する挙動なので Reject されている
    • そもそもチケット自体が7年前のやつだった

[Bug #17814] inconsistent Array.zip behavior

  • 以下のように Array#zip だとイテレーションが1回余計に呼ばれているというバグ報告
i = 0
# 1 ずつ増えるカウンタ
e = Enumerator.produce { i += 1 }

# 1つ余計にイテレーションが発生する
p [0, 0, 0, 0].zip e
# => [[0, 1], [0, 2], [0, 3], [0, 4]]
p i
# => 5

# Enumerable#zip だと再現しない
p [0, 0, 0, 0].each.zip e
# => [[0, 6], [0, 7], [0, 8], [0, 9]]
p i
# => 9

[Feature #17785] Allow named parameters to be keywords

  • 先週も書いてた キーワード_ という名前で変数参照できるようにしよう、という提案
def check(arg, class:)
  # _ を付けて参照できるようにする
  arg.is_a?(class_)
end

check(42, class: Integer) # => true
  • __params__ みたいな値で受け取れるようにしようという提案があってこれは普通にほしいと思った
def check(arg, class:)
  arg.is_a?(__params__[:class])
end

check(42, class: Integer) # => true
  • ただし、キーワード引数以外の引数をどうするかの課題がある
    • __params__ というよりかは __keyword_params__ みたいな?

[Feature #17718] a method paramaters object that can be pattern matched against

  • 次のようにキーワード引数を一括で返すメソッドを追加する提案
    • パターンマッチで利用することを想定している
def get_perdiem(city: nil, state: nil, zip:nil)

  case parameters_match  # (return an object of the parameters we can pattern match on)
  in {zip: zip}
     find_perdiem_by_zip(zip)
  in {state: s, city: c}
     find_perdiem_by_state_and_city(s, c)
  in { state: s}
     find_perdiem_by_state(s)
  else
     raise 'need combination of zip, city,state'
  end
end
def getParam(**args)
  case args
  in {zip: zip}
    p "zip"
  in {state: s, city: c}
    p "state+city"
  in { state: s}
    p "state"
  else
    raise 'need combination of zip, city, state'
  end
end

[Bug #4443] odd evaluation order in a multiple assignment

def foo
  p :foo
  []
end
def bar
  p :bar
end

# bar -> foo という順に評価される
x, foo[0] = bar, 0
# output:
# :bar
# :foo

# これは foo -> bar という順になる
foo[0] = bar
# output:
# :foo
# :bar
def foo
  p :foo
  []
end
def bar
  p :bar
end

# 最新版だと foo -> bar と評価されるようになった
x, foo[0] = bar, 0
# output:
# :foo
# :bar

[Feature #17398] SyntaxError in endless method

  • 以下のようにエンドレスメソッド定義の本体が statement だった場合にエラーになる、というチケット
# OK
def foo() = puts("bar")

# syntax error, unexpected string literal, expecting `do' or '{' or '('
def hoge() = puts "bar"
# current: SyntaxError
# patch: Allow
def foo() = puts "bar"

# current: SyntaxError
# patch: SyntaxError
private def foo() = puts "bar"

[Feature #15198] Array#intersect?

  • Array#intersect? を追加する提案
  • これは以下の動作と同じ意味
    • Array#&
    • [1, 1, 2, 3] & [3, 1, 4] #=> [1, 3]
(a1 & a2).any?

2021/04/18 今週の気になった bugs.ruby のチケット

内容は適当です。
今週と言っても今週みかけたチケットなだけでチケット自体は昔からあるやつもあります。
あくまでも『わたしが気になったチケット』で全ての bugs.ruby のチケットを載せているわけではありません。

[Feature #17790] Have a way to clear a String without resetting its capacity

  • 文字列の中身を空にする String#clear メソッドがある
str = "abc"
str.clear
p str     # => ""
  • この String#clear メソッドは capacity を含めて解放する
require "objspace"

str = String.new("homuhomu", capacity: 1024)
puts ObjectSpace.dump(str)
# => {"address":"0x55d9bdae3e80", "type":"STRING", "class":"0x55d9bd96a888", "bytesize":8, "capacity":1024, "value":"homuhomu", "encoding":"UTF-8", "memsize":1065, "flags":{"wb_protected":true}}

# clear 後は capacity も含めて解放されている
str.clear
puts ObjectSpace.dump(str)
# => {"address":"0x55d9bdae3e80", "type":"STRING", "class":"0x55d9bd96a888", "embedded":true, "bytesize":0, "value":"", "encoding":"UTF-8", "memsize":40, "flags":{"wb_protected":true}}
  • これは例えば次のように同じバッファを使い回す時に解放されないほうがよいケースもある
buffer = String.new(encoding: Encoding::BINARY, capacity: 1024)

10.times do
  build_next_packet(buffer)
  udp_socket.send(buffer)
  buffer.clear
end
  • なので clear では capacity を開放しないようにする、という提案
  • 議論が進んでいて clear(shrink: true/false) みたいに clear 時に制御するのがいいんじゃない?みたいなコメントもでてる

[Feature #17785] Allow named parameters to be keywords

  • 次のようにキーワード引数に『言語のキーワード』を指定すると参照するのが難しい
def check(arg, class:)
  # ここで class 引数を使いたいが class はキーワードなので参照できない
  arg.is_a?(class)
end

check(42, class: Integer) # => true
  • これを キーワード_ という名前で変数参照できるようにしよう、という提案
def check(arg, class:)
  # _ を付けて参照できるようにする
  arg.is_a?(class_)
end

check(42, class: Integer) # => true
def check(arg, class:)
  # local_variable_get で動的に変数を取得する
  class_ = binding.local_variable_get(:class)
  arg.is_a?(binding.local_variable_get(:class))
end

check(42, class: Integer) # => true
def check(arg, class:)
  arg.is_a?(\class)
end

# キーワード引数以外も適用可能
def diff(start, \end)
  \end - start
end

[Feature #17786] Proposal: new "ends" keyword

  • 複数の endends という1つのキーワードで定義できるようにしようという提案
def render(scene, image, screenWidth, screenHeight)
  screenHeight.times do |y|
    screenWidth.times do |x|
      color = self.traceRay(....)
      r, g, b = Color.toDrawingColor(color)
      image.set(x, y, StumpyCore::RGBA.from_rgb(r, g, b))
    end
  end
end
  • これを以下のように記述する
def render(scene, image, screenWidth, screenHeight)
  screenHeight.times do |y|
    screenWidth.times do |x|
      color = self.traceRay(....)
      r, g, b = Color.toDrawingColor(color)
      image.set(x, y, StumpyCore::RGBA.from_rgb(r, g, b))
ends
  • いろいろとコメントされているが流石に否定的な内容が目立つ…
  • 例えば次のように途中に ends がある場合に class Aend が不用意に閉じられて SyntaxError になる
class A
  def b
    c do
      d do
        e
  ends     # ここで class A のスコープが閉じられる

  def c
  end
end        # なのでここで SyntaxError になる

jq コマンドを使って Vim で選択した範囲の JSON をフォーマッティングする

書いてみた。

" 選択した範囲を jq コマンドを使ってフォーマッティングする
" 範囲選択しない場合は現在の行を対象とする
command! -range JSONFormatter :<line1>,<line2>!jq .

べんり。
別途 jq 外部コマンドが必要なので注意。