LoginSignup
123
118

More than 5 years have passed since last update.

Elixirの入門の独り言

Last updated at Posted at 2015-08-29

1日1時間、業務と関係なく何か勉強しようと思った。

Elixir にした理由: lily whiteと学ぶElixir入門 というのを偶然見つけたから。そんだけ。

Elixir とは

関数型の言語のひとつ。
えりくさーと読む。

Erlang とは

Elixir は Erlang(あーらん) という言語で実装されているので、その特徴を引き継いでるのらしい。
いちいち読み方がむずい。

Erlang は、もともと通信事業者で使われていたようで、並行処理とか分散処理にすごい向いてる。
この Erlang が Facebook のチャットとか、What's up というLINE的なものにも使われてるくらい、イケてるし実績も積んでる。

その Erlang で実装されたのが Elixir。これは期待!!

・・いやいや、Erlang イケてるなら、Erlang で普通に書いたらいいじゃないか。

構文がむずかったり、ツールとかライブラリとかが少ないから流行ってないんだって。可哀想に。

その点、Elixir はイケてる。

Elixir がイケてるところ

書き方はRubyとちょっと似てる!!これは嬉しいね!
でもオレはRubyほとんどやったことないから別にどうってことない。

MixやHexっていうツールが準備されてる!
Mix は プロジェクトの buildツール. とりあえず初期化したり、依存モジュールのinstall. 起動とかまぁプロジェクト全体に対して色々できるやつ。perlでいうと carton的な?

Hex はElixirのライブラリの管理してるやつ。cpan的な?

まぁ、確かに Elixir のほうがよさそうな雰囲気はある。

もう能書きは飽きたのでinstallしてみる。

install

brew install elixir

完。

はろーわーるど

対話環境があるそうな。

$ iex
Erlang/OTP 17 [erts-6.4] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

Interactive Elixir (1.0.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>

iex って、いっつも忘れちゃう。elix とかから始まって欲しい。

じゃあはろーしてみる。

iex(1)> IO.puts("はろーわーるど")
はろーわーるど
:ok

いけた。マルチバイト余裕だった。

IO.puts が、要は print 。めんどいね。
でも System.out.println よりはましか。

FizzBuzz

FizzBuzz してみよう。

とりあえず動くもの編

fizzbazz.exs
#! /usr/bin/env elixir

defmodule FizzBuzz do
  def run(limit) do
    print(1, limit)
  end

  defp print(n, limit) when n == limit do
    cond do
      rem(n, 15) == 0 -> IO.puts "#{n}: FizzBuzz"
      rem(n, 5)  == 0 -> IO.puts "#{n}: Fizz"
      rem(n, 3)  == 0 -> IO.puts "#{n}: Buzz"
      true            -> nil
    end

  end

  defp print(n, limit) do
    print(n, n)
    print(n + 1, limit)
  end

end

input = 10

FizzBuzz.run(input);

# elixir ./fizzbazz.exs で実行
# ----
# 3: Buzz
# 5: Fizz
# 6: Buzz
# 9: Buzz
# 10: Fizz

どうしてこうなった。普通に for 文でくるくるまわすのに。。
Elixir は for 文ないんだって!ヤバイ。

じゃあどうするかっていうと、再帰(recursion) を使うのか。

↑のコードだと、まず run/1 ってのが呼ばれる。
あ、この method/N っていうのは method名と引数の数を表してて、Elixir ではこうやって記載するんだって。

run/1 から print/2 が呼ばれるんだけど、2個ある!!
でもかたっぽは条件指定してるんです。nlimitが同じときだけ呼ばれる。

今回は run/110 で呼んでるので、print/21, 10 の引数で呼ばれるので、
最初は条件にマッチせず、後者の方の print/2 に入る。そこで前者の方の print/2 を呼んで、
1 の部分をインクリして再帰する。
最後インクリされて 10 まで行ったら、今度は後者の print/2 に入らないので、前者の分だけやって終わり。
ふーー。
謎の頭をつかうわ。
ていうか前者とか後者とかその辺の呼び方がほすぃ。

その他スルーしたところ。

  • defmodule は module の宣言。
  • def と defp で関数宣言。defp は private なので外から呼べないよ。
  • cond do は雰囲気でわかると思うので省略。
  • rem n, m は perl で言うところの n % m
  • ダブルクォートの中で変数展開するときは、#{hoge} としなきゃいけない

そしてこのレベル低いサンプル書く中で既にハマっている私。。。

  • cond do は最後に必ず通る true 用の処理が必要みたい。
  • print/2 の順番を条件が付く方を後に書いてたら、無限ループになった。
    • 上から順番に条件合致するかチェックしている臭い

こんなとこかな。まぁ動いてよかった。
そしてブロックコメントがないぽくてびびった。

これをもうちょっといい感じにしていこう。

引数もらおうぜ編

input = 10 って固定なのはいけてないから引数でもらおう。

System.argv ってのでもらえるみたいだ。
http://elixir-lang.org/docs/v1.0/elixir/System.html#argv/0 ってのを見つけた。

でもこれListか。まぁそりゃそうか。
今回は1個あればいいのよね。

Listの頭だけ欲しいんだけどat_first的なやつあるかなー。
http://elixir-lang.org/docs/v1.0/elixir/List.html#first/1
あったわ。

これで安心!ってことで、こんな感じで置き換える。

input = System.argv |> List.first

この |> っていうのは pipeline という記法。
まぁいうたら、

input = List.first(System.argv)

という意味。

けどpipelineつかっておしゃれに書くのがElixir流。たしかにちょっときもちいー。

おしゃれに書けたし、これでバッチリ!引数に10つけて実行!
・・・なぜだ。なぜか無限ループ。。。

ハマりまくって、ハマりまくって、もう寝ようかなーとおもったタイミングで解決した。

input = System.argv |> List.first |> String.to_integer;

とぅーいんてじゃーー!

iex(15)> 1 == "1"
false

いやーそうかそうか。そういうもんか。
甘えていた自分がいたのね。
いやーいい感じに型変換してくれると思うじゃない。それは甘えだったのか。
いやー。ゆとってた。いやーまじかー。

printf編

30を引数に実行とかすると。

$ elixir ./fizzbuzz.exs 30
3: Buzz
5: Fizz
6: Buzz
9: Buzz
10: Fizz
12: Buzz
15: FizzBuzz
18: Buzz
20: Fizz
21: Buzz
24: Buzz
25: Fizz
27: Buzz
30: FizzBuzz

ちょっとずれるのがキモいから、printf '%02d' $n 的なことがやりたい。
どうやるんだろ。ぐぐる。英語ばっかり。

なんか、Erlang のほうに io:format っていうのがあるらしい。
でもこれ Elixir だし。。。
でも :io.format って感じで呼ぶことで Erlang の関数呼べるらしい。よかった。

だがそれよりも、このformatがむずすぎる。
だめだ。全然わからん。マニュアルみてもさっぱりわからなかったが、色々なサイト見た結果、

iex(51)> :io.format("~2..0B~n", [1])
01
:ok

って感じでやればいいらしい。 ~n は改行だから無視してよし。
ということで、IO.puts してたところを書き換えて

    cond do
      rem(n, 15) == 0 -> :io.format "~2..0B: FizzBuzz~n", [n]
      rem(n, 5)  == 0 -> :io.format "~2..0B: Fizz~n", [n]
      rem(n, 3)  == 0 -> :io.format "~2..0B: Buzz~n", [n]
      true            -> nil
    end

こうすればいい。

03: Buzz
05: Fizz
06: Buzz
09: Buzz
10: Fizz
12: Buzz
.
.

うん。
出力は綺麗になったけど、コードはキモくなった :heart:

printf編 リベンジ

いやーやっぱどうにかしたい。あのformatとかキモすぎる。

そこで、 Exprintf というモジュールを使えばいつものprintfが使えるようだ。
でも外部モジュールだね。

外部モジュールを使う場合は mix を使うのが定石のよう。
script 1枚でやってたけど、ちゃんとproject をつくってやってみるとするか。

再掲しておくと、

Mix は プロジェクトの buildツール. とりあえず初期化したり、依存モジュールのinstall. 起動とかまぁプロジェクト全体に対して色々できるやつ。perlでいうと carton的な?

て感じ。
elixir 入れただけで mix はいってるよ。

mix project つくる

$ mix new fizzbuzz
* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/fizzbuzz.ex
* creating test
* creating test/test_helper.exs
* creating test/fizzbuzz_test.exs

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

    cd fizzbuzz
    mix test

Run `mix help` for more commands.

うぇい。
中身はどうかな

$ tree
.
├── README.md
├── _build
│   └── test
│       └── lib
│           └── fizzbuzz
│               └── ebin
│                   ├── Elixir.Fizzbuzz.beam
│                   └── fizzbuzz.app
├── config
│   └── config.exs
├── lib
│   └── fizzbuzz.ex
├── mix.exs
└── test
    ├── fizzbuzz_test.exs
    └── test_helper.exs

8 directories, 8 files

うぇい。
テスト流してみろって書いてたな。

$ cd fizzbuzz
$ mix test
Compiled lib/fizzbuzz.ex
Generated fizzbuzz app
.

Finished in 0.06 seconds (0.06s on load, 0.00s on tests)
1 tests, 0 failures

うぇい。
テストって何書いてるんだろ。

$ cat test/fizzbuzz_test.exs
defmodule FizzbuzzTest do
  use ExUnit.Case

  test "the truth" do
    assert 1 + 1 == 2
  end
end

・・・うぇい。

exprintf を入れてみる

まずはcpan的な存在となってるhexにあるかみてみよう。
https://hex.pm/

・・・あった。

では mix.exs ってファイルがあるので、そこに依存を追加する。
node でいうところの package.json 的な存在かな。

  defp deps do
    [{:exprintf, "~> 0.1.6"}]
  end

的な。これ version 書かないといけないのかな。とりあえず最新。って書き方あんのかな。

まぁおいといて 依存をinstallしてみる

$ mix deps
* exprintf (Hex package)
  the dependency is not available, run `mix deps.get`

$ mix deps.get
Running dependency resolution
Dependency resolution completed successfully
  exprintf: v0.1.6
* Getting exprintf (Hex package)
Checking package (https://s3.amazonaws.com/s3.hex.pm/tarballs/exprintf-0.1.6.tar)
Fetched package
Unpacked package tarball (/Users/kuwa0224/.hex/packages/exprintf-0.1.6.tar)

入ったにおい。

$ ls -l deps/
total 0
drwxr-xr-x  7 kuwa0224 kuwa0224  238  8 29 13:48 exprintf

まずはmix経由でさっきのやつ実行してみる

まずはさっきの script をproject以下において実行してみよう。

$ mix run fizzbuzz.exs 10
==> exprintf
Compiled lib/exprintf.ex
Generated exprintf app
==> fizzbuzz
Compiled lib/fizzbuzz.ex
Generated fizzbuzz app

14:03:30.781 [error] Loading of /Users/kuwa0224/work/elixir/fizzbuzz/_build/dev/lib/fizzbuzz/ebin/Elixir.FizzBuzz.beam failed: :badfile


14:03:30.781 [error] beam/beam_load.c(1250): Error loading module 'Elixir.FizzBuzz':
  module name in object code is Elixir.Fizzbuzz



14:03:30.782 [error] Loading of /Users/kuwa0224/work/elixir/fizzbuzz/_build/dev/lib/fizzbuzz/ebin/Elixir.FizzBuzz.beam failed: :badfile


14:03:30.782 [error] beam/beam_load.c(1250): Error loading module 'Elixir.FizzBuzz':
  module name in object code is Elixir.Fizzbuzz


03: Buzz
05: Fizz
06: Buzz
09: Buzz
10: Fizz

おーなんか怒られたーー。でも結果でてる。
あーはいはい。

さっき、mix new fizzbuzz したときに、lib 以下にFizzBuzzができてたのね。
そんで lib でも script のなかでも defmodule Fizzbuzz してるからダブってエラーになったんだろう。

じゃあ、script の中から lib の方に処理を移してみよう。
こんな感じ。

lib/fizzbuzz.ex
defmodule FizzBuzz do
  def run(limit) do
    print(1, limit)
  end

  defp print(n, limit) when n == limit do
    cond do
      rem(n, 15) == 0 -> :io.format "~2..0B: FizzBuzz~n", [n]
      rem(n, 5)  == 0 -> :io.format "~2..0B: Fizz~n", [n]
      rem(n, 3)  == 0 -> :io.format "~2..0B: Buzz~n", [n]
      true            -> nil
    end

  end

  defp print(n, limit) do
    print(n, n)
    print(n + 1, limit)
  end

end
fizzbuzz.exs
#! /usr/bin/env elixir

input = System.argv |> List.first |> String.to_integer;

FizzBuzz.run(input);

これで defmodule FizzBuzz は1回しかやってないことになったぞ。
じゃあ実行してみる。

$ mix run fizzbuzz.exs 10
Compiled lib/fizzbuzz.ex
Generated fizzbuzz app
03: Buzz
05: Fizz
06: Buzz
09: Buzz
10: Fizz

うぇーい。

exprintf つかってみる

exprintfのReadme見る限り、import したあと普通に使えばいいみたい。
まぁやってみよう。

lib/fizzbuzz.ex
defmodule FizzBuzz do
  import ExPrintf

  def run(limit) do
    print(1, limit)
  end

  defp print(n, limit) when n == limit do
    cond do
      rem(n, 15) == 0 -> printf "%02d: FizzBuzz\n", [n]
      rem(n, 5)  == 0 -> printf "%02d: Fizz\n", [n]
      rem(n, 3)  == 0 -> printf "%02d: Buzz\n", [n]
      true            -> nil
    end

  end

  defp print(n, limit) do
    print(n, n)
    print(n + 1, limit)
  end

end
$ mix run fizzbuzz.exs 10
Compiled lib/fizzbuzz.ex
Generated fizzbuzz app
03: Buzz
05: Fizz
06: Buzz
09: Buzz
10: Fizz

いやーーだいぶすっきりしたーーー!
作者に感謝。

テスト書くか

まぁ module がつくられたわけだし、テストを書いてみるか。

標準出力のテストってどうやんのかなーとおもって調べてたら、こんなのがあるみたい。
http://elixir-lang.org/docs/v1.0/ex_unit/ExUnit.CaptureIO.html

test/fizzbuzz_test.exs
defmodule FizzbuzzTest do
  use ExUnit.Case

  import ExUnit.CaptureIO

  test "fizzbuzz with 10" do

    assert capture_io( fn -> FizzBuzz.run(10) end ) == """
03: Fizz
05: Buzz
06: Fizz
09: Fizz
10: Buzz
"""

  end
end

テスト流してみよう!

$ mix test


  1) test fizzbuzz with 10 (FizzbuzzTest)
     test/fizzbuzz_test.exs:6
     Assertion with == failed
     code: capture_io(fn -> FizzBuzz.run(10) end) == "03: Fizz\n05: Buzz\n06: Fizz\n09: Fizz\n10: Buzz\n"
     lhs:  "03: Buzz\n05: Fizz\n06: Buzz\n09: Buzz\n10: Fizz\n"
     rhs:  "03: Fizz\n05: Buzz\n06: Fizz\n09: Fizz\n10: Buzz\n"
     stacktrace:
       test/fizzbuzz_test.exs:8



Finished in 0.06 seconds (0.05s on load, 0.01s on tests)
1 tests, 1 failures

Randomized with seed 942417

あ、 Fizz と Buzz 逆だったことに今更きづく。。(ガチ)
直した。

$ mix test
Compiled lib/fizzbuzz.ex
Generated fizzbuzz app
.

Finished in 0.05 seconds (0.05s on load, 0.00s on tests)
1 tests, 0 failures

Randomized with seed 117381

まぁいい練習になったね。テスト1ケースだけどまぁいいか。

・・・とかすんなり書いたけどテストだいぶつまってた。
最初 mock とかいうのを使って書こうとしたんだけど、
関数によってmockできたり、できなかったりがあってだいぶ詰まった。。

実はEnum.map使えばもっとすっきりできた

ここで Elixir の FizzBuzz でぐぐってみた。
めっちゃすっきりしてる。それを活かしてみる。

どうしてこうなった。普通に for 文でくるくるまわすのに。。
Elixir は for 文ないんだって!ヤバイ。

じゃあどうするかっていうと、再帰(recursion) を使うのか。

って書いてたけど、色々見てたらどこだったかに、
「再帰再帰っていっても面倒だよね、一応 Enum.map 関数あるから使おうや」
ってあった。

ほほう。どれどれ。

iex(136)> Enum.map( 1..3, fn(x) -> x + 5 end )
[6, 7, 8]

おぉーー。これは perl の map とノリが一緒だ。こんなんあったのかー。

iex(137)> Enum.map( 1..3, fn(x) -> x + 6 end )
'\a\b\t'

え?は?どうしたどうした。
5 を 6 に変えただけで事件が起きたんだけど。

iex(141)> [7, 8, 9] == '\a\b\t'
true

いやでも、 true なんだねー。へーーーー。
よくわからんが内部的には同じものとして扱っているんだね。

シングルクォートの場合は各文字の文字コードのリストになるっていう仕様があるらしいし。
びっくりしたけど、実は同じ実体なんだね。
それにしてもバビるな。これわ。

何ってことない数字のリストが、シングルクォートの文字と同じ実体。
っていうのはElixirの特徴と一つとい言えるんだろう。
いやーこれ結構罠なんじゃない?そんなことないのかな。

というのは置いておいて、map が使えそうなのはわかった。
これを使ってFizzBuzz書きなおしてみよう。

lib/fizzbuzz.ex
defmodule FizzBuzz do
  import ExPrintf

  def run(limit) do
    Enum.map(1..limit, &(print &1))
  end

  defp print(n) do
    cond do
      rem(n, 15) == 0 -> printf "%02d: FizzBuzz\n", [n]
      rem(n, 5)  == 0 -> printf "%02d: Buzz\n", [n]
      rem(n, 3)  == 0 -> printf "%02d: Fizz\n", [n]
      true            -> nil
    end
  end
end

動きました。めっちゃスッキリ。
map の第二引数のcoderef 渡すところ調子こいて別な書き方にしてみました。
こういう書き方もありなんだね。

パターンマッチングちゃんとつかってみるか

Elixir はパターンマッチングが強力らしいので、
まぁ若干無理やりだけど使ってみるか。

こんなんになる。

  defp print(n) do
    case { rem(n, 3), rem(n, 5), n } do
      { 0, 0, n } -> printf "%02d: FizzBuzz\n", [n]
      { _, 0, n } -> printf "%02d: Buzz\n", [n]
      { 0, _, n } -> printf "%02d: Fizz\n", [n]
      { _, _, n } -> nil
    end
  end

ちょっとすっきりしたかも?
_ は無視していいやつ。 caseの横に。ターゲットとなるものを書いて、
各行でマッチングチェックしていく感じ。

ちなみに一番下の nil のやつがないと、もしどこにも引っかからないときがあったら、

** (CaseClauseError) no case clause matching: {1, 1, 1}

って怒られる

ていうか並行処理まったくやってなくね

折角なのでこれもFizzBuzzでやってみる。
並行処理は spawn / send / receive をうまく使うみたい。

  • spawn で別プロセスにて処理を実行させる。
  • send でその別れたプロセスから、別のプロセスへメッセージング。
  • receive でメッセージングを受け取る。

うん。まぁまぁわかりますな。
ちなみにこの別プロセスってやつだけども、

ElixirのプロセスをOSのプロセスと混同してはいけません.プロセスは(他の多くのプログラミング言語におけるスレッドとは異なり)メモリとCPUにとって非常に軽量です.ですから,同時に動作する数千のプロセスを保持することは特別珍しいことではありません.

by http://elixir-ja.sena-net.works/getting_started/11.html

とのことでした。まぁとにかく軽いと。

で、これらを使って実装してみるとこうなった。

lib/fizzbuzz.ex
defmodule FizzBuzz do
  import ExPrintf

  def run(limit) do
    parent = self()
    pids = Enum.map(1..limit, fn(n) ->
      spawn fn ->
        send parent, calculate(n)
      end
    end)

    Enum.map(pids, fn(pid) ->
      receive do
        { :true,  n, msg } -> printf "%02d: %s\n", [n, msg]
        { :false, _, nil } -> nil
      end
    end)
  end

  defp calculate(n) do
    case { rem(n, 3), rem(n, 5), n } do
      { 0, 0, n } -> { :true, n, "FizzBuzz" }
      { _, 0, n } -> { :true, n, "Buzz" }
      { 0, _, n } -> { :true, n, "Fizz" }
      { _, _, n } -> { :false, n, nil }
    end
  end
end

ちゃんとreceive 使いたかったので、さっきの例の print/1calcurate/1 に変えました。
まぁ計算だけして、printはしないようになったよ。くらいですな。

まぁ流れとしては、多分むずくないとおもう。

  1. self() で今のpidがとれるんでとっておく。
  2. そのあと、Enum.map/2 で クルクル処理を回す
    • そこではfn -> send parent, caluculate(n) end って無名関数を各子プロセスでやってくれーいって感じで回している。
  3. spawnの返り値は、子プロセスのpidになるので、子プロセスのpidの配列が pids に格納される
  4. で、その後子プロセス配列を map で回し、子プロセスからの通知を receive で待つ。
  5. そんなことを親プロセスがしている間に、子プロセスは さっき指定された calucurate/1 を実行している
    • この case 句でかかれたマッチングの行の右辺がそのまま返り値になる
    • 元々 send parent, caluculate(n) で呼ばれていたので、これがこのまま 親プロセスの receive で受け取られる
  6. 親プロセスで、receive できたら、それを更にマッチングにかけて printf を実行する
  7. 親プロセスはまた次の receive を行う

というざっくり流れ。
意外と普通。

sleep sort

前に先輩にAnyEventの練習でおしえてもらってsleep sortってのがあったのでElixirでもやってみよう。
sleep sort ってのは、数字の配列のsortをsleepを使ってやるというまさかのソートアルゴリズムw

sleep_sort.exs
#! /usr/bin/env elixir

arr = Enum.shuffle(1..10)

IO.puts "target is ..."
IO.inspect arr

parent = self()
pids = Enum.map( arr, fn(n) ->
  spawn fn ->
    :timer.sleep n * 1000
    send parent, n
  end
end)

IO.puts "result is ..."
result = Enum.map( pids, fn(_) ->
  receive do
    n -> n
  end
end)
IO.inspect result
# -----
# $ mix run sleep_sort.exs
# target is ...
# [3, 7, 2, 8, 6, 10, 4, 9, 1, 5]
# result is ...
# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

結構かんたんだった。

  • IO.inspect は配列とかをdumpしてくれる君
  • :timer.sleep で sleep処理はできるが、ミリ秒単位で待っていることを注意

参考にさせてもらったやつ

123
118
3

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
123
118