エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

Hound を使った Phoenix app の integration test

For browser automation and writing integration tests in Elixir.

Hound を使って Phoenix app の integration test を書きました。 ここで書いたコードは GitHub に push してあります。

mattsan/phoenix_integration_test_sample

プロジェクトを用意

Phoenix app のテストなので、Phoenix プロジェクトを用意します。 プロジェクト名はご自由に。

$ mix phx.new sample
$ cd sample

パッケージを追加する

Hound パッケージを追加します。 mix.exs を編集して、hound の行を追加します。テストでのみ利用するので only: :test のオプションを指定しておきます。

       {:phoenix_html, "~> 2.10"},
       {:phoenix_live_reload, "~> 1.0", only: :dev},
       {:gettext, "~> 0.11"},
-      {:cowboy, "~> 1.0"}
+      {:cowboy, "~> 1.0"},
+      {:hound, "~> 1.0", only: :test}
     ]
   end
 end

編集できたらHound パッケージを取得(ダウンロード)します。

$ mix deps.get

設定する

config/test.exs を編集して Hound の設定を追加します。

テスト時にもサーバが起動するようにします。

config :sample, SampleWeb.Endpoint,
   http: [port: 4001],
-  server: false
+  server: true

ドライバを指定します。ここでは PhantomJS を利用する設定をしています。そのほかのドライバを指定するばあいはこちらを参照してください。

+config :hound, driver: "phantomjs"

test/test_helper.exs を編集してテスト開始前に Hound が起動するようにします。

+Application.ensure_all_started(:hound)
 ExUnit.start()

テストを書く

テストを書きます。今回は test/sample_web/ の下に intagration/ を作り、そこにテストファイルをおくようにしました。

例としてホームページのテスト test/sample_web/intagration/page_index_test.exs を書きます。

  • use Hound.Helpers を追加します
  • hound_session() を追加します。この関数を記述しておくとセッションの管理を自動的に行ってくれます
  • navigate_to/2 でページをリクエストします。詳しくはドキュメントを参照してください
  • find_element/3 で要素を取得します。詳しくはドキュメントを参照してください
  • inner_text/1 で要素のテキストを取得します。詳しくはドキュメント(ry
defmodule SampleWeb.PageIndexTest do
  use SampleWeb.ConnCase
  use Hound.Helpers

  hound_session()

  test "home", %{conn: conn} do
    navigate_to(page_path(conn, :index))

    h2 =
      find_element(:tag, "h2")
      |> inner_text()

    assert h2 == "Welcome to Phoenix!"
  end
end

テストを実行する…前に PhantomJS を起動しておく

PhantomJS を利用するので先に起動しておきます。このばあい、グローバルで実行できる状態でインストールされている必要があります(すぐ後でプロジェクトに含めておく方法を書きます)。 Remote WebDriver mode で利用するのでオプションで指定します。

$ phantomjs --wd

mix test でテストを実行します。今回書いたテストだけを実行するばあいはファイル名を指定して実行します。

$ mix test test/sample_web/intagration/page_index_test.exs 

PhantomJS をパッケージに追加する

PhantomJS はアプリケーションとは別に用意してもよいのですが、プロジェクトの中に閉じておきたいので assets に PhantomJS を追加してそれを利用するようにします。

assets/package.json に PhantomJS を追加します。合わせてスクリプトを定義しておいて簡単に実行できるようにしておきます。

 {
   "repository": {},
   "license": "MIT",
   "scripts": {
     "deploy": "brunch build --production",
-    "watch": "brunch watch --stdin"
+    "watch": "brunch watch --stdin",
+    "phantomjs": "./node_modules/phantomjs-prebuilt/bin/phantomjs --wd"
   },
   "dependencies": {
     "phoenix": "file:../deps/phoenix",
     "phoenix_html": "file:../deps/phoenix_html"
   },
   "devDependencies": {
     "babel-brunch": "6.1.1",
     "brunch": "2.10.9",
     "clean-css-brunch": "2.10.0",
+    "phantomjs-prebuilt": "^2.1.16",
     "uglify-js-brunch": "2.10.0"
   }
 }

インストールします。

$ cd assets
$ npm install

実行するには assets/ に移動して npm run で起動します。

$ npm run phantomjs

PhantomJS をテスト実行時に自動的に起動する、そして終了時に停止する

PhantomJS を別途起動する必要がないように、テストヘルパに PhantomJS の起動と停止を書きます。 これで PhantomJS の起動停止を気にする必要がなくなりますが、

  • テストのたびに起動のオーバーヘッドが発生する
  • 複数のテストを同時には実行できない(それぞれが同じポートを利用しようとするので)

などのデメリットもあります。

test/test_helper.exs を次のように編集します。

TestHelper. setup_phantomjs/0 で PhantomJS を起動します。そのなかでテスト終了時に kill コマンドを実行して PhantomJS のプロセスを停止するコードを System.at_exit/1 にかいておきます。 TestHelper.wait_starting/0 は、PhantomJS の起動が完了したときに標準出力に出力する文字列を待つ関数です。

テスト開始前に PhantomJS が起動するように TestHelper. setup_phantomjs/0 呼び出しを追加します。

defmodule TestHelper do
  @phantomjs_path "./assets/node_modules/phantomjs-prebuilt/bin/phantomjs --wd"

  def setup_phantomjs do
    port = Port.open({:spawn, @phantomjs_path}, [:binary])
    {:os_pid, os_pid} = Port.info(port, :os_pid)

    System.at_exit(fn _ ->
      "kill #{os_pid}"
      |> String.to_charlist()
      |> :os.cmd()
    end)

    wait_starting()
  end

  def wait_starting do
    receive do
      {_port, {:data, output}} ->
        if !String.match?(output, ~r/running on port 8910/) do
          IO.puts "Starting PhatomJS failed: #{inspect output}"
          System.halt(1)
        end

      after 1_000 ->
        wait_starting()
    end
  end
end

TestHelper.setup_phantomjs()
Application.ensure_all_started(:hound)
ExUnit.start()

いつか読むはずっと読まない:もののけのみやつこ

初版が発売されたとき、友人と散々プレイしたカードゲーム。

今年は初版発売から 30 年だそうです。

MONSTER MAKER モンスターメーカー

MONSTER MAKER モンスターメーカー

Mix で管理する Elixir プロジェクトでコンパイル対象のパスを追加する

Phoenix のプロジェクトの mix.exs を見れば一目瞭然なのですが、project/0 が返すキーワードリストに、:elixirc_paths をキーにコンパイル対象のパスのリストを値にして追加すると、それらのパスがコンパイル対象になります。

# mix.exs

  def project do
    [
      app: :sample,
      version: "0.1.0",
      elixir: "~> 1.6",
      elixirc_paths: ["lib", "test/support"], # `lib` と `test/support` をコンパイル対象にする
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

Phoenix プロジェクトと同じように書くと、ビルド環境によって対象を切り替えることができます。

# mix.exs

  def project do
    [
      app: :sample,
      version: "0.1.0",
      elixir: "~> 1.6",
      elixirc_paths: elixirc_paths(Mix.env()),
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end


  def elixirc_paths(:test), do: ["lib", "test/support"] # test 環境の時は `test/support` をコンパイル対象に含める
  def elixirc_paths(_), do: ["lib"]

ドキュメントの中にはなぜか記述が見つからない。

コードを追いかけてみるとデフォルトでは ["lib"] になっているのがわかります。

Phoenix で Bootstrap の JavaScript を使う

最近、個人的には Elixir ばかり書いています。特に現在は Ruby on Rails TutorialPhoenix でなぞるということをやっています。

とはいえ。やはり勝手が違うので思わぬところでつまずいたり。

例えば Phoenixframework では標準で Bootstrap のスタイルを利用することができますが JavaScript はインストールされていないので、JavaScript を必要とする動きのあるコンポーネントを利用することができません。

調査と試行錯誤の結果、それを使えるようになったので、その手順の記録です。

最小限の手順を記録して置きたいので、プロジェクトの作成から順に説明していきます。

プロジェクトの用意

新しい Phoenix プロジェクトを作成します。sample というプロジェクトを作成しその作業ディレクトリに移動します。

$ mix phx.new sample --no-ecto # 今回は DB を使わないので Ecto を外しました
$ cd sample

Bootstrap コンポーネントを使ったページの用意

Bootstrap のコンポーネントを使ったページを用意します。

例えば lib/sample_web/templates/page/index.html.eex を編集して、内容を Bootstrap のモーダルのサンプルコードで置き換えます。

Phoenix Server を起動します。

$ mix phx.server

ブラウザで http://localhost:4000 開くとボタンが一つ表示されます。

f:id:E_Mattsan:20180327090811p:plain

しかし Bootstrap の JavaScript が機能していないので、ボタンを押してもモーダルは表示されません。

Bootstrap のインストール

Bootstrap をインストールします。

プロジェクトのディレクトリの下の assets ディレクトリに移動して npm で Bootstrap をインストールします。現在利用している Phoenixframewok 1.3.2 では Bootstrap3 を利用しているので、バージョンに 3 を指定します。 また Bootstrap が jquery を必要とするので合わせてインストールします。

$ cd assets
$ npm install bootstrap@3
$ npm install jquery

インストールできたら元のディレクトリに移動します。

Bootstrap の設定

インストールしたパッケージを読み込めるようにします。

assets/js/app.js を開いて次の 2 行を追加します。

global.jQuery = require("jquery")
require("bootstrap")

Bootstrap がグローバルな jQuery を参照しているらしく、このようにする必要がありました。もしかするともう少しよい書き方があるのかもしれません。

以上で設定は完了です。 ボタンを押すと今度は無事モーダルが表示されます。

f:id:E_Mattsan:20180327092159p:plain

明日はもう通用しないかも

ちなみに。現在の Phoenix の変化は早いので、明日はもう通用しないかもしれません。 標準で利用できる Bootstrap もひと世代前のものなので近いうちの更新が予想されます。

次の 1.4.0 では brunch を廃して webpack を利用することになりそうですので、その時は Bootstrap 自体が利用されなくなっているかもしれません。

Mock パッケージを使った Elixir のテスト

Mock を使ったテストを覚えたので、そのメモです。

全容は GitHub に置いてあります。

リポジトリ名は test_with_mocks_ex ですが、中の Elixir のプロジェクト名 は fizz_buzz になってます。気をつけて。

プロジェクトで定義している唯一の関数 FizzBuzz.fizz_buzz/1 は、数値を与えると FizzBuzz します。 実態はコマンド fb を呼び出しています。fb は各自実装してください。

…というわけにもいかないので、fb コマンドの呼び出しをモックします。

パッケージの追加

"Mock" というパッケージを利用します。

mix.exs に依存関係を追加します。

  defp deps do
    [
      {:mock, "~>0.3"}
    ]
  end

パッケージの取得。

$ mix deps.get

テスト対象

テスト対象の関数はこのようになっています。

関数 FizzBuzz.fizz_buzz/1System.cmd/2 を使ってコマンド fb を実行し、結果を返します。

defmodule FizzBuzz do
  @moduledoc """
  Documentation for FizzBuzz.
  """

  @doc """
  外部コマンド `fb` を利用して FizzBuzz する。

  `fb` コマンドは各自実装してください。
  """
  def fizz_buzz(n) do
    {res, _} = System.cmd("fb", ["#{n}"])
    res
  end
end

テストを書く

モジュールの import

タイプする手間を減らすために、テスト対象のモジュール FizzBuzz と、モックを提供するモジュール Mock を import しておきます。

defmodule FizzBuzzTest do
  use ExUnit.Case

  import Mock
  import FizzBuzz

  ...
end

モックで囲って assert する

マクロ Mock.with_mock/4 はモックするモジュール、モックする関数、およびブロックを取り、モックする関数がブロック内から呼び出されたときに本来の関数に代わってマクロに与えた関数が呼び出されます。

  test "fizz_buzz 1" do
    with_mock System, cmd: fn "fb", ["1"] -> {"1", 0} end do
      assert fizz_buzz(1) == "1"
    end
  end
# 引数
1 モジュール System
2 オプション (ここでは省略)
3 モック [{:cmd, fn "fb", ["1"] -> {"1", 0} end}]
4 ブロック do
assert fizz_buzz(1) == "1"
end

モックはキーワードリストになっているため、[cmd: fn "fb", ["1"] -> {"1", 0} end] と書くことができます。さらに引数では括弧 [] を省略できるため、上記のようは記述になっています。

FizzBuzz.fizz_buzz(1) は内部で System.cmd("fb", ["1"]) という形で呼び出しているので、モックする関数でその形に合わせた引数と戻り値を用意しています。

複数のモックで囲って assert する

マクロ Mock.with_mocks/2 は、モックをリストにして複数定義できます。

なのですが。ここではモックできる関数を用意できていないので、リストの要素は一つになっています。

  test "fizz_buzz 3" do
    with_mocks [{System, [], [cmd: fn "fb", ["3"] -> {"Fizz", 0} end]}] do
      assert fizz_buzz(3) == "Fizz"
    end
  end

Mock.with_mocks/2 に与えているモックのリストの要素 {System, [], [cmd: fn "fb", ["3"] -> {"Fizz", 0} end]}Mock.with_mock/4 と同じ構成になっています。 ただ、引数でないので省略した記述ができないのでオプションの空リストやモックの括弧も記述しています。

setup でモックする

モックを setup で定義します。 Mock.setup_with_mocks/2 のモックのリストの書式は Mock.with_mocks/2 と同じです。

ブロックの戻り値の扱いは ExUnit.Callbacks.setup/1 と同じです。ですので特に返す値がない場合は :ok を返します。

コンテクスト変数を受け取れる ExUnit.Callbacks.setup/1 に対応する Mock.setup_with_mocks/3 もあります。

  describe "case 5" do
    setup_with_mocks [{System, [], [cmd: fn "fb", ["5"] -> {"Buzz", 0} end]}] do
      :ok
    end

    test "fizz_buzz 5" do
      assert fizz_buzz(5) == "Buzz"
    end
  end

test でモックする

テスト定義でモックを記述します。 モックの記述の書式は Mock.with_mock/4 と同じです。

こちらもコンテクスト変数を受け取れる Mock.test_with_mock/6 が用意されています。

  test_with_mock "fizz_buzz 9", System, cmd: fn "fb", ["9"] -> {"Fizz", 0} end do
    assert fizz_buzz(9) == "Fizz"
  end

呼び出されたことを確かめる

マクロ Mock.call/1 は、引数に与えたモックされた関数の呼び出しがあったばあいに真を返します。

  test "fizz_buzz 15" do
    mock = fn "fb", ["15"] -> {"FizzBuzz", 0} end

    with_mock System, cmd: mock do
      assert fizz_buzz(15) == "FizzBuzz"
      assert called(System.cmd("fb", ["15"]))
    end
  end

いつか読むはずっと読まない:三部作、再び

いままで読んできたシリーズものでも邦訳が完結してないのが多いんだよな…。「恐竜惑星(惑星アイリータ調査隊)」とか「ダーコーヴァ年代記」とか「ワイルド・カード」とか…。

「〈クロノス・クロニクル〉第一弾」?

…またシリーズものに手を出してしまった。

2011/01/26 のブログの記事より)

ボヘミアの不思議キャビネット (創元推理文庫)

ボヘミアの不思議キャビネット (創元推理文庫)

天球儀とイングランドの魔法使い (創元推理文庫)

天球儀とイングランドの魔法使い (創元推理文庫)

それから何年経っただろうか。〈クロノス・クロニクル〉第 3 巻はいまだ刊行されていない。

そして。

本書はパテルの小説の処女作であるが、二〇一五年三月に出版されたのち同年七月には続巻の Cities and Thrones が出版され、今年(二〇一七年)には本三部作の最終巻となる The Song of the Dead が出版された。

(本書のあとがきより)

…またシリーズものに手を出してしまった。

墓標都市 (創元SF文庫)

墓標都市 (創元SF文庫)

Elixir で Gettext を使う

由緒正し I18n の仕組みということを、 Phoenix を触るようになって初めて知りました。

Phoenix ではプロジェクトを作成すると自動的にパッケージが追加されますが、通常の Elixir プロジェクトで利用する手順を調べました。

くわしくは公式ドキュメントに全部書いてあります。

プロジェクトを用意する

$ mix new my_app
$ cd my_app

パッケージを追加する

バージョンはそのときどきの適切な(たいていは最新の)バージョンを指定してください。

  defp deps do
    [
      {:gettext, "~> 0.15.0"}
    ]
  end

パッケージの取得とコンパイル

$ mix do deps.get, deps.compile

実行すると gettext 関連のタスクが追加されます。

$ mix help
...
mix gettext.extract   # Extracts translations from source code
mix gettext.merge     # Merge template files into translation files
...

プロジェクトの Gettext モジュールを作る

こんな感じで。

# lib/my_app/gettext.ex
defmodule MyApp.Gettext do
  use Gettext, opt_app: :my_app
end

Gettext を利用したコードを書く

たとえばこんな感じで。

# lib/my_app.ex
defmodule MyApp do
  import MyApp.Gettext

  def say do
    IO.puts gettext("Hello, %{name}!", name: "世界")
    IO.puts gettext("Hi.")
    IO.puts gettext("Bye.")
  end
end

実行するとこんな感じ。まだなにもしていないので gettext に与えられた文字列がそのまま出力されています。

$ mix run -e 'MyApp.say'
Hello, 世界!
Hi.
Bye.

文字列を抽出する

mix gettext.extract を実行して文字列を抽出します。 priv/gettext/default.pot というファイルが作成され、抽出した文字列が格納されます。

$ mix gettext.extract
Compiling 2 files (.ex)
Extracted priv/gettext/default.pot

ロケールに対応した .po ファイルを作成する

$ mix gettext.merge priv/gettext --locale=ja
Created directory priv/gettext/ja/LC_MESSAGES
Wrote priv/gettext/ja/LC_MESSAGES/default.po

priv/gettext の下に指定したロケールディレクトリが作成され、その中に default.po というファイルが作成されます。

msgidgettext に与えた文字列で、文字列を置き換える時の ID になります。 msgstr に置き換える文字列を記述します。このときプレイスホルダ(ここでは %{name} の部分)は置き換え後も機能するようにそのまま残します。

たとえばこんな感じ。

## `msgid`s in this file come from POT (.pot) files.
##
## Do not add, change, or remove `msgid`s manually here as
## they're tied to the ones in the corresponding POT file
## (with the same domain).
##
## Use `mix gettext.extract --merge` or `mix gettext.merge`
## to merge POT files into PO files.
msgid ""
msgstr ""
"Language: ja\n"
"Plural-Forms: nplurals=1\n"

#, elixir-format
#: lib/my_app.ex:7
msgid "Bye."
msgstr "ぢゃ、そんな感じで。"

#, elixir-format
#: lib/my_app.ex:5
msgid "Hello, %{name}!"
msgstr "こんにちは、%{name}!"

#, elixir-format
#: lib/my_app.ex:6
msgid "Hi."
msgstr "やはぁ。"

デフォルトのロケールを指定する

config/config.exs にデフォルトロケールの指定を追加します。

# config/config.exs
config :my_app, MyApp.Gettext,
  default_locale: "ja"

コンパイルして実行

$ mix compile --force
Compiling 2 files (.ex)
Generated my_app app
$ mix run -e 'MyApp.say'
こんにちは、世界!
やはぁ。
ぢゃ、そんな感じで。

文字列を追加する

Gettext を使う文字列を追加してみます。

# lib/my_app.ex
defmodule MyApp do
  import MyApp.Gettext

  def say do
    IO.puts gettext("Hello, %{name}!", name: "世界")
    IO.puts gettext("Hi.")
    IO.puts gettext("Leave it to me!")
    IO.puts gettext("Bye.")
  end
end

抽出します。

$ mix gettext.extract
Compiling 2 files (.ex)
Extracted priv/gettext/default.pot

priv/gettext/default.pot に追加した文字列が追加されます。

ロケール.po ファイルにマージします。

$ mix gettext.merge priv/gettext --locale=ja
Wrote priv/gettext/ja/LC_MESSAGES/default.po

priv/gettext/ja/LC_MESSAGES/default.po を編集します。

#, elixir-format
#: lib/my_app.ex:7
msgid "Leave it to me!"
msgstr "むぁ〜かせて!"

コンパイルして実行します。

$ mix compile --force
Compiling 2 files (.ex)
Generated my_app app
$ run -e 'MyApp.say'e
こんにちは、世界!
やはぁ。
むぁ〜かせて!
ぢゃ、そんな感じで。

動的にロケールを変更する

Gettext.put_locale/1ロケールの文字列を指定すると動的にロケールを変更できます。

$ mix run -e 'Gettext.put_locale("en"); MyApp.say'
Hello, 世界!
Hi.
Leave it to me!
Bye.

もしくは。 MyApp.Gettextロケールのみを変更したいばあいは、Gettext.put_locale/2 を使ってモジュールを指定します。

$ mix run -e 'Gettext.put_locale(MyApp.Gettext, "en"); MyApp.say'
Hello, 世界!
Hi.
Leave it to me!
Bye.
$ mix run -e 'Gettext.put_locale(MyApp.Gettext, "en"); MyApp.say; Gettext.put_locale(MyApp.Gettext, "ja"); MyApp.say'
Hello, 世界!
Hi.
Leave it to me!
Bye.
こんにちは、世界!
やはぁ。
むぁ〜かせて!
ぢゃ、そんな感じで。

Phoenix で使う

Phoenix プロジェクトを作成すると、パッケージが追加された状態で priv/gettext も用意された状態になっています。

$ mix phx.new my_phx
my_phx/
└── priv/
     └── gettext/
          ├── en/
          │   └── LC_MESSAGES/
          │       └── errors.po
          └── errors.pot

ここで mix gettext.extract を実行すると、lib/my_phx_web/templates/page/index.html.eex"Welcome to %{name}!"priv/gettext/default.pot に抽出されます。

$ mix gettext.extract
Compiling 12 files (.ex)
Generated my_phx app
Extracted priv/gettext/errors.pot
Extracted priv/gettext/default.pot

同様に mix gettext.merge を実行します。

$ mix gettext.merge priv/gettext --locale=ja
Created directory priv/gettext/ja/LC_MESSAGES
Wrote priv/gettext/ja/LC_MESSAGES/errors.po
Wrote priv/gettext/ja/LC_MESSAGES/default.po

生成された priv/gettext/ja/LC_MESSAGES/default.po を編集します。

#, elixir-format
#: lib/my_phx_web/templates/page/index.html.eex:2
msgid "Welcome to %{name}!"
msgstr "%{name}へようこそ!"

config/config.exs を編集してデフォルトロケールの指定を追加します。

config :my_phx, MyPhxWeb.Gettext,
  default_locale: "ja"

サーバを起動します。

$ mix phx.server

ブラウザで http://localhost:4000 にアクセスします。

f:id:E_Mattsan:20180227213619p:plain

ぢゃ、そんな感じで。

いつか読むはずっと読まない:なぜなら、猫だから

本書の帯より。

この本に登場する猫たち

  • マシュマロを焼く天才猫
  • マスコットとして宇宙船に乗った猫
  • 銀河文明を変えた猫
  • 生き返ってから年を取らなくなった猫
  • テレパスになった猫

整数値でも浮動小数点数値でもパースしたい

Elixir で文字列から数値に変換するときに String.to_integer/1String.to_float/1 を使いますが、前者は浮動小数点数値の文字列を与えると、後者は整数値の文字列を与えると例外を投げてしまいます。

iex(1)> String.to_integer("123")  
123
iex(2)> String.to_integer("123.4")
** (ArgumentError) argument error
    :erlang.binary_to_integer("123.4")
iex(2)> String.to_float("123.4")
123.4
iex(3)> String.to_float("123")  
** (ArgumentError) argument error
    :erlang.binary_to_float("123")

例外を投げない関数として、Erlangstring モジュールにそれぞれ同名の関数があります。こちらはパースに成功すると、成功した部分の数値とパースできなかった部分を残りの文字列のタプルを返します。 パースできる文字列がなかった場合には :error とその理由のタプルを返します。

iex(1)> :string.to_integer("123+987")
{123, "+987"}
iex(2)> :string.to_float("123.4+987.6")
{123.4, "+987.6"}
iex(3)> :string.to_integer("abc")      
{:error, :no_integer}
iex(4)> :string.to_float("abc")  
{:error, :no_float}

ここまではいいのですが。任意の数値の文字列を返す関数がなくて困りました。string:to_integer/1浮動小数点数値の文字列を与えると小数点の手前までしかパースしませんし、string:to_float/1 に整数値の文字列を与えるとエラーになります。

iex(1)> :string.to_integer("123.4")
{123, ".4"}
iex(2)> :string.to_float("123")    
{:error, :no_float}

対策としては。最初に string:to_float/1 でパースして失敗したら string:to_integer/1 でパースするという手があります。

defmodule MyParser do
  def to_number(source) do
    case :string.to_float(source) do
      {:error, _} -> :string.to_integer(source)
      result -> result
    end
  end
end

これでだいたいうまくいきます。

iex(1)> MyParser.to_number("123")         
{123, ""}
iex(2)> MyParser.to_number("123.4")
{123.4, ""}
iex(3)> MyParser.to_number("123+987")
{123, "+987"}
iex(4)> MyParser.to_number("123.4+987.6")
{123.4, "+987.6"}
iex(5) MyParser.to_number("abc")
{:error, :no_integer}

ほぼ問題ないのですが、エラーのときに {:error, :no_integer} と返ってくるのでこれを整えたい。

case をもう一段重ねればそれなりに動きます。

defmodule MyParser do
  def to_number(source) do
    case :string.to_float(source) do
      {:error, _} ->
        case :string.to_integer(source) do
          {:error, _} -> {:error, :no_number}
          result -> result
        end
      result -> result
    end
  end
end
iex(1)> MyParser.to_number("abc")
{:error, :no_number}

もうちょっとどうにかならないかとライブラリを読んでいたら。Enum.reduce_while/3 という関数を見つけました。

名前の通り条件が満たされるまで第 3 引数で与える関数を適用し、満たされたら適用を中断するというものです。

第 3 引数で与える関数は 2 要素のタプルを返す必要があります。タプルの第 1 要素が :cont だったばあいは処理を継続しタプルの第 2 要素が次の処理に引き継がれます。タプルの第 1 要素が :halt だったばあいは処理を中断しタプルの第 2 要素が Enum.reduce_while/3 の戻り値になります。

それらをふまえて。

defmodule MyParser do
  def to_number(source) do
    [&:string.to_float/1, &:string.to_integer/1]
    |> Enum.reduce_while(nil, fn f, _ ->
      case f.(source) do
        {:error, _} -> {:cont, {:error, :no_number}}
        result -> {:halt, result}
      end
    end)
  end
end

第 2 引数の値は利用していないので nil を与え、適用する関数の第 2 引数も _ にしています。

実行。

iex(1)> MyParser.to_number("123")        
{123, ""}
iex(2)> MyParser.to_number("123.4")      
{123.4, ""}
iex(3)> MyParser.to_number("123+987")    
{123, "+987"}
iex(4)> MyParser.to_number("123.4+987.6")
{123.4, "+987.6"}
iex(5)> MyParser.to_number("abc")        
{:error, :no_number}

今回は適用する関数がふたつだったので Enum.reduce_while/3 を利用する利点があまりありませんでしたが、適用する関数の数がもっと多くなったばあいには重宝しそうです。

いつか読むはずっと読まない:二十世紀前半のイギリスの赤毛の女性の物語

図らずしも。偶然に続けてよんだ小説が、どちらも二十世紀前半の(とはいえ一方は初頭、一方は第二次大戦時)のイギリスを舞台にした赤毛の女性が主人公の物語。

こちらは既刊が 6 巻。

チャーチル閣下の秘書 (創元推理文庫)

チャーチル閣下の秘書 (創元推理文庫)

こちらは全 3 巻。第 3 巻はもうじき刊行予定。

紙の魔術師 (ハヤカワ文庫FT)

紙の魔術師 (ハヤカワ文庫FT)

標準入力、標準出力、標準エラーをコードでリダイレクトする

cstdio と iostream が混ざっているのが少々座りが悪いのですが。iostream からファイルディスクリプタを取得できればよいのですが仕様上は仕組みが用意されていないようなので、cstdio を利用しています。

#ifndef REDIRECT_H__
#define REDIRECT_H__

#include <unistd.h>

template<int FD>
class RedirectT {
public:
    RedirectT(int fd) : dup_fd_(dup(FD)) {
        dup2(fd, FD);
    }

    ~RedirectT() {
        sync();
        dup2(dup_fd_, FD);
        close(dup_fd_);
    }

private:
    int dup_fd_;
};

typedef RedirectT<0> RedirectStdin;
typedef RedirectT<1> RedirectStdout;
typedef RedirectT<2> RedirectStderr;

#endif//REDIRECT_H__

使用例。

#include <iostream>
#include <cstdio>

#include "redirect.h"

int main(int, char* []) {
    {
        FILE* file = std::fopen("temp.txt", "w");
        RedirectStdout to(fileno(file));
        std::cout << "This message is redirected to file." << std::endl;
        fclose(file);
    }

    {
        FILE* file = std::fopen("temp.txt", "r");
        RedirectStdin from(fileno(file));
        std::cout << std::cin.rdbuf() << std::endl;
        fclose(file);
    }

    std::cout << "This message is not redirected to file." << std::endl;

    return 0;
}

コンパイルして実行。

$ g++ -o redirect_sample redirect_sample.cpp 
$ ./redirect_sample 
This message is redirected to file.

This message is not redirected to file.
$ cat temp.txt 
This message is redirected to file.

標準出力への文字列の出力が temp.txt への書き込みになり、標準入力からの文字列の入力が text.txt からの読み出しになっています。