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

Elixirで試しに何か書いてみる(その5) - 失敗したらやり直す

More than 5 years have passed since last update.

今回は例外処理について1

Elixirで試しに何か書いてみる(その4) - Taskを使って簡単にするを書いたあと、Taskのタイムアウト時間を修正したりパースするタグをいじったりと修正を繰り返していたのですが「そういえばいちいちどっちのタイプとか指定したくないなあ」と思いました。

どっちのタイプ、というのはgithubのリリースページを調べてみて

簡単にリリースバージョンだけ並んでるこういうやつを :type1
jquery_github.png

リリースノートなどが混じって書かれているのを :type2
TypeScript_guthub.png

と分類してソースの中のURLとともにこんな感じで

    urls = [ #{ URL, type}
      {"https://github.com/jquery/jquery/releases", :type1},
      {"https://github.com/angular/angular/releases", :type1},
      {"https://github.com/facebook/react/releases", :type2},
      {"https://github.com/PuerkitoBio/goquery/releases", :type1},
      {"https://github.com/revel/revel/releases", :type2},
      {"https://github.com/lhorie/mithril.js/releases", :type1},
      {"https://github.com/riot/riot/releases", :type1},
      {"https://github.com/atom/atom/releases", :type2},
      {"https://github.com/Microsoft/TypeScript/releases", :type2},
      {"https://github.com/docker/docker/releases", :type2},
      {"https://github.com/JuliaLang/julia/releases", :type2},
      {"https://github.com/nim-lang/Nim/releases", :type1},
      {"https://github.com/elixir-lang/elixir/releases", :type2},
      {"https://github.com/philss/floki/releases", :type1},
      {"https://github.com/takscape/elixir-array/releases", :type2},
    ]

記述していたんですね、今まで。

でもこれだと予めどっちのタイプか判断して書いておかないといけないのでイマイチです。どっちのタイプかその場で判断する、という関数を考えるという手もあるのでしょうが…。

これまでのプログラムだとタイプが合っていないと

  def fetch_content(param) do
    {{url, type}, i} = param
    #IO.puts "URL = #{url}"
    #IO.puts "i = #{i}"
    ret = HTTPoison.get!( url )

    %HTTPoison.Response{status_code: 200, body: body} = ret

    {_,_,n} = Floki.find(body, ".container strong a") |> List.first
    {_, d} = Floki.find(body, "time") |> Floki.attribute("datetime")
                                      |> List.first
                                      |> Timex.DateFormat.parse("{ISOz}")
    if(type == :type1) do
      {_,[{_,_}],x} = Floki.find(body, ".tag-name") |> List.first
    else
      {_,[{_,_}],x} = Floki.find(body, ".release-title a") |> List.first
    end
    d |> Timex.Date.Convert.to_erlang_datetime
      |> Timex.Date.from "Asia/Tokyo"
      #IO.inspect n
    {:ok, {hd(n),hd(x),d,i}}
  end

{_,[{_,_}],x} = Floki.find(body, ".tag-name") |> List.first
のあたりで

** (EXIT from #PID<0.172.0>) an exception was raised:
    ** (MatchError) no match of right hand side value: nil
        (vercheckex) lib/vercheckex.ex:29: VercheckEx.fetch_content/1

になってMatchErrorで失敗していました。

変えたところ

このパターンマッチしてるところで失敗したら(少なくとも今のところ2タイプしかないので)もう片方を試せばいいのですね。

そこで、tryrescue による例外処理を入れて以下のようにしてみました。

まず、上記のパターンマッチを2つのプライベート関数に分けます。

  defp parse_title(body, :type1) do
    try do
      {_,[{_,_}],x} = Floki.find(body, ".tag-name") |> List.first
      {:ok, x}
    rescue
      what -> {:error, what}
    end
  end

  defp parse_title(body, :type2) do
    try do
      {_,[{_,_}],x} = Floki.find(body, ".release-title a") |> List.first
      {:ok, x}
    rescue
      what -> {:error, what}
    end
  end

関数parse_title自体も引数パターンマッチで:type1, :type2の処理を振り分けるようにしました。

Elixirの例外処理はエラーを拾う場合は

try do
  {:ok, 結果の値} # エラーが起きない場合の式(戻り値)
rescue
  e -> {:error, e} # エラーが起きた時の式(戻り値)
end

のような形を取ります。呼び出す側で

    # まずは:type1として呼び出す
    {result,x} = parse_title(body, :type1)
    # Match Errorが起きたらもう一方のタイプでやり直す
    if result == :error do
      {_,x} = parse_title(body, :type2)
      x
    end

としておけば例外はちゃんとハンドルされてダメな場合はもう片方のタイプとしてパースし直します。

ソースはgithubにあります。

まとめ

だいぶすっきりしてきました。


  1. "Let it crash"のコンセプトからすると、例外で落っこちたら再度プロセスを立ち上げる、方がElixirらしいのかもしれませんが… 

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