Tambourine作業メモ

主にスキル習得のためにやった作業のメモ。他人には基本的に無用のものです。

Python2.7をインストールして、ちょっとしたツールを書く。

仕事で、Python2.7しかない環境でちょっとした修正をしなくてはならなくなり、 手元にインストールする。

pyenvを使う。

今は、3.8しか入ってない。自分はPythonといえばこれしか使ったことがないRuby野郎である。

> pyenv version
3.8.5 (set by /Users/tambara/.pyenv/version)

pyenv install --listして入れるバージョンを決める

> pyenv install --list
Available versions:
  2.1.3
  2.2.3
  2.3.7
  2.4.0
  2.4.1
(以下略)

2.7の最新は2.7.18らしい。

> pyenv install 2.7.18
python-build: use openssl from homebrew
python-build: use readline from homebrew
Downloading Python-2.7.18.tar.xz...
-> https://www.python.org/ftp/python/2.7.18/Python-2.7.18.tar.xz
Installing Python-2.7.18...
python-build: use readline from homebrew
python-build: use zlib from xcode sdk
Installed Python-2.7.18 to /Users/tambara/.pyenv/versions/2.7.18

作業ディレクトリで、2.7.18を使うことを宣言する。

> pyenv local 2.7.18
> python --version
Python 2.7.18

おっけー。

やりたいことは、以下の様なHTMLの表が

<table class="cols" border="2" cellspacing="0" cellpadding="4" frame="box">
<tr valign="top">
<td class="propname"><B><script language="JavaScript">splitLongName('テスト')</script></B></td>
<td class="propval">VarChar/Unicode(1)</td>
</tr>

JavaScriptのない環境では上手く表示できないのを直す。要するに

<script language="JavaScript">splitLongName('テスト')</script>

テスト

に出来ればよい。

こんな感じ?

> cat fix_htm.py 
#!/usr/bin/env python

import re
import sys

r = re.compile(r"<script language=\"JavaScript\">splitLongName\('(.+)'\)</script>")

with open(sys.argv[1]) as f:
    for line in f:
        mo = r.search(line)
        if mo:
            item = mo.group(1)
            print r.sub(item, line.rstrip()) 
        else:
            print line.rstrip()

> ./fix_htm.py sample.txt
<table class="cols" border="2" cellspacing="0" cellpadding="4" frame="box">
<tr valign="top">
<td class="propname"><B>テスト</B></td>
<td class="propval">VarChar/Unicode(1)</td>
</tr>

よさげ。

ちなみに、Rubyならワンライナーである。

> ruby -pe '$_.sub!(%r!<script language="JavaScript">splitLongName\\(\'(.+)\'\\)</script>!, \'\\1\')' sample.txt 
<table class="cols" border="2" cellspacing="0" cellpadding="4" frame="box">
<tr valign="top">
<td class="propname"><B>テスト</B></td>
<td class="propval">VarChar/Unicode(1)</td>
</tr>

Pythonの方も、もう少しスマートに出来そうなものだけど・・・

Elixirで遊んでみる(11)

13章はまだまだ続く。13.11から。全開までで、GitHubから取得したIssueのデータは、マップの形にまでなった。

今度はこれをテーブルの形に整形する。

文字列の表にしたいから、まず、各列の文字列の最長幅を知る必要がある。 そのために、データをいったん列のリストに組み替えて、 列の最長幅のリストに変換し、さらにそれを使って:io.formatのフォーマット文字列を作っている。 これはErlangのライブラリなので、参照するのはここ。 フォーマットの説明がよくわからない・・・ElixirとErlangの両方のライブラリを見なければいけないのはなかなか面倒くさい。

その後、出力はまた行ごとのデータにしなければならないので、List.zipで戻してる。

・・・というような処理の流れをP.160のコードから読み取るのはなかなか面倒くさかった。 テストがなかったら、わからなかったかも。

というわけで、なんとか写経完了。iex上から、Issues.CLI.runを実行するとちゃんと動いている。

これをちゃんとコマンドラインのアプリにする。escriptというツールを使うんだそうな。 mix.exsにmain関数を持つモジュールを指定すればOK。 あとは、mix escript.buildを実行すると、実行ファイルが出来る。便利だ。

> ls -l
total 3488
-rw-r--r--   1 tambara  staff      492 Jan  2 18:41 README.md
drwxr-xr-x   4 tambara  staff      128 Jan  2 23:13 _build/
drwxr-xr-x   3 tambara  staff       96 Jan  3 12:19 config/
drwxr-xr-x  12 tambara  staff      384 Jan  3 10:31 deps/
-rwxr-xr-x   1 tambara  staff  1772174 Jan  4 14:57 issues*
drwxr-xr-x   4 tambara  staff      128 Jan  2 20:10 lib/
-rw-r--r--   1 tambara  staff      733 Jan  4 14:57 mix.exs
-rw-r--r--   1 tambara  staff     2827 Jan  3 10:31 mix.lock
drwxr-xr-x   6 tambara  staff      192 Jan  3 22:47 test/

ライブラリを持っていくからだと思うけど、そこそこ大きい。 これはErlangがインストールされていれば、コンピュータのアーキテクチャを問わず、どこでも実行出来る。 Javaっぽいね。

> ./issues elixir-lang elixir
numbe | created_at           | title                                                                           
------+----------------------+---------------------------------------------------------------------------------
10609 | 2020-12-30T10:48:11Z | Missing keys in type inference for keyword lists                                
10611 | 2020-12-31T01:05:09Z | "Incompatible types in guard" warning                                           
10617 | 2021-01-02T04:48:59Z | Correct protocol error, when @fallback_to_any true and no implementation for Any
10620 | 2021-01-03T01:01:34Z | Fix cryptic error when defimpl is defined without :for option     

13章はこの後、ロガーの説明と、ExDoc(JavaDocみたいなもの)の説明がある。 必要だね。というわけで、長かった13章はおしまい。

13章で出てきたmixのコマンドをまとめておく。

  • mix new (プロジェクト名)
  • mix run -e '(Elixirのコード)'
  • mix test
  • mix deps
  • mix deps.get
  • mix escript.build
  • mix docs
  • iex -S mix ←オマケ

Elixirで遊んでみる(10)

13章の続き。

さて、GitHubAPIからJSONが返ってきているので、これを処理してやる必要がある。

JSONのライブラリとしてはpoisonを使うみたい。 hex.pmに行って検索し、depsの記述をコピってmix.exsに入れる。

P.156の記述を元に、Issues.GithubIssues.handle_responseを修正する。ここで、

  def handle_response({_, %{status_code: status_code, body: body}}) do
    {
        status_code |> check_for_error(),
        body |> Poison.Parser.parse!()
    }
  end

のようにしてるんだけど、これはwarningが出る。

warning: Poison.Parser.parse!/1 is undefined or private. Did you mean one of:

      * parse!/2

  lib/issues/github_issues.ex:17: Issues.GithubIssues.handle_response/1

parse!には引数が2つ必要だと言ってるんだけど、それがwarningなのがよくわからない。 実行時エラーになるのかな?このままにしておいてみよう。

ちなみに、2つ目の引数にはパースのオプションを入れるみたい。何も指定しないなら%{}を渡す。 %{keys: :atoms!}を渡すと、戻ってくるマップのキーがアトムになる。でも、この!は何だろう?

Poisonのドキュメントにこう注釈してある

Note that keys: :atoms! reuses existing atoms, i.e. if :name was not allocated before the call, you will encounter an argument error message.

keys: :atoms!はアトムを再利用する。例えば、:nameがそれ以前に使われてなければ、エラーになる」・・・なるほど?

You can use the keys: :atoms variant to make sure all atoms are created as needed. However, unless you absolutely know what you’re doing, do not do it. Atoms are not garbage-collected, see Erlang Efficiency Guide for more info

keys: :atomsを使うこともできるが、全てのアトムが必要になった時点で作られる。これが意味するところを完全に理解していないならば、これを使うべきではない。アトムは決してGCされないから」。な、なるほど。しかし、JSONのキーに出てくる名前の全部のアトムを使うことがそんなに問題なのかな?

本ではここでちょっと寄り道。ソースコードにハードコードしたGitHubのURLをconfig.exsへ出すのだと。ふむふむ。

mix new で新しいプロジェクトを作ったとき、config/ というディレクトリが作られたのを 思い出してほしい。config ディレクトリには、config.exs というファイルが含まれていた。 このファイルは、アプリケーションレベルの設定だ。

アレ?私の手元には作られてないんだけど?

ググってみると、これは1.9での変更らしい。

dev.to

まず、mix newconfig/config.exsをつくらなくなりました。設定ファイルに依存することは、ライブラリやその作者にとって望ましくないとされてきたからです。つぎに、mix new --umbrella は、子アプリケーションごとの設定はつくりません。すべての設定は、アンブレラプロジェクトのルートで宣言することになります。理由については「No longer generate config for mix new」をご参照ください。

ふむ・・・

use Mix.Configはやわらかな非推奨(soft-deprecated)となります。これからは、Elixirに新たに備わったConfigモジュールをimport Configとしてお使いください。

ここは後でフォローするとして、いったんは本の通りにしてみようかな。

> mkdir config
> echo "use Mix.Config" > config/config.exs

これで良いのかな?

これで、P.157の通りにconfig.exsとgithub_issues.exを修正。

実行してみよう。

> iex -S mix
Erlang/OTP 23 [erts-11.1.5] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe] [dtrace]

Compiling 3 files (.ex)
warning: Poison.Parser.parse!/1 is undefined or private. Did you mean one of:

      * parse!/2

  lib/issues/github_issues.ex:18: Issues.GithubIssues.handle_response/1

Generated issues app
Interactive Elixir (1.11.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Issues.GithubIssues.fetch("elixir-lang", "elixir")
** (UndefinedFunctionError) function Poison.Parser.parse!/1 is undefined or private. Did you mean one of:

      * parse!/2

    (poison 4.0.1) Poison.Parser.parse!("{\"message\":\"Not Found\",\"documentation_url\":\"https://docs.github.com/rest\"}")
    (issues 0.1.0) lib/issues/github_issues.ex:18: Issues.GithubIssues.handle_response/1

あー、やっぱり実行時エラーになった。

というわけで、

body |> Poison.Parser.parse!(%{})

のように引数を足す。 これでOK。

後は、Issues.CLI.processにソートと指定の件数だけ取り出す機能を追加する。 これは本の通りにやって、特に不明なところはない。

Elixirで遊んでみる(9)

13章はビルドツールのMixの話。最近は言語ごとにビルドツールがあるのが当たり前になった。

このタイミングで自分のMacにElixirをインストールすることにする。brew install elixirするだけ。 いろいろ依存で入るので、そこそこ時間がかかる。というかPython3.8と3.9を両方インストールしようとするのは何故なのか。 そして、Rubyはクリスマスに出たばっかりの3.0.0_1が入るw

では、さっそくP.143に従って、mix new issuesする。issuesというプロジェクト名なのは、 GitHubからIssueのリストを取ってくるアプリを作ってみようというサンプルだから。 libとconfig, testというディレクトリが作られる。

まず、コマンドライン引数の処理を作る。 規約として、プロジェクト名.CLIというモジュールを作るらしい。 ファイルとしては、lib/$project_name/cli.exになる。なるほど。

とりあえず、今は本のcli.exをそのまま写経する。写経しているときに浮かんだ理解や疑問点をメモっておく。

  • javadoc的なものをモジュールの属性として書くのは面白い。
  • モジュールのスタートポイントをrunという関数にするのは慣習?
  • parse_argsがdefpじゃないのは何でだろう。
  • OptionParseの戻り値の扱いが荒っぽくみえるけど、これで十分なのかな?

そして、作ったものをExUnitというテストフレームワークでテストする。testディレクトリ以下にcli_test.exsを作る。 こっちは.exじゃなくて.exsなのね。面白い。

  • doctestは何を意味しているんだろう?
  • importでparse_args/1だけを対象にしている。parse_argsがdefpじゃなかったのはこのため?

そして、テストが出来た時点で、リファクタリングする。caseのパターンマッチ1つ1つを関数にする。 これがわかりやすいのかどうかはわかんないけど、そういう流儀なんだね。

次に、GitHubにアクセスする部分を作る。httpクライアントが必要だ。 RubyRubyGemsやNode.jsのnpmみたいなものとして、Hexがある。 ただし、これはErlangとElixirの両方にとってのパッケージみたい。 全然シンタックスの違う言語だけど、共用できるものなのかしらん?

それはともかく、ここからhttpクライアントを探してインストールする。 著者はHTTPoisonというのを使うといっているのでそれを選ぶ。 Mix用の依存記述があるので、mix.exsのdepsのところにコピペする。 このあたりは、Maven Reposなんかと同じなので、戸惑うことはない。

mix depsで依存性チェックが行われる。実際に取得するにはmix deps.getする。

> mix deps
Could not find Hex, which is needed to build dependency :httpoison
Shall I install Hex? (if running non-interactively, use "mix local.hex --force") [Yn] Y
* creating /Users/tambara/.mix/archives/hex-0.20.6
* httpoison (Hex package)
  the dependency is not available, run "mix deps.get"

> mix deps.get
Resolving Hex dependencies...
Dependency resolution completed:
New:
  certifi 2.5.3
  hackney 1.17.0
  httpoison 1.7.0
  idna 6.1.1
  metrics 1.0.1
  mimerl 1.2.0
  parse_trans 3.3.0
  ssl_verify_fun 1.1.6
  unicode_util_compat 0.7.0
* Getting httpoison (Hex package)
* Getting hackney (Hex package)
* Getting certifi (Hex package)
* Getting idna (Hex package)
* Getting metrics (Hex package)
* Getting mimerl (Hex package)
* Getting parse_trans (Hex package)
* Getting ssl_verify_fun (Hex package)
* Getting unicode_util_compat (Hex package)

なるほど。

というわけで、これを使ってIssues.GithubIssues.fetchを実装する。また写経。

写経ができたら、今度はIExで動作を確かめる。iex -S mixでmixでの依存性を解決した上で起動される。

> iex -S mix
Erlang/OTP 23 [erts-11.1.5] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe] [dtrace]

Could not find "rebar3", which is needed to build dependency :parse_trans
I can install a local copy which is just used by Mix
Shall I install rebar3? (if running non-interactively, use "mix local.rebar --force") [Yn] Y
* creating /Users/tambara/.mix/rebar
* creating /Users/tambara/.mix/rebar3
===> Compiling parse_trans
===> Compiling mimerl
===> Compiling metrics
===> Compiling unicode_util_compat
===> Rebar3 detected a lock file from a newer version. It will be loaded in compatibility mode, but important information may be missing or lost. It is recommended to upgrade Rebar3.
===> Compiling idna
==> ssl_verify_fun
Compiling 7 files (.erl)
Generated ssl_verify_fun app
===> Compiling certifi
===> Rebar3 detected a lock file from a newer version. It will be loaded in compatibility mode, but important information may be missing or lost. It is recommended to upgrade Rebar3.
===> Compiling hackney
==> httpoison
Compiling 3 files (.ex)
Generated httpoison app
==> issues
Compiling 3 files (.ex)

== Compilation error in file lib/issues/github_issues.ex ==
** (SyntaxError) lib/issues/github_issues.ex:18:29: keyword argument must be followed by space after: status_code:
    (elixir 1.11.2) lib/kernel/parallel_compiler.ex:314: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/7

途中でインストールを要求されているrebar3はErlangのビルドツール。素直に入れる。 おっと、コンパイルエラーになってるわ。直してリトライ。

> iex -S mix
Erlang/OTP 23 [erts-11.1.5] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe] [dtrace]

Compiling 3 files (.ex)
Generated issues app
Interactive Elixir (1.11.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Issues.GithubIssues.fetch("elixir-lang", "elixir")
{:ok,
 "[{\"url\":\"https://api.github.com/repos/elixir-lang/elixir/issues/10621\",\"repository_url\":\"https://api.github.com/repos/elixir-lang/elixir\",\"labels_url\":\"https://api.github.com/repos/elixir-lang/elixir/issues/10621/labels{/name}\",\"comments_url\":\"https://api.github.com/repos/elixir-lang/elixir/issues/10621/comments\",\"events_url\":\"https://api.github.com/repos/elixir-lang/elixir/issues/10621/events\",\"html_url\":\"https://github.com/elixir-lang/elixir/pull/10621\",\"id\":777551967,\"node_id\":\"MDExOlB1bGxSZXF1ZXN0NTQ3ODE2NzU5\",\"number\":10621,\"title\":\"Update to a more descriptive typespec in Macro.Env\",\"user\":{\"login\":\"eksperimental\",\"id\":9133420,\"node_id\":\"MDQ6VXNlcjkxMzM0MjA=\",\"avatar_url\":\"https://avatars2.githubusercontent.com/u/9133420?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/eksperimental\",\"html_url\":\"https://github.com/eksperimental\",\"followers_url\":\"https://api.github.com/users/eksperimental/followers\",\"following_url\":\"https://api.github.com/users/eksperimental/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/eksperimental/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/eksperimental/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/eksperimental/subscriptions\",\"organizations_url\":\"https://api.github.com/users/eksperimental/orgs\",\"repos_url\":\"https://api.github.com/users/eksperimental/repos\",\"events_url\":\"https://api.github.com/users/eksperimental/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/eksperimental/received_events\",\"type\":\"User\",\"site_admin\":false},\"labels\":[],\"state\":\"open\",\"locked\":false,\"assignee\":null,\"assignees\":[],\"milestone\":null,\"comments\":0,\"created_at\":\"2021-01-03T01:04:21Z\",\"updated_at\":\"2021-01-03T01:04:21Z\",\"closed_at\":null,\"author_association\":\"MEMBER\",\"active_lock_reason\":null,\"pull_request\":{\"url\":\"https://api.github.com/repos/elixir-lang/elixir/pulls/10621\",\"html_url\":\"https://github.com/elixir-lang/elixir/pull/10621\",\"diff_url\":\"https://github.com/elixir-lang/elixir/pull/10621.diff\",\"patch_url\":\"https://github.com/elixir-lang/elixir/pull/10621.patch\"},\"body\":\"\",\"performed_via_github_app\":null},{\"url\":\"https://api.github.com/repos/elixir-lang/elixir/issues/10620\",\"repository_url\":\"https://api.github.com/repos/elixir-lang/elixir\",\"labels_url\":\"https://api.github.com/repos/elixir-lang/elixir/issues/10620/labels{/name}\",\"comments_url\":\"https://api.github.com/repos/elixir-lang/elixir/issues/10620/comments\",\"events_url\":\"https://api.github.com/repos/elixir-lang/elixir/issues/10620/events\",\"html_url\":\"https://github.com/elixir-lang/elixir/pull/10620\",\"id\":777551630,\"node_id\":\"MDExOlB1bGxSZXF1ZXN0NTQ3ODE2NDk2\",\"number\":10620,\"title\":\"Fix cryptic error when defimpl is defined without :for option\",\"user\":{\"login\":\"eksperimental\",\"id\":9133420,\"node_id\":\"MDQ6VXNlcjkxMzM0MjA=\",\"avatar_url\":\"https://avatars2.githubusercontent.com/u/9133420?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/eksperimental\",\"html_url\":\"https://github.com/eksperimental\",\"followers_url\":\"https://api.github.com/users/eksperimental/followers\",\"following_url\":\"https://api.github.com/users/eksperimental/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/eksperimental/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/eksperimental/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/eksperimental/subscriptions\",\"organizations_url\":\"https://api.github.com/users/eksperimental/orgs\",\"repos_url\":\"https://api.github.com/users/eksperimental/repos\",\"events_url\":\"https://api.github.com/users/eksperimental/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/eksperimental/received_events\",\"type\":\"User\",\"site_admin\":false},\"labels\":[],\"state\":\"open\",\"locked\":false,\"assignee\":null,\"assignees\":[],\"milestone\":null,\"comments\":0,\"created_at\":\"2021-01-03T01:01:34Z\",\"updated_at\":\"2021-01-03T01:01:34Z\",\"closed_at\":null,\"author_association\":\"MEMBER\",\"active_lock_reason\":null,\"pull_request\":{\"url\":\"https://api.github.com/repos/elixir-lang/elixir/pulls/10620\",\"html_url\":\"https://github.com/elixir-la" <> ...}

なんか動いているっぽい。今回はここまで。

Elixirで遊んでみる(8)

12章は制御フロー。といっても、パターンマッチとガード説で制御フローの大半を作ってしまうので、Elixirでは登場頻度は低い。

ifは今までの構文と同じようにdo:を指定できる。さらにelse:も指定できる。 この構文の特徴をもう一度確認しておこう。

iex> if(true, [do: "hoge", else: "fuga"])
"hoge"

このように、ifは2つの引数を取る。1つ目が条件、2つ目はキーワードリストだ。else:はなくてもいい。

で、関数呼び出しの括弧は省略できる。最後の引数がキーワードリストの場合、リストの角括弧も省略できる。

iex> if true, do: "hoge", else: "fuga"   
"hoge"

さらにdo:はブロックのように書くシンタックスシュガーがある

iex> if true do                       
...>   "hoge"
...> else
...>   "fuga"
...> end
"hoge"

面白いね。

ついでに、Rubyと同じようにunlessもある。Rubyのunlessは後置で早期リターンに使うのが便利。 それ以外で使うと混乱しがち。特にunlessにelseを付けると大混乱しがち。 それを踏まえると、Elixirのunlessはどういうときに使うんだろう?

次はcond。true/falseを返す条件式を使って、if..elsif..elseみたいなことがしたいときに使う。 条件じゃなくて、パターンマッチで実行するコードを選びたければ、caseを使う。 この2つの文法はよく似てる。

Elixirには例外もある。raise "xxx"でメッセージと共にRuntimeErrorを起こせる。 まあ、あんまり積極的には使わないものらしい。

Elixirで遊んでみる(7)

11章は文字列。

Elixirで面白いのは、シングルクォートとダブルクォートで全く違うものを表すこと。 ダブルクォートがいわゆる文字列で、シングルクォートは文字コードのリスト。内部表現は全然違う。 というか、ワイルドなことにElixirは文字列として出力可能な整数のリストをシングルクォートで囲まれた文字として出力する。マジカ。

iex> hello = [0x48, 0x65, 0x6c, 0x6c, 0x6f]
'Hello'
iex> hello ++ [0]
[72, 101, 108, 108, 111, 0]
iex> [first |rest] = hello
'Hello'
iex> {first, rest}
{72, 'ello'}

おもしろい。

次にバイナリ。ビット列の表現で、かなり厳密にビットの並びを表現できる。 さらに、その表現を使ってパターンマッチすることでビットを分割できる。 下はIEEE754倍精度浮動小数点数をフィールドで分解して再構築している例。

iex> << sign::size(1), exp::size(11), mantissa::size(52) >> = << 3.14159::float >>
<<64, 9, 33, 249, 240, 27, 134, 110>>
iex> (1 + mantissa / :math.pow(2, 52)) * :math.pow(2, exp-1023) * (1 - 2*sign)
3.14159

これはかなり簡潔で凄い。

さて、ダブルクォート文字列は内部的にUTF-8のバイト列としてバイナリに格納されている。 つまり、リストではない。そして、バイト列としての長さと、文字列の長さはまったく別である。

iex> s_and_b = "寿司🍣ビール🍺"
"寿司🍣ビール🍺"
iex> String.length s_and_b
7
iex> byte_size s_and_b
23
iex> String.codepoints(s_and_b)
["寿", "司", "🍣", "ビ", "ー", "ル", "🍺"]

String.codepointsとString.graphemesの違いの説明あり。 コードポイントと書記素は違うもの。この辺がちゃんと用意されているのが新しい言語だなーと思う。

iex> 日米対決 = "🇯🇵VS🇺🇸"
"🇯🇵VS🇺🇸"
iex> String.codepoints 日米対決
["🇯", "🇵", "V", "S", "🇺", "🇸"]
iex> String.graphemes 日米対決
["🇯🇵", "V", "S", "🇺🇸"]

そして、リストじゃないので[ car | cdr ]ではパターンマッチできない。 代わりに<< head :: utf8, tail :: binary>>を使う。

Elixirで遊んでみる(6)

9章は2ページしかない。Elixirには型みたいなものが2つのレベルである。 プリミティブなデータ型と、モジュールとそれが提供する関数が規定するレベルでの型だ。 あるモジュールはプリミティブなデータ型に加えてある構造を要求する。

例えば、ListもKeywordも実体としてのデータの型は単なるリストだ。 しかし、「アトムと値のペアからなるタプルのリスト」という追加の構造を持つことでKeywordモジュールの関数は成り立っていて、 利用するプログラマはそれを含めてKeywordをListとは違う型のように受け取っている。 つまり、OOP言語で言えば、プリミティブな文法レベルの上に標準クラスライブラリの話がはじまるが、 それに当たるAPIの話がこの後に来て、プログラマとしてはまさにそれに習得することが重要なのだろう。

というわけで10章はコレクションAPIの話になる。

最初はEnum。だいたい、RubyのEnumerableにありそうな関数がある。mapとかfilterとかsortとか。joinもここにある。 all?とかempty?みたいな?が関数名につくのもRuby style。ただし、include?はmember?になってる。 当然、reduceもここにある。

さて、P.102には練習問題がある。all?を実装してみろと。挑戦してみよう。こんな感じかな?

defmodule MyEnum do
  def all?([], _), do: true
  def all?([first|rest], f) when not f.(first), do: false
  def all?([_|rest], f), do: all?(rest, f)
end

えっ?ダメなの?

iex(117)> c "ch10ex5.exs"

== Compilation error in file ch10ex5.exs ==
** (CompileError) ch10ex5.exs:3: invalid expression in guard, anonymous call is not allowed in guards. To learn more about guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html
    (stdlib 3.14) lists.erl:1358: :lists.mapfoldl/3
** (CompileError)  compile error
    (iex 1.11.2) lib/iex/helpers.ex:200: IEx.Helpers.c/2

ガード節の中で無名関数は使えないと。えー、マジカヨ。

defmodule MyEnum do
  def all?([], _), do: true
  def all?([first|rest], f) do
    if f.(first) do
      all?(rest, f)
    else
      false
    end
  end
end

ダサい。猛烈にダサい。何かがおかしい。コレジャナイハズ。うーむ・・・

次は、Stream。遅延評価したいときはEnumじゃなくてStreamを使う。

自分でストリームを作ることも出来る。以下はcycle, repeatedly, iterateの例。

iex> Stream.cycle([1, 2, 3]) |> Enum.take(5)
[1, 2, 3, 1, 2]
iex> Stream.repeatedly(&:random.uniform/0) |> Enum.take(5)
[0.4435846174457203, 0.7230402056221108, 0.94581636451987, 0.5014907142064751,
 0.311326754804393]
iex> Stream.iterate(1, &(&1 * -1)) |> Enum.take(5)
[1, -1, 1, -1, 1]

これはわかりやすい。

Stream.unfoldは、前のイテレーションのステータスによって次の値が作られるようなものだ。 何に使うんだろう。とりあえず、本にはフィボナッチの例が出ている。

iex> Stream.unfold({0,1}, fn {f1, f2} -> {f1, {f2, f1+f2}} end) |> Enum.take(15)
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377]

Stream.resourceはやりたいことはわかるけど、なかなか難しい。 3つの関数を与える。初期化、値の取得、終了処理の3つ。そうすると、値が取り出せるストリームが作られる。 例えば、ファイルを1行ずつ読むとしたら、(1)ファイルのオープン(2)1行リード(3)ファイルのクローズ だ。 実際、これを作ることになったらかなり悩みそう。

この章の最後は、内包表現。理解できるかなあ・・・。こんな感じだ。

iex> first8 = Enum.to_list 1..8
[1, 2, 3, 4, 5, 6, 7, 8]
iex> for x <- first8, y <- first8, x >= y, rem(x*y, 10) == 0, do: {x, y}
[{5, 2}, {5, 4}, {6, 5}, {8, 5}]

forの後ろに複数のパターンマッチを書いてよい。その後ろにフィルタが来る。パターンマッチやフィルタでは、それ以前のマッチの結果をつかっていい。

内包表現の返り値はリストに限らない。into: で変更できる。

iex> for x <- ~w{ USA Japan Luxembourg }, into: %{}, do: {x, String.length(x)}
%{"Japan" => 5, "Luxembourg" => 10, "USA" => 3}