Help us understand the problem. What is going on with this article?

[翻訳] Elixir 1.2の`with`を使い(倒し)ます

More than 3 years have passed since last update.

Dave Thomas1 さんの2016年2月23日付けのブログ記事、(Over)using with in Elixir 1.2の翻訳です。
以前Elixirのwithを学ぶにも別の翻訳記事を書きましたがあちらは主に「複数のマッチ式を扱える」という説明でした。
この記事では「ローカルスコープの変数が使える」という点にもポイントを置いて説明されています。なるほど…!


Elixir 1.2で表現型withが導入されています。新しすぎてこのブログで使っているシンタックスハイライト機能が知らないぐらいです2

withは他の関数型言語のletにローカルスコープの変数を定義するという点で少し似ています。こんな感じに書けるということです。

owner = "Jill"

with name  = "/etc/passwd",
     stat  = File.stat!(name),
     owner = stat.uid,
do:
     IO.puts "#{name} is owned by user ##{owner}"

IO.puts "And #{owner} is still Jill"

with式は2つの部分からできています。最初の部分は式のリスト、後の部分はdoブロックです。初期化式が順番に評価され続いてdoブロック内のコードが評価されます。withの内側で初めて出てきた変数は全てそのwithのローカル変数となります。この例の場合、owner = stat.uidの行は新しく変数を生成し、同じ名前3の外側のスコープの変数のバインドを変更しません。

そして、大きな利点ですが、パイプラインに素直に食わせられないような複雑な関数呼出しのシーケンスを自然に分解してくれます。おおまかに言えばテンポラリ変数を使えるようになったということです。それによってコードを読むのも楽しくなります。

例として、私が1年前に書いたコードを挙げましょう。これはEarmark マークダウンパーサーのコマンドラインオプションを処理する部分です:

defp parse_args(argv) do
  switches = [
    help: :boolean,
    version: :boolean,
    ]
  aliases = [
    h: :help,
    v: :version
  ]

  parse = OptionParser.parse(argv, switches: switches, aliases: aliases)

  case parse do

  { [ {switch, true } ],  _,  _ } -> switch
  { _, [ filename ], _     } -> open_file(filename)
  { _, [ ],          _ }     -> :stdio
    _                        -> :help
  end
end

ベタベタですね!このコードをよく見て何回switches変数が関数内で使われているか当ててみてください。じっくり構文解析してみないとわからないと思います。また末尾にある汚いcase式も大したことない、というわけでもありません。

では次に今朝私がこれをどう書き直したか見てみましょう:4

defp parse_args(argv) do
  parse =
    with switches = [ help: :boolean, version: :boolean ],
         aliases  = [ h: :help, v: :version ],
    do:
         OptionParser.parse(argv, switches: switches, aliases: aliases)

  case parse do
    { [ {switch, true } ],  _,  _ } -> switch
    { _, [ filename ], _     }      -> open_file(filename)
    { _, [ ],          _ }          -> :stdio
      _                             -> :help
  end
end

今度はswitchesaliasesのスコープは明確です。caseの中で使われていないということもはっきりわかります。

それでもまだparseという変数があります。これはネストしたwithで何とかすることもできますが関数が読みにくくなりそうです。代わりにこれを2つのヘルパー関数を使うようにリファクタリングするほうがよさげです:

defp parse_args(argv) do
  argv
  |> parse_into_options
  |> options_to_values
end

defp parse_into_options(argv) do
  with switches = [ help: :boolean, version: :boolean ],
       aliases  = [ h: :help, v: :version ],
  do:
       OptionParser.parse(argv, switches: switches, aliases: aliases)
end

defp options_to_values(options) do
  case options do
    { [ {switch, true } ],  _,  _ } -> switch
    { _, [ filename ], _     }      -> open_file(filename)
    { _, [ ],          _ }          -> :stdio
      _                             -> :help
  end
end

ぐっとよくなりました。読みやすいし、テストしやすいし、そして変更しやすい。

ところで、ここに来てどうして私がwith式をparse_into_options関数に残したのか訝しがられるかもしれません。いい質問ですね。それについてはwithの2つめの使い方について述べた後にお答えしましょう。

withとパターンマッチング

前の節ではコマンドライン引数のパースをやりました。ではそれを(ちょっとだけ)変更して関数間で受け渡されるオプションの確認について見てみましょう。

私はGitLab、つまりGitHubのオープンソースのそっくりさんのElixirインタフェースを書いている真っ最中です。それは単純ですが少なくとも数十から数百個の幅広いJSON REST API5呼び出しです。それらのAPIのほとんどが名前付きパラメータのセットとともに呼び出されます。パラメータのいくつかは必須ですしオプショナルなパラメータもあります。例えばユーザーを生成するためのAPIは4つの必須パラメータ(email, name, password, username)に加えてオプショナルなパラメータの山(bio, Skype及びTwitterのハンドル、などなど)を持ちます。

私のインタフェースコードで渡されるパラメータがGitLab APIの仕様に合うかどうか確認したかったので簡単なオプションチェックライブラリを書きました。どのように使うかのヒントをいくつか示します:

@create_options_spec %{
  required: MapSet.new([ :email, :name, :password, :username ]),
  optional: MapSet.new([ :admin, :bio, :can_create_group, :confirm,
                         :extern_uid, :linkedin, :projects_limit,
                         :provider, :skype, :twitter, :website_url ])
}

def create_user(options) do
  { :ok, full_options } = Options.check(options, @create_options_spec)
  API.post("users", full_options)
end

オプションの仕様は2つのキー:required:optionalを持つマップです。これをOptions.chekに渡すとAPIに渡すオプションが全ての必須の値が含まれていてオプショナルな値がoptionalセットに入っているかを確認します。

以下にチェッカーの最初の実装を示します:

def check(given, spec) when is_list(given) do
  with keys = given |> Dict.keys |> MapSet.new,
  do:
       if opts_required(keys, spec) == :ok && opts_optional(keys, spec) == :ok do
         { :ok, given }
       else
         :error
       end
end

与えられたオプションからキーを抽出し、全ての必須の値が含まれているか及びオプショナル値のリストに何かキーがあるかを確認する2つのヘルパーメソッドを呼び出します。どちらもチェックが通れば:okを、ダメなら{:error, msg}を返します。

このコードで動くんですがコンパクトさを保つためにエラーメッセージを犠牲にしています。どちらかのチェック関数が:okを返せなければ処理を中断して:errorを返します。

ここでwithが光ります。withdoの間の式のリストの中で新しい条件パターンマッチ演算子<-を使うことができます6

def check(given, spec) when is_list(given) do
  with keys = given |> Dict.keys |> MapSet.new,
       :ok <- opts_required(keys, spec),
       :ok <- opts_optional(keys, spec),
  do:
       { :ok, given }
end

<-演算子は=のようにパターンマッチを行います。もしマッチングが成功するとこれら2つの効果は全く同じです-必要であれば左辺の変数が値とバインドされ処理が続行します。

=<-はマッチングが失敗した場合に異なる挙動をします。=演算子は例外を投げます。しかし<-はちょっとずるい動きをします: with式の実行を終了しますが例外を投げません。代わりにwith式はマッチできなかった値を返します。

このオプションチェッカーではもし必須及びオプショナルのチェックが両方共:okを返した場合、処理はずーっと流れていってwith{:ok, given}のタプルを返します。

しかしどちらかが失敗したら、それは{:error, msg}を返します。そうなると<-演算子はマッチしないので処理は早いうちにwith節を抜けます。その値はエラーのタプルで、この関数の戻り値になります。

こじつけ気味の要点

新しく追加されたwith式はあなたに2つの素晴らしい機能をきちんと箱詰めして提供します:レキシカルスコープと失敗時の早期離脱です。

これでコードがよくなりますね。

使いましょう。

どんどん。

私と Joséの違うところ

Elixir FountainでJohnny Winnが数週間前に José7 にインタビューしています。

話はElixir 1.2の新機能に及び、Joséはwithについて述べていました。結局、なんというか彼は控えめで、あんまり使う必要はないかもしれない、でも使ったならばすごく有益なんだけど、としか語りませんでした。Elixirのソースコードではたしか数回しか使われていないとも。

私が思うにwithはそんなもんじゃないです。確かに必要に迫られることはめったにないかもしれませんが、使うことで何度も利益を得られるでしょう。私は関数レベルのローカル変数を作るときには常にwithを使うという実験をしています。

そしてわかったのは、この訓練によって私はよりシンプルで単機能の関数を書くようになったということです。もしある関数がwithによって簡単にローカルスコープに何かを閉じ込めることができなかったら、それを2つに分割できないかと考える時間を取るようになりました。そのように分割すると常に私のコードは改良されるのです。

それがwithを先ほどのparse_into_options関数に残しておいた理由です8

defp parse_into_options(argv) do
  with switches = [ help: :boolean, version: :boolean ],
       aliases  = [ h: :help, v: :version ],
  do:
       OptionParser.parse(argv, switches: switches, aliases: aliases)
end

やれ、という訳ではないですが、私は関数の2つの部分の輪郭を描くやり方が好きです。そうすると何がおまけの部分で何が本質かクリアになるからです。私の頭の中ではそういうコードは単純で直線的なコードには欠けているストーリー性を持った構造があるのです。

まあ、これは根拠のない意見に過ぎません。ただあなたもこの技術的な難敵で数週間実験してどんな風に役立つか知りたくなったのではありませんか?



  1. "Programming Elixir"の著者。 

  2. Qiitaのシンタックスハイライト機能も未対応です。早く対応してください。 

  3. 1行目のowner = "jill" 

  4. Githubを見てみましたが3月2日時点ではまだ書き直したものはコミットされていないようです 

  5. こんな感じです(http://doc.gitlab.com/ce/api/README.html

  6. 公式ドキュメントのwith参照。with式専用です。 

  7. José Valim, Elixirの作者。 

  8. ここは分割しないでwith式を使うほうがうまいやり方ということでしょう。 

HirofumiTamori
黒猫の錬金術師と呼ばれたいおっさん
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした