ようへいの日々精進XP

よかろうもん

ゴールデンウィークスペシャル : 奥さんが髪を切っている間に「ExcelできればElixirマスターできる」を ExUnit で写経する (1) #fukuokaex

tl;dr

  先日の Fukuoka.ex #8 で @piacere さんのお話で紹介されていた資料.

speakerdeck.com

インパクトのあるタイトルだったが, 確かに Excel での結果を横に表示しながら, Enum の各関数の実行結果が紹介されていて, とても分かりやすい発表 (資料) だったのでこの資料を元にして, Enum の代表格である sort/1filter/2 及び map/2 を触ってみたい. 尚, 今回, 手元の環境に Excel が導入されていないので, ExUnit でテストを書きながら写経を進めたいと思う.

Elixir の導入

資料では, ソースコードから Erlang と Elixir を導入する手順や, 各 OS のパッケージ管理システムからインストールする方法が紹介されていたが, 今回は Docker のコンテナイメージを利用することにした.

$ docker pull elixir
$ mkdir elixir-excel
$ cd elixir-excel
$ docker run --name=elixir-excel -t -i -v $(pwd):/elixir-excel elixir:latest /bin/bash

コンテナにアクセスして Elixir のバージョンを確認する.

root@77bd00ecbf3f:/# 
root@77bd00ecbf3f:/# elixir --version
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]

Elixir 1.6.4 (compiled with OTP 20)

ということで, Elixir のバージョンは 1.6.4 で Erlang/OTP 20 で写経を進める.

写経 (Excel の操作と同じ Elixir)

事前に

資料では, iex で直接 Elixir の関数を実行していたが, 本記事では ExUnit を利用する為, mix new で Elixir プロジェクトを作成する.

mix new elixir_excel_handson

以下のように出力される.

# mix new elixir_excel_handson
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/elixir_excel_handson.ex
* creating test
* creating test/test_helper.exs
* creating test/elixir_excel_handson_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd elixir_excel_handson
    mix test

Run "mix help" for more commands.

Enum.sort()

Enum.sort() は, その名の通り, Enumerable (辞書を引くと「数えられる, 数え上げられる」とある) 対象をソート (並び替える) する. Excel だと, まさに「並べ替え」が該当する.

早速, ExUnit のテストケースを書いてみる.

defmodule ElixirExcelHandsonTest do
  use ExUnit.Case

  test "Enum.sort() を試す" do
    assert ElixirExcelHandson.sort_num([10, 8, 13]) == [8, 10, 13]
  end
end

このテストケースでは, 関数 sort_num において, 10, 13, 8 という数字の並びのリストを引数として与えた場合に, Enum.sort() によって 8, 10, 13 という昇順に並び替えられて結果を返すことを期待している.

このテストケースが正しく通るように sort_num 関数を書くと以下のようなコードとなるはず.

defmodule ElixirExcelHandson do
  def sort_num(list) do
    Enum.sort(list)
  end
end

テストしてみる. ExUnit によるテストは mix test 叩くだけ. 簡単.

root@df37b126e238:/elixir-excel/elixir_excel_handson# mix test --trace

ElixirExcelHandsonTest
  * test Enum.sort() を試す (7.0ms)


Finished in 0.08 seconds
1 test, 0 failures

Randomized with seed 739203

イイ感じ.

ちなみに, 資料に記載されている通りにコードを書き換えて試してみる.

defmodule ElixirExcelHandson do
  def sort_num(list) do
    list |> Enum.sort()
  end
end

パイプライン演算子を利用して,「データ (変数: list) -> 並べ替え」という Excel と同じような雰囲気で書くことが出来る気がする.

テストを書いているので, 先程と同じ結果になることを確認することが出来る.

root@df37b126e238:/elixir-excel/elixir_excel_handson# mix test --trace

ElixirExcelHandsonTest
  * test Enum.sort() を試す (9.2ms)


Finished in 0.07 seconds
1 test, 0 failures

Randomized with seed 589493

LGTM, LGTM.

Enum.filter()

引続き, Enum.filter(). これも Excel のフィルタ同様に, データを一定の条件でフィルタして, そのフィルタされた結果を返す.

例の如く, テストケースから書いていく.

defmodule ElixirExcelHandsonTest do
  use ExUnit.Case

  test "Enum.filter() を試す" do
    assert ElixirExcelHandson.filter_num([10, 8, 13]) == [10, 13]
  end
end

このテストケースでは, 関数 filter_num において, 10, 13, 8 という数字の並びのリストを引数として与えた場合に, Enum.filter() で定義された条件 (今回は 8 以外を出力) にてフィルタリングされ, 10, 13 という結果をリストで返すことを期待している.

このテストケースが正しく通るように filter_num 関数を書くと以下のようなコードとなるはず.

defmodule ElixirExcelHandson do
  def filter_num(list) do
    list |> Enum.filter(fn(n) -> n != 8 end)
  end
end

また, このコードは Enum.sort() の時と同様に, 以下のように書くことも出来る.

defmodule ElixirExcelHandson do
  def filter_num(list) do
    Enum.filter(list, fn(n) -> n != 8 end)
  end
end

fn(n) -> n != 8 end では, 引数 n が 8 以外の数値 (10, 13) であることをチェックしている. ここでの fn(n) は無名関数で, 変数 list 内のデータを n に代入して -> n !=8 end に渡すことを意味している.

では, テストを実行してみる.

root@df37b126e238:/elixir-excel/elixir_excel_handson# mix test --trace

ElixirExcelHandsonTest
  * test Enum.filter() を試す (4.5ms)
  * test Enum.sort() を試す (0.00ms)


Finished in 0.04 seconds
2 tests, 0 failures

Randomized with seed 296252

イイ感じ.

fn(n) -> n != 8 end の書き方がちょっと特殊な感じで戸惑ったりしたけど, 確かに Excel のフィルタっぽい.

Enum.map()

最後に, Enum.map() を試す. この関数はデータの変換を行う関数で, Enum.filter() 同様に無名関数を利用して, 関数に渡された引数に対する処理を書く. Excel だと, セルに数値を入れておいて, 別のセルに定義した関数から数値を参照して結果を出力するようなイメージ. これも Excel を利用する上では一般的な使い方だと思う.

いつものようにテストから.

defmodule ElixirExcelHandsonTest do
  use ExUnit.Case

  test "Enum.map() を試す" do
    assert ElixirExcelHandson.map_num([10, 8, 13]) == [20, 16, 26]
  end
end

このテストケースでは, 関数 map_num において, 10, 13, 8 という数字の並びのリストを引数として与えた場合に, Enum.map() で定義された処理 (今回は全ての値を 2 倍する) が実行され, 20, 16, 26 という結果をリストで返すことを期待している.

このテストケースが正しく通るように filter_num 関数を書くと以下のようなコードとなるはず.

defmodule ElixirExcelHandson do
  def map_num(list) do
    list |> Enum.map(fn(n) -> n * 2 end)
  end
end

また, これまでの関数同様に, 以下のように書くことも出来る.

defmodule ElixirExcelHandson do
  def map_num(list) do
    Enum.map(list, fn(n) -> n * 2 end)
  end
end

テストを実行してみる.

root@df37b126e238:/elixir-excel/elixir_excel_handson# mix test --trace
Compiling 1 file (.ex)

ElixirExcelHandsonTest
  * test Enum.map() を試す (0.01ms)
  * test Enum.sort() を試す (0.00ms)
  * test Enum.filter() を試す (0.01ms)


Finished in 0.07 seconds
3 tests, 0 failures

Randomized with seed 653686

イイ感じ.

尚, fn(n) -> n * 2 end&(&1 * 2) という風にも書ける.

defmodule ElixirExcelHandson do
  def map_num(list) do
    list |> Enum.map(&(&1 * 2))
  end
end

シンプル!だけど, なんか直感的では無い気がするので, 慣れるまでは fn(n) な書き方にしようと思った次第.

合わせ技

これまで出てきた関数の合わせ技を試してみる.

defmodule ElixirExcelHandsonTest do
  use ExUnit.Case

  test "Enum.map() と Enum.sort() を一気を試す" do
    assert ElixirExcelHandson.mix_functions_num([10, 8, 13]) == [16, 20, 26]
  end
end

このテストケースでは, 関数 mix_functions_num において, 10, 13, 8 という数字の並びのリストを引数として与えた場合に, Enum.map() で定義された処理 (今回は全ての値を 2 倍する) が実行して, さらにその結果をソートして 16, 20, 26 という結果をリストで返すことを期待している.

このテストケースが正しく通るように mix_functions_num 関数を書くと以下のようなコードとなるはず.

defmodule ElixirExcelHandson do
  def mix_functions_num(list) do
    list
    |> Enum.map(fn(n) -> n * 2 end)
    |> Enum.sort
  end
end

テストを実行してみる.

root@df37b126e238:/elixir-excel/elixir_excel_handson# mix test --trace
Compiling 1 file (.ex)

ElixirExcelHandsonTest
  * test Enum.sort() を試す (0.00ms)
  * test Enum.map() を試す (0.00ms)
  * test Enum.map() と Enum.sort() を一気を試す (0.00ms)
  * test Enum.filter() を試す (0.00ms)


Finished in 0.06 seconds
4 tests, 0 failures

Randomized with seed 21059

イイ感じ.

また, 文字列でも同様の処理を行うことは可能. ちょっと試す. 以下, テスト.

defmodule ElixirExcelHandsonTest do
  use ExUnit.Case

  test "Enum.map() と Enum.sort() を一気を試す (文字列版)" do
    assert ElixirExcelHandson.mix_functions_str(["かっぱ", "おーたに"]) == ["おーたにさーん", "かっぱさーん"]
  end
end

以下, 実装コード.

defmodule ElixirExcelHandson do
  def mix_functions_str(list) do
    list
    |> Enum.map(fn(n) -> n <> "さーん" end)
    |> Enum.sort
  end
end

ちなみに, Elixir での文字列結合 <> を利用する.

root@df37b126e238:/elixir-excel/elixir_excel_handson# iex
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:10] [hipe] [kernel-poll:false]

Interactive Elixir (1.6.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> "abc" <> "defg"
"abcdefg"
iex(2)> "おーたに" <> "さーん"
"おーたにさーん"

以下, テスト.

root@df37b126e238:/elixir-excel/elixir_excel_handson# mix test --trace

ElixirExcelHandsonTest
  * test Enum.map() を試す (6.6ms)
  * test Enum.filter() を試す (0.01ms)
  * test Enum.map() と Enum.sort() を一気を試す (0.06ms)
  * test Enum.sort() を試す (0.00ms)
  * test Enum.map() と Enum.sort() を一気を試す (文字列版) (0.06ms)


Finished in 0.07 seconds
5 tests, 0 failures

Randomized with seed 579972

おーたにさーん.

以上

技術未来とElixir「第2回:プログラミング未経験でもExcelできればElixirマスターできる」 の「入門①:Excel の操作と同じ Elixir」を写経してみた.

写経しつつ, 頭の中の Excel で操作イメージを描くことが出来たので, この資料で書かれている「ExcelできればElixirマスターできる」という言葉は, まさにその通りだと思う (100 % その通りでは無いかもしれない...ちょっと高度なことをしようとするとそれなりの学習は必要になると思う!)

ということで, そろそろ奥さんの髪切りが終わる時間なので, この続きは...第二弾にて.