7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

はじめに

Crystalにはwith ... yieldという構文があります。これはブロック呼び出しのコンテキストをwithyieldの間の式を評価した結果のオブジェクトにして、ブロックを呼び出す構文です。

DSLを作る際に便利なのですが、あまり使われていないような印象があります。

今日Crystalに出したPull Requestでwith ... yieldを使ったので、その辺の話をします。

本編

with ... yieldって?

with yieldというのは、ブロック呼び出しの際にブロック内のスコープを指定したオブジェクトに変える構文です。

class Foo
  private def private_foo
    42
  end
end

def foo
  with Foo.new yield
end

foo do
  p private_foo # => 42
end

ここでFooprivate_fooが呼び出せていることに注目してください。
このようにwith ... yieldで呼び出されたブロックの中ではレシーバーが無いためプライベートメソッドが呼び出せます。

これを使うと、ブロック内でのみ有効なメソッドを作ることをができて、DSLを作るのに便利です。

with ... yieldをCrystalのコンパイラに出したPull Requestに使った話

ところで、今日こんなPull RequestをCrystalに出しました。

Spec: detect nesting it/pending and raise compilation error

https://github.com/crystal-lang/crystal/pull/7207

これはCrystalの標準ライブラリのspecitpendingがネストしているときにコンパイルエラーにする、というPull Requestです。

例えばこのようなソースコードがあったとき、

require "spec"

it "foo" do
  pending
end

こんな風にエラーになります。

$ crystal run foo.cr
Error in foo.cr:3: instantiating 'it(String)'

it "foo" do
^~

in foo.cr:3: cannot nest 'pending' of spec

it "foo" do
^~

これは、with ... yieldを使って実現しています。

まず、このようなScopeモジュールを用意します。

  module Scope
    macro it(description = "assert", file = __FILE__, line = __LINE__, end_line = __END_LINE__, &block)
      {{ raise("cannot nest 'it' of spec") }}
    end

    macro pending(description = "assert", file = __FILE__, line = __LINE__, end_line = __END_LINE__, &block)
      {{ raise("cannot nest 'pending' of spec") }}
    end
  end

そして、元のitpendingメソッドの中でwith Scope yieldとしてブロックを呼び出します。

  def it(description = "assert", file = __FILE__, line = __LINE__, end_line = __END_LINE__, &block)
    return unless Spec.matches?(description, file, line, end_line)

    Spec.formatters.each(&.before_example(description))

    start = Time.monotonic
    begin
      Spec.run_before_each_hooks
      with Scope yield # ←ここ
      Spec::RootContext.report(:success, description, file, line, Time.monotonic - start)
    rescue ex : Spec::AssertionFailed
      Spec::RootContext.report(:fail, description, file, line, Time.monotonic - start, ex)
      Spec.abort! if Spec.fail_fast?
    rescue ex
      Spec::RootContext.report(:error, description, file, line, Time.monotonic - start, ex)
      Spec.abort! if Spec.fail_fast?
    ensure
      Spec.run_after_each_hooks
    end
  end

  # ...

  def pending(description = "assert", file = __FILE__, line = __LINE__, end_line = __END_LINE__, &block)
    return unless Spec.matches?(description, file, line, end_line)

    # Run block on compile-time to detect nesting `it` and `pending`.
    typeof(with Scope yield) # ←ここ

    Spec.formatters.each(&.before_example(description))

    Spec::RootContext.report(:pending, description, file, line)
  end

ここで、pendingではtypeof(with Scope yield)としていて、コンパイルはされるけれど実行されていないことも重要です。

このように、with ... yieldはブロックの中でしか呼び出せないメソッドを表現することもできますが、一方ブロック内で呼び出してはいけないメソッド、というものも表現できます。

(ただ、この方法だとitpendingがブロック外にあった場合に検出できないので、実行時のチェックも必要かな、とか思っています)

まとめ

with ... yieldは便利なのでもっと使われればいいと思いますが、一方わりとバグっぽい挙動をすることもあるので本当に薦めていいのか悩むところもあります。

とりあえずみなさんCrystalやっていきましょう。

Crystal Advent Calendar 2018の20日目でした。

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?