Tambourine作業メモ

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

F#で遊んでみる(1)

2年ぐらい前に、戯れにOCamlの勉強をしてみた。ML系の言語は、広く使われているとは言えないもののこれが必要な人にとっては他に換えがたいもののようである。その中で、OCamlという言語はMLとしては実用的な言語として開発母体もそこそこちゃんとしていて、それなりのエコシステムがあり、その手の人に広く使われているものらしい。

ところが、ここのところML系の言語の選択肢として、OCamlからMicrosoftのF#へと人気が移ってきているという記事をちらほらと見かけた。ホントなんだろうか。とりあえず、F#という言語が.net Frameworkのお仲間としてあるとは聞いているが、それはあのOCamlの代わりになるようなシロモノなんだろうか。

調べてみたら、そんなシロモノだった。というか、F#は.NETの上でOCamlすることが目的の言語で、むしろシンタックスはそっくりレベルのものだった。ただ、Visual Studio 2010に搭載されてデビューした当時は、MLを使いたいからといってそれだけの為に人はVSを買わなかっただろうし、VSを買う人が「せっかくだから」と使うわけもないし、私のような人には何のためにあるのかよくわかんない言語だと認識されていたように思う。しかし、そこから時は流れ、10年経ち、言語としてのF#は熟成が進み、ランタイムとしての.NETは.NET CoreとしてWindows以外の実行環境でもちゃんと動いて、かつ、無償で配布され、Visual Studio CodeによってVSがなくても、Windowsじゃなくても、全然問題なくなった。Linux上でF#を書いたり実行したり普通に出来て、ライブラリはMicrosoftの.Netエコシステムという非常に強力なものが使える。こりゃええわい・・・ということらしい。

そんじゃ、ま、とりあえず、Macの上でF#で気軽にコードを書いてみることができるのか。いや、関数型言語を気楽に書くような能力は私には全然ないわけだけども、とりあえず、どんな感じだか体感してみよう。

参考にするのは、この記事。

F# を知ってほしい - Qiita

さて、何はなくともインストール。Microsoftのサイトから、.NETのSDKを落としてくる。

dotnet.microsoft.com

ここで、.NET5を選べば良いのか、.NET Core 3.1を選べば良いのか・・・さっぱりわからない。.NET ⊇ .NET Core だと思う。たぶん、ASP.netが入っているかいないかとか、そういう差なんだと思うんだけど・・・わからない。ま、とりあえず、.NET 5をダウンロードして、インストーラーに従って入れてみる。

コマンドラインdotnetコマンド(これがnpmとかcargoみたいなものらしい)を実行してみよう。

> dotnet --info
.NET SDK (global.json を反映):
 Version:   5.0.102
 Commit:    71365b4d42

ランタイム環境:
 OS Name:     Mac OS X
 OS Version:  10.15
 OS Platform: Darwin
 RID:         osx.10.15-x64
 Base Path:   /usr/local/share/dotnet/sdk/5.0.102/

Host (useful for support):
  Version: 5.0.2
  Commit:  cb5f173b96

.NET SDKs installed:
  5.0.102 [/usr/local/share/dotnet/sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.App 5.0.2 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 5.0.2 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]

To install additional .NET runtimes or SDKs:
  https://aka.ms/dotnet-download

ほほぅ。

さて、察するにdotnetがcargoみたいなものだとしたら、dotnet newとかするとアプリのテンプレートを作ってくれたりするに違いない。--helpオプションを付けてヘルプを参照して、こんな感じかなって実行してみた。

> dotnet new console -lang F#
The template "Console Application" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on /Users/tambara/study/fs_study/fs_study.fsproj...
  復元対象のプロジェクトを決定しています...
  /Users/tambara/study/fs_study/fs_study.fsproj を復元しました (3.58 sec)。
Restore succeeded.

> ls
Program.fs       fs_study.fsproj  obj/

なんか出来たぞ?

では、このディレクトリをVSCで開いてみよう。EXTENSIONSのRECOMMENDEDにIonide-fsharpというのが上がってくる。これをインストールすればいいらしい。とりあえず、シンタックスハイライトは出来ているし、これでいいのかな。

当然、runも出来るのだろう。

> dotnet run
Hello world from F#

これでとりあえずの準備は良さそうだ。

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>>を使う。