「「サマータイム導入はコンピュータシステム的に難あり」は本当か (1/2)」は本当か

「「サマータイム導入はコンピュータシステム的に難あり」は本当か」という刺激的なエントリーが某界隈を賑わしていますが、技術者以外の人は信じてしまいそうな恐れがあるので、検証してみます。

「導入すべき理由について考えてみる」について考えてみる

サマータイムとは季節により大きく異なる日の出日の入りの時刻に、一定期間の時間をずらして揃えることで、日の当たる時間を有効活用しようとするものです。」と辞書的な意味を述べるだけで、
結局なにも主張していないので、とくに理由はないようです。
つまりは、7月22日から8月9日と言う猛暑でのオリンピックの競技日程および競技時刻とは何の関係もないということです。*1
次に、「コンピュータシステムについて」ですが、「混乱しない」と言い切っています。
これについては別途まとめて指摘します。

「これに触れる前に今回の「サマータイム」についての根本的な問題を指摘しておきます」で根本的な問題を指摘してみる

なぜ7月や8月なのかは、言及されている通りで引用すると、

アメリカ国内のテレビ放送におけるゴールデンタイムにあわせてのことです。莫大な「放映権料」を支払うアメリカの放送局の都合ということ。
また、かつての東京五輪は真夏の猛暑を避けて10月開催でしたが、これまた視聴率を稼げるアメリカのメジャースポーツや、プロサッカーのオフシーズンを避けると7月、8月の開催しかないという台所事情。

いずれにせよアスリートファーストどころかテレビ局ファーストです。このアスリートファーストとは、夜も昼もないブラック企業が、福利厚生としてマッサージ器を設置する程度の話しです。

さて、すでにその競技日程および時刻が決まってしまったわけで、これをもとにIOCは各国のテレビ局に放映権を販売することが基本的なビジネスモデルです。当然花形競技(国によって違うでしょうが)はお高い値段になるでしょう。

枠を買った後でサマータイム導入で競技時刻がずれるなんてことがあったら大問題です。マラソンなんて始まったと思ったら、ゴール直前ですよ。オリンピックが国内の都合だけで調整できる時代はとっくに終わっているのです(それは上記の引用文にもあるとおりでブログ著者も認識していると思われます)。

つまり「サマータイム導入」なる言葉は欺瞞であり、官公庁の目論見と、サマータイムファーストの銭ゲバ共の、ゲスな欲望を糊塗する詭弁に過ぎないということです。

「コンピュータシステムについて」について

ここからは技術的に看過できない記述が続きます。

コンピュータ(システムやプログラム)には「時間経過」の概念がありません。命令を受けた瞬間からの経過時間は、秒単位でカウントアップしていくだけで、つまり「その瞬間」しかコンピュータは認識していません。

今時の計算機は秒単位でカウントアップなんてしていません。内臓のクロックはナノ秒以上の精度でカウントアップしています。CPU自身は時刻を「認識」はしていませんが、プログラムでは時刻情報を元に計算の分岐はしています。そうであるからこそ2000年問題2038年問題が騒がれたのですし、数年に一度挿入/削除される「うるう秒」で世界的に障害が起きたりしているのです。*2

これをプログラミングにより擬似的に、時間経過の概念があるように見せかけているのが、エアコンなどの「タイマー機能」です。

これはその通りですが、時刻情報を扱っているものもあればないものもあります。毎日決まった時刻にエアコンが起動して、決まった時刻に停止するようなタイマーもありますよね。これらのタイマーが具体的にどういう実装になっているかはそれぞれでしょうが、ほとんど人が設定を変えるまでは自動的にサマータイムになるわけではないため、たしかに影響は少ないといえます。

ホストコンピュータなどと接続していて、連続した情報をやりとりしているシステムなら、バチっと電源を落として、その後の立ち上げで日時の変更をすればよいだけのこと。

このあたりから怪しくなってきます。連続した情報をやり取りしているシステムでバチっと電源をとしていいとする発想が理解不能ですよね。

サマータイムとなり自然時間の定義が変われば、コンピュータも同時に変えなければと「思いこみ」ますが、コンピュータが処理に用いているのは、埋め込まれたクォーツが刻む「内部時間」だけです。

これも部分的に正しいのですが、コンピュータでほんものの「内部時間」を生で使うことはほとんどなく、日付、時刻情報を永続化させるために、UTC的なものを用います。売上日などをデータベースに特定のコンピュータの起動日からの「内部時間」として格納してもバチっと電源落としてしまったら、あとでいつの情報かわかりません。
したがって、UTC(協定世界時)やJST(日本標準時)を格納します。
そしてUTCから日本標準時へはタイムゾーン情報(ハードコードかもしれませんが)を使って変換するのです。
現在主に使われているtzdataとよばれるこのタイムゾーン情報はうるう秒や夏時間およびそれらの変遷をすべて包含したもので、暦の歴史ともいえるものです。
このデータはOSのアップデートなどでひっそり更新されています。

なにより、今の時点からでも即座に対応できる程度のことです。2018年8月8日の読売新聞は「元号の変更とあわせてシステム改修が間に合わない」と書いていましたが、あまりにも無知。

突然元号の話にもなっていますが、元号についてはtzdataではなくOSや言語ランタイムの地域情報になるので全く別の話。それも行儀よく使っていればいいのですが、Javaが広く使われるようになったのが平成になってからであることを考えるとあまりにも無知。

コンピュータの内部は西暦で処理されていますが、元号表記が必要な場合、「1989年1月8日からは平成」と変換しているだけのことです。それが次は「2019年5月1日からは○○」と新元号の処理を一行加えるだけです。

それをどこで加えるかが問題で、行儀よく作られているなら、calendars.propertiesなどで定義すればよいでしょうが、そうでない場合にはプログラムの数だけやらないといけないので大変だというのが「システム改修が間に合わない」の論旨なのに。

サマータイムも同じく、「サマータイム」のフラグが立っていれば、サマータイムの開始日時を参照し、その切り換え日ならば、必要な時間調整をするというだけのもの。プログラミング初心者でもできる程度の処理です。

サマータイムのフラグはどうやって立てるのでしょうね。tzdataしかないでしょうが、個別でやるなら大変なことになります。サマータイムは一度始めたら影響は永遠に残るし、ルールは変わるのです。

たとえばアメリカは2007年からは3月8日以降の日曜日の午前2:00から、11月の第一日曜日2:00までが夏時間になります。その前は1987年からは4月の第一日曜日2:00からでした。
サマータイムを導入するということは、そういうことを未来永久に記録することになります。そのため、サマータイム導入済みの国では廃止論も出ています。*3
日本でも1948年5月第1週の日曜日2:00から1時間、9月第二週の日曜日2:00までサマータイムを実施しています。翌年の1949年には4月第一週の日曜日になり、1950年と1951年は5月第1週日曜日にもどっています。そしてそのまま1951年9月第二週日曜日で終了となりました。

すべての情報に時刻がついてまわり、それが延々と蓄積され続けている現代の社会では、そのような恣意的な暦の操作はもはや現実的ではありません。それがヨーロッパでの廃止論になっています。

一方で、ここがサマータイム導入の議論が日本で難しいところに直結します。この記事を書いた記者のように、コンピュータへの無理解と、同時にコンピュータへの過大な期待が、日本社会の一部にはあるからです。

つまり、アスリートファーストなどの詭弁では無く「不便なのは開始と終了の二日だけ。あとは慣れようよ」と、開き直ったアナウンスを政府がしてしまえば、勤勉でいてイベント好きな日本人なら、勝手に盛り上げてアジャストすることでしょう。

過去にサマータイムを導入した日本では、 制度が定着しなかったこと廃止の理由ですが、現在も健康面への影響などが懸念されているので、日本人ファースト的な考え方なら、導入しないほうがよいでしょう。この記事を書いた人は誰ファーストなのだろうか。
*4

「平成サマータイム議論」の推進は、冒頭にも記述し、この記事を書いた人も指摘しているように、オリンピックは関係ないという前提なら、全く必要のないことばかりか、社会混乱を招き、国民の健康を損なう天下の悪法を推進することになるのです。

さらに、月替わりではなく「週末」や、やたらと増えた「三連休」をサマータイムの切り換え日に当てれば、寝不足や時間調整のイベント化だってできます。

そういうのであれば、サマータイムなどで公的に導入するのではなく、勝手にシフト勤務なり営業すれば良いでしょう。自分が平気なら人も平気というのはあまりにも無神経な考え方と言えます。
変更したいなら、それなりの根拠を丁寧に説明する必要があるのに、それを怠り、「できるからやるのだ」というのは、暴論もいいところです。

その他の影響

日本標準電波(JST=UTC+9)を受信して時刻調整をしている時計は、調整をオフにしない限り、サマータイムに対応できません。標準電波のフォーマットでは予備ビットがあり、それによって夏時間の有無を定義するという仕様も予備ビットの拡張例として記載されていますが、何時間ずらすかは記載がありません*5。今回2時間ずらすので、拡張例を実装していても無理。カシオのG-Shockも日本時間だけ合わないという切ないことに。
テレビやビデオのチューナーもずれるでしょうね。まぁ今時テレビなんかみないから関係ないかな。

まとめ

炎上を狙って自分のサイトへの流入量を増やしたいとかそういう下世話なことを勘ぐってしまいます。
だって、得する人がいないんだもの。いや、いるなら教えて欲しい。
SIer元号対応やみずほ統合作業中で空前の人不足でこんなことやってる場合じゃない。
オリンピック目的は、IOCから怒られるだけだろうし。
政府は、歴史に名を残せるというのはあるでしょうけど、これは良い意味じゃないと思うんだけどいいのかな。

[perl]Spreadsheet::XLSXのeast asia localな事情について

とある事情からperlでXLSXファイルを読まなければならなくなり、cpanから
それっぽいSpreadsheet::XLSXをインストールしてテストしてみた。

|佐藤|
|鈴木|
|田代|

これをSpreadsheet::XLSXでcsvに変換したいわけだが、どういうわけか、

佐藤サトウ
鈴木スズキ
田代タシロ

となってしまうというのが今回の話。

XLSXあるいはOpen Office XMLあるいはecma-376の亡霊

こういう時はgoogle先生の出番なので調べると、というタグに
読み仮名があり、それをレンダリングするためにこうなっているらしく、
いろんな人がタグの部分を無視するようなパッチを作っていた。
これはこれで素晴らしいのだが、これでいいのだろうか? rPhは本当に
無視してしまっていいのだろうか? と思い元の規格であるecma-376
読んでみた。
18.4.6と18.4.3にrPhとphoneticProについて以下のように書かれている。

18.4.6 rPh (Phonetic hint)

This element represents a run of text which displays a phonetic hint
for this String Item (si). Phonetic hints are used to give
information about the pronunciation of an East Asian language. The
hints are displayed as text within the spreadsheet cells across the
top portion of the cell.

18.4.3 phoneticPr (Phonetic Properties)

This element represents a collection of phonetic properties that
affect the display of phonetic text for this String Item (si).
Phonetic text is used to give hints as to the pronunciation of an East
Asian language, and the hints are displayed as text within the
spreadsheet cells across the top portion of the cell. Since the
phonetic hints are text, every phonetic hint is expressed as a
phonetic run (rPh), and these properties specify how to display that
phonetic run.

つまり東アジアの言語の発音のヒントとして使われるためのものらしい。
実際、ExcelではIM入力したものは、読み仮名でソートされるが、他所から
コピぺで貼り付けたものは読み仮名情報が欠落するので文字コード順の
ソートになる。Excelきもいよ。

ということは、無下に捨てるわけにもいかないな。

いつもの変態ハック

ということで、

phonetic hint support cf ecma-376 Part1 18.4.6 rPh, 18.4.3 phoneticPr by k1complete · Pull Request #1 · k1complete/Spreadsheet-XLSX · GitHub

に勝手にプルリクを作ってみた。

これは、セルA1に'課きく 毛こ'を入れた場合(なおこれは、ecma-376で
exampleとして挙げられている由緒正しい例です)、

$cells->[0][0]->{Val}; # =>'課きく 毛こ'
$cells->[0][0]->{Rph}->[0]->{Val}; # => 'キク'
$cells->[0][0]->{Rph}->[0]->{Sb}; # => '0'
$cells->[0][0]->{Rph}->[0]->{Eb}; # => '1'
$cells->[0][0]->{Rph}->[1]->{Val}; # => 'コ'
$cells->[0][0]->{Rph}->[0]->{Sb}; # => '4'
$cells->[0][0]->{Rph}->[0]->{Eb}; # => '5'
$cells->[0][0]->{PhoneticPr}->{FontId}; # => 1

のように取り出せるものです。Sb, Ebは読み仮名に対応する文字列の開始と終了の0始まりのインデックスです。始まりは'=<'、終わりは'<'な感じのインデックスです。
ValはSpreadsheet::XLSXの値取り出しのキーとして使われているので、それを踏襲していますが、そのほかのハッシュのキーはecma-376の属性名やタグ名の先頭文字を大文字化したものを使っています。なお、PhoneticPrにはtypeという属性も定義されていて、デフォルトはfullwidthKatakanaです。

結論

なお、LibreOfficeで開いてからOpen Office XML形式で閉じると、rPhやphoneticPrといった、これらの情報は捨てられます。極東アジアでしか使われていないからね。先輩曰く「east asisaの人間は本当に捨ててもいいのか? おまえら一体どうしたいんだ? ということをwest europaの人にわかってもらわないとマージしてくれないよ」とのことで、格調高く作ってみたものの、やっぱり捨ててもいいんじゃないかと。

実行時にマクロ展開を行う

この記事は #サッポロビーム #137 でやったことの記事です。

クリス・デイトのリレーショナル言語「tutorialD」が気に入ったので、処理系を作ろうと思っていた。
erlangのmnesiaを基盤として、とりあえずできたのだが、何をするにもフルスキャン必須で、実用にならないため、実装方法を再検討することにした。qlcは、リスト内包表記のコンパイル時に、使える場合はインデックスを使うようにクエリを最適化してくれるので、その機能を使いたい。

例:

 {table2, key, mark, value}

というタプルの場合で、value列にはindexを作成していると仮定して、valueを検索したいとしよう。タプルXのvalue要素はelement(4, X)で取り出せるので、インデックスを使って欲しい場合、qlcのクエリとしては

  qlc:q("[ X || X <- Q, element(4, X) =:= Y]", 
        [{Q, mnesia:table(table2)}, {Y, x}])

のようにするとよい。しかしこの、element(4,X)の4というのはコンパイル時に決まっている必要があるが、データベースのスキーマコンパイル時には予見できない。
でもプログラミング時にはvalueという名前で4番目の要素を指定したい。

こんな時にはマクロでなんとかできないかと考えるわけだが、今回は実行時に解決しなければならない。ということで、実行時にASTを展開して、一旦文字列化してからqlc:string_to_handle/3 を呼ぶ方針にする。

もう一つの問題

relvar |> where(value == 4 and mark == :atom1) 

などと書くと、実行時に (value == 4 and mark == :atom1)を書き換えて、

    :qlc.string_to_handle("[ X || X <- Q, element(4, X) =:= Y, element(3, X) =:= Z].",
         [{Q, mnesia:table(relvar.table()), {Y, 4}, {Z, :atom}])

のようにして欲しいわけだが以下の問題がある。

  • =:=/2という中置演算子はelixirにはないのでMacro.to_string/2では :"=:="(a, b)となってしまう。
  • 同様に,/2や;/2も,(a, b), ;(a, b)となってしまう。

実はMacro.to_string/2の第2引数を使うことで、文字列化の制御が可能だ。

   def fmt(ast, x) do
    case ast do
      {:"==", m, [a, b]} ->
        Macro.to_string(a, &fmt/2) <> " =:= " <> Macro.to_string(b, &fmt/2)
      {:and, m, [a, b]} ->
        Macro.to_string(a, &fmt/2) <> ", " <> Macro.to_string(b, &fmt/2)
      {:or, m, [a, b]} ->
        Macro.to_string(a, &fmt/2) <> "; " <> Macro.to_string(b, &fmt/2)
      _ ->
        x
    end
  end

このようにすることで、必要な演算子の文字列化を制御できる。書き換えポイントで再帰的に
Macro.to_string/2を呼び出しているので、それまで構築していたものは捨てられてしまうが
しょうがない。

最後の問題

上記では、where句の引数は実行時まで評価されないため、elixirの変数を含んだ式を記述できない。
これではいくら何でも使いにくすぎる。そのため、

   a = 4
   b = :atom1
   relvar |> where ([a: a, b: b], value == a and mark == b)

のようにして変数の束縛を指定することで、コンパイル時に

   a = 4
   b = :atom1
   relvar |> where (quote bind_quoted: [a: a, b: b], do: value == a and mark == b)

のように変換し、実行時(つまりマクロ展開時)にa, bの値を決められるようにしておけばいい。
つまり「マクロを生成するマクロ」を作り、実行時に生成されたマクロを展開したうえでqlcが求めている形式にて:qlc.string_to_handle/3で最適化されたクエリを構築することになる。

elixir-v1.1.0リリース

elixirのv1.1.0がリリースされました。パチパチ。MapSetなど重要な機能が追加されています。Macro周りではimportでの名前の衝突を検出して、勝手に乗っ取ることがしずらくなっています。

Mm.exというファイルを作ってみます。

defmodule Mm do
  def h do
    IO.puts "override\n"
  end
end

これをコンパイルしてimport Mmすると昔ならIExのhコマンドを乗っ取ることができましたが、....

bash-3.2$ iex 
Eshell V6.3  (abort with ^G)
Interactive Elixir (1.1.0-beta) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> c("Mm.ex")
[Mm]
iex(2)> import Mm
nil
iex(3)> h
** (CompileError) iex:3: function h/0 imported from both Mm and IEx.Helpers, call is ambiguous
    (elixir) src/elixir_dispatch.erl:111: :elixir_dispatch.expand_import/6
    (elixir) src/elixir_dispatch.erl:82: :elixir_dispatch.dispatch_import/5
iex(3)>

MmとIEx.Helpersとでh/0が不明瞭であると怒られてしまいます。
これを回避(?)したい場合、import でexceptオプションを使い、IEx.Helpers.h/0を除外しておきます。

iex(2)> import Mm
nil
iex(3)> import IEx.Helpers, except: [h: 0]
nil
iex(4)> h
override

:ok
iex(5)> 

これと同じことをExgettext.Helperで行うと、hコマンドを乗っ取って、日本語リソースにアクセスできるようになります。

$ iex -S mix
...
iex(4)> import IEx.Helpers, except: [h: 0, h: 1]; import Exgettext.Helper
nil
iex(5)> h Enum.reverse/1

                            def reverse(collection) 

コレクションを逆順にします。

例

┃ iex> Enum.reverse([1, 2, 3]) 
┃ [3, 2, 1]

iex(6)> 

https://github.com/k1complete/l10n_elixir
https://github.com/k1complete/l10n_ex_unit
https://github.com/k1complete/l10n_iex

に言語リソースのソースを置きました。今回からex_docにパッチを当てなくても、上記のプロジェクトをcloneしてmix docsでドキュメント生成まではできるようになりました。ただし、elixir本体のドキュメント生成では、「動作中のelixir」から(ex_docが)ドキュメントを取得するため、deps/elixir/binにPATHを設定する必要があります。

http://ns.maqcsa.org/elixir/docs/v1.1.0/elixir/
http://ns.maqcsa.org/elixir/docs/v1.1.0/iex/
http://ns.maqcsa.org/elixir/docs/v1.1.0/ex_unit/

にex_docで生成したリファレンスを置きました。

enjoy!

elixir v1.1.0-betaに対応したリファレンス作った

最近盛り上がってきているelixirですが、v1.1.0-beta記念に、久しぶりに
リファレンスの翻訳を再開してみた。
いつものように elixir, iex, ex_unit だけですが、

http://ns.maqcsa.org/elixir/docs/v1.1.0-beta/

に置いてみました。これまで同様、やる気のないトップページにめげずに各リンクをクリックするとリファレンスが読めるようになります。もとのドキュメントがこの単位なので、翻訳もこの単位です(言い訳)。
なお、これまで関数のドキュメントは@doc、モジュールドキュメントは@moduledocでしたが、@typeのや@callbackにも@docをつけることでそれぞれのドキュメントになるようになっているようでした。

そのため基盤のexgettextも修正しなければならなくなったが、まぁしょうがない。以外にもex_docはほとんど修正が必要なかった。

それにしても、GNU gettextの仕組みはさすがに素晴らしくて、差分を適切にマーキングしてくれるので、fuzzyとuntranslatedのものだけを見ていけばいいw。

とか言っているうちに、v1.1.0-rc.0が出ている!
そして、本家からgettextが出てるみたいなのです。

QLC for elixir

QLCとはerlangのリスト内包表記を使ったクエリ書式であり、リストやEts, Dets, Mnesiaなどに対応している。今回はこれをelixirで使えるようにしようと思う。

qlcの使い方

基本的には、erlangなので変数は大文字で、

[Expression || Qualifier1, Qualifier2, ...]

Expression :: 任意のErlang式(template)

Qualifier :: Filter or Generators

Fiilter :: bool値を返すErlangGenerator :: Pattern <- ListExpression

ListExpression :: Qlc_handle or list()

わかりにくいかもしれないので、例を見てみる。

1> QH = qlc:q([{X,Y} || X <- [a,b], Y <- [1,2]]),
qlc:eval(QH).
[{a,1},{a,2},{b,1},{b,2}]

[a, b]と[1,2]の直積を計算している。
数学の集合の定義のように記述できるのが魅力である。
さて、これをelixirで実現するのだが、幸いqlc:string_to_handle/3というものがあるのでこれを使ってもよい。マニュアルにも、
'This function is probably useful mostly when called from outside of Erlang, for instance from a driver written in C.'
のように書いてある。しかし、erlangと同じbeam上に構築されたelixirがCと同様の土俵まで降りていくのは残念である。

:erl_scan.string/1, :erl_parse.parse_exprs/1

erlangには当然だが、スキャナとパーサが内蔵されている。これを使ってマクロにすることで、elixirのコンパイル時に文字列をerlang式としてパースすることができる。
elixirとちがいerlangの文字列は文字リストであることに注意すると、

  def exprs(s) do
    c = String.to_char_list(s)
    {:ok, m, _} = :erl_scan.string(c)
    {:ok, [expr]} = :erl_parse.parse_exprs(m)
    expr
  end

で文字列をErlangのASTにすることができる。これを実行時に:qlc.transform_expression/2で変数をバインドすることでqlc_handleを作ることができる。

Macro.escape/1 の使い所

通常マクロはelixir ASTをハンドリングするのだが、今回はErlang ASTをhandleするので、ただのタプルとして取り扱って欲しい。そのためにMacro.escape/1
を使う。

quote はbind_quotedする

マクロ中ではいつものquoteを使うのだが、引数をバインドするためには unquoteよりもbind_quoted: [keyword]を使うのが主流だ。

    quote bind_quoted: [exprl: exprl, bindings: bindings, opt: opt] do
      Qlc.expr_to_handle(exprl, bindings, opt)
    end

unquoteがないため実に見やすい。それだけではなく、評価の回数が保証されるため安全である。

これらを合わせてQlcマクロを作成すると、このようにかけることになる。

      iex> require Qlc                                                          
      iex> list = [a: 1,b: 2,c: 3]                                              
      iex> qlc_handle = Qlc.q("[X || X = {K,V} <- L, K =/= Item]",              
      ...>        [L: list, Item: :b])                                          
      ...> Qlc.e(qlc_handle)                                                    
      [a: 1, c: 3]                                                              
      ...> Qlc.q("[X || X = {K, V} <- L, K =:= Item]",                          
      ...>       [L: qlc_handle, Item: :c]) |>                                  
      ...> Qlc.e                                                                
      [c: 3]                                                                    

ソースはqlcに置いてみた。

アナフォリックマクロを作ってみる

Elixir Advent Calendar 2014 16日目。

またマクロのネタを。
On Lispでは、Common LISPの力の例として、アナフォリックマクロが紹介されている。elixirでも、アナフォリックマクロを作ってみよう。

アナフォリックマクロとは

アナフォラ(anaphora)というのは、既出の語をさす事で代名詞の事。アナフォリックマクロというのは、(決まった)代名詞(つまりアナフォラ)で既に評価された値を参照して利用できるマクロである。
文章で書くと分かりにくいが、sedでいうと、fooをfoobarに置換するときのs/foo/&bar/における'&'だし、オブジェクト指向的言語()のselfやthisなどもそうだ。perlはアナフォラが活躍しており、$_は至る所で様々なものを参照することになっている*1

初めてのアナフォラ

さて、どんなものをアナフォラにすると嬉しいだろうか。elixirはnilとfalse以外は真偽値としてはtrue扱いになるため、ifでの条件値をdoやelseでitとかいう変数名で参照出来ると嬉しいかもしれない*2。名前はanaphoric ifの頭文字でaifとする*3。つまり、こんな感じにつかえることになる。

iex(6)> it =2
2
iex(7)> aif (length [1,2,3,4]) do
...(7)>   IO.inspect it  ## <== ここで itという変数でaifの条件値を参照
...(7)> end
4
4
iex(8)> it ## itの値は外のコンテキストに影響与えない
               ## 健全(Hygiene)ですね!!
2

次はこういうマクロを作ってみよう。

マクロの設計

形は決まったので、いつものように、どう展開してほしいかを検討してみる。下記の例をどう展開してほしいかというと、

aif (length [1,2,3,4]) do
  IO.inspect it
end

これを、

if (it = (length [1,2,3,4])) do
  IO.inspect it
end

としてほしい。
なので、Aifというモジュールにaifマクロを記述できるような気がするのでやってみる。

defmodule Aif do
  defmacro aif(e, block) do
    quote do
      if (it = unquote(e), block)
    end
  end
end

ちょっとわざとらしいが、コンパイルして使ってみよう。

iex(1)> c("lib/aif.ex")
[Aif]
iex(2)> import Aif
nil
iex(3)> aif (length [1,2,3,4]) do
...(3)>   IO.inspect it
...(3)> end
** (RuntimeError) undefined function: it/0
    
iex(3)> 

怒られた。実は、Elixirのマクロは健全(Hygiene)なので、マクロで導入した変数は外部のコンテキストへ影響を与えることは出来ない。blockの中身のコンテキストはマクロの外側なので、IO.inspect itは外部のコンテキストのitを探しにいってしまっていたのだ。
この場合、解決策は3つある。

  1. quote中でvar!/1を使い、外部コンテキストの変数とする。この場合、itの値が外側に漏れだしてしまう。以下で説明する。
  2. block中の変数を強制的にマクロの内部のコンテキストにする。Elixirではこれで行ってみよう。
  3. aifを無名関数とその実行(ようはクロージャ)として実装する。これでもかまわないが、遅いのと、block中で他の変数の(再)束縛ができなくなってしまう。これはかなり痛いので却下。

まず1は、

defmodule Aif do
  defmacro aif(e, block) do
    quote do
      if (var!(it) = unquote(e), block)
    end
  end
end

とすればいい。これだとitは呼び出し側コンテキストの変数として扱われる。

iex(5)> it = 0
0
iex(6)> aif (length [1,2,3,4]) do
...(6)>   IO.inspect it
...(6)> end
4
4
iex(7)> it
4

aifの実行後、0だったitが4に再束縛されていることが分かる。これでは怖くて使えない。そこで2の方法を使う。

コードウォーク

block中の変数(のコンテキスト)を書き換えるためには、コードウォークが必要だが、Elixirには幸いにもMacro.prewalk/2があるのでこれを使ってみよう。今回は、itという変数 {:it, META, CONTEXT} (ただしCONTEXTはアトム)について、{:it, META, Aif} (Aifはaifが定義されているモジュール)としてAifコンテキストに指定してみる。

defmodule Aif do
  defmacro aif(e, block) do
    b2 = Macro.prewalk(block,
                       fn(x) ->
                         case x do
                           {:it, m, ctx} when is_atom(ctx) ->
                             {:it, m, Aif}
                           x ->
                             x
                         end
                       end)
    quote do
      if(it = unquote(e), unquote(b2))
    end
  end
end

これでコンパイル、実行してみよう。

iex(5)> c("lib/aif.ex")
lib/aif.ex:1: warning: redefining module Aif
[Aif]
iex(6)> import Aif
nil
iex(7)> it = 0
0
iex(8)> aif (length [1,2,3,4]) do
...(8)>   IO.inspect it
...(8)> end
4
4
iex(9)> it
0
iex(10)> 

うまくいった。aifブロックの内部のみitがlength [1,2,3,4]に束縛されていることが分かる。しかし、aifをネストするとどうだろう。

iex(10)> aif (length [1,2,3,4]) do
...(10)>   aif (length [1,2,3]) do
...(10)>     IO.inspect [it: it]
...(10)>   end
...(10)>   IO.inspect [it4: it]
...(10)> end
[it: 4]
[it4: 4]
[it4: 4]

最初のaifの展開時に、内側のaifに属するitも展開してしまっているため、itが4になってしまっている。これを防ぐためには、コードウォークを工夫する必要がある。自分でコードウォークを書く方法もあるが、今回は、コードウォーク中にaif/2を見つけたら、Macro.expand_once/2を呼び出して、そこの部分から強制的にマクロ展開してしまうという方法を使おう。この方法は、マクロ定義の中で自分自身の展開を呼び出すというちょっと不思議なことになる。

defmodule Aif do
  defmacro aif(e, block) do
    b2 = Macro.prewalk(block,
                       fn(x) ->
                         case x do
                           {:aif, m, ctx} when is_list(ctx) ->
                             Macro.expand_once({:aif, m, ctx}, __ENV__)
                           {:it, m, ctx} when is_atom(ctx) ->
                             {:it, m, Aif}
                           x ->
                             x
                         end
                       end)
    quote do
      if(it = unquote(e), unquote(b2))
    end
  end
end

さてこれを使ってみよう。

iex(26)> aif (length [1,2,3,4]) do
...(26)>   aif (length [1,2,3]) do
...(26)>     IO.inspect [it: it]
...(26)>   end
...(26)>   IO.inspect [it: it]
...(26)> end
[it: 3]
[it: 4]
[it: 4]

うまく捕捉できているようだ。
このマクロを使っての制限は、通常の変数としてitを使う事が出来なくなる点である。aifの内部で束縛され、外部に漏れる事はない。どうしても、外部へ持っていきたいときは、doブロック中でvar!/1を使う。

iex(31)> it = 0
0
iex(32)> aif (length [1,2,3,4]) do
...(32)>   var!(it) = it
...(32)> end
4
iex(33)> it
4
iex(34)> 

参照透明性?

アナフォラは参照透明性を侵している。しかし、__ENV__や__MODULE__、__CALLER__はどうだろう。便利に使っているが、これも現在のコンテキストを参照できる、一種のアナフォラである。アナフォラは参照透明性を侵しているため嫌う人もいるが、アナフォラそのものが悪ではなく、混乱が悪なのである。参照透明性は、混乱を防ぐためのポリシーに過ぎない。そもそも、オブジェクトが不変であるElixirやErlangにとっては、変数そのものも値につけたラベルにすぎず、アナフォラのようなものだ。
aifの場合、ただ一つの変数を作成するだけであり、その変数の作成こそがaifマクロの存在理由であるため、混乱することはないだろう*4

ということで、アナフォリックマクロを作る話でした。
明日は、@Joe-nohさんです!。

*1:省略される事も多いが

*2:もっともelseで参照してもnilかfalseなのであまり嬉しくないかもしれない

*3:On Lispに倣ったというのが本当のところ

*4:この言い回しはOn Lispの受け売りです