一筋縄ではいかない話題
この記事は英語の原文を日本語に翻訳したものです。原文: https://adoptingerlang.org/docs/development/hard_to_get_right/
翻訳元: adoptingerlang/adoptingerlang 2025-12-18(コミット 899008f)
ここまで読み進めれば、Erlangプロジェクトをどう構成すべきかはひと通り理解できているはずです。言語の基礎を解説するガイドと合わせれば、開発に着手するための準備はほぼ整っています。ただし、Erlangのドキュメントのどこにもうまくまとまっていない、いくつかの厄介な話題が残っています。本章では、Unicodeの扱い、時刻の扱い、SSL/TLS設定という3つのテーマについて指針を示します。
これらはそれぞれが単独でも複雑なテーマです。各テーマの背景知識は紹介しますが、この章を読んだだけですぐに専門家になれるわけではありません。それでも、どれほどの複雑さが存在するかを知っておくだけで、大きな失敗を避けやすくなります。
Unicodeの扱い
Erlangは文字列の扱いに関して芳しくない評判を持っています。その大きな理由は、専用の文字列型を持たないことと、長年コミュニティ製のライブラリ以外にまともなUnicodeサポートがなかったことです。専用の文字列型がない点は今も変わっていませんが、この設計には利点もあります。そしてUnicodeサポートの不足については、近年のErlangリリースでようやく解消されました。
Unicodeの基礎知識
Unicodeを一言で言えば、利用者の言語(プログラミング言語ではなく、実際に話される言語)を問わず、コンピュータ上でテキストをどう扱うかを定めた標準規格の集まりです。あらゆる細部にまで踏み込んだ、途方もなく複雑な巨大仕様へと成長しており、開発者が迷子になるのも無理はありません。
Unicodeのすべてを知らなくても、明らかな失敗を避けて効果的に使うための知識は身につけられます。まずはいくつかの用語を紹介します。
- 文字:Unicodeにおける「文字」という言葉の定義は、かなり曖昧です。「文字」という言葉を見かけたら、話し手はアルファベットの一文字から、絵文字のような図形、アクセント記号や文字修飾(
¸とcを組み合わせたçなど)、「バックスペース」のような制御シーケンスまで、何でも指しうる非常に抽象的な語として使っていると考えてください。テキストの断片を指す一般的だが不正確な言い方に過ぎず、あまり深い意味を持たせるべきではありません。Unicode自体は、もっと厳密で正確な定義を用意しています。 - コードポイント:Unicode標準は、基本となりうるすべての「文字」を巨大な一覧として定義しており(それ以上のものも含みます)、それぞれに一意の識別子を割り当てています。この識別子がコードポイントであり、
U+<16進数>という形式で表記されるのが一般的です。たとえば「M」のコードポイントはU+004D、♻のコードポイントはU+267Bです。全一覧はUnicode Tableで確認できます。 - エンコーディング:コードポイントは単なる整数であり、検索に使えるインデックスに過ぎません。それだけではプログラミング言語でテキストを表現するには不十分です。歴史的には、多くのシステムやプログラミング言語が、ある言語で有効なすべての文字をバイト(
0〜255)で表現してきました。それ以上の文字数が必要になれば、別の言語に切り替えるしかありませんでした。あらゆる種類のシステムと互換性を持たせるため、Unicodeはエンコーディングを定義し、コードポイントの並びをさまざまな方式で表現できるようにしました。もっとも一般的なのがUTF-8で、すべてをバイトで表現します。UTF-8の構造はASCIIやLatin-1と基本的に同じであるため、ラテン語系やゲルマン語系の言語で非常に広く使われるようになりました。UTF-16とUTF-32はその代替であり、より幅の広い単位(16ビットまたは32ビット)で表現します。 - コードユニット:あるコードポイントを特定のエンコーディングでどう表現するかを指定する単位です。UTF-8では、1つのコードポイントが1〜4個のコードユニットになります。たとえば「F」はUTF-8では
46という1つのコードユニットで表現され、UTF-16では0046、UTF-32では00000046になります。一方「©」のコードポイントはU+00A9ですが、UTF-8では2つのコードユニット(C2とA9)、UTF-16とUTF-32ではそれぞれ1つのコードユニット(00A9と000000A9)で表現されます。 - グリフ:文字の図形的な表現です。たとえば
U+2126は「オーム記号」で「Ω」と表示され、U+03A9は「ギリシャ文字の大文字オメガ」で、こちらも見た目のよく似た「Ω」として表示されます。書体によっては両者が同じ形になることもあれば、ならないこともあります。同様に、文字「a」は「а」や「α」のような形のグリフで表現されうる場合もあります。コードポイントの中にはグリフを持たないもの(「バックスペース」など)もあれば、複数のコードポイントを一度に表す合字(aeに対するæなど)として使われるグリフもあります。 - 書記素クラスタ:ここまでの用語はどれも、かなり抽象的な概念を扱っています。Unicodeには結合文字のような仕組みがあり、複数のコードポイントを1つの「文字」にまとめる方法が用意されています。これがひどくややこしいのは、利用者が考える「文字」とプログラマが考える「文字」が一致しないためです。書記素クラスタとは、「利用者が1つの文字として知覚するテキストの単位」を指す言葉です。たとえば「ï」という文字は、ラテン小文字の
i(U+0069)と、結合分音記号の「¨」(U+0308)という2つのコードポイントから成り立っています。プログラマから見れば、これはUTF-8で3コードユニットにエンコードされた2つのコードポイントに見えます。しかし利用者からすれば、バックスペースを1回押せば分音記号と「i」の両方が消えることを期待するはずです。
ずいぶん多くの用語が出てきましたが、どれも知っておく価値があります。プログラマがどう文字を書くかと、それが利用者にどう表示されるかのあいだには、直接の対応関係がないのです。
とりわけおもしろい例が、単一のコードポイント(U+FDFD)でありながら「﷽」として表示されるアラビア語のリガチャ「ビスミッラー・アッラフマーン・アッラヒーム」です。これは現時点でUnicode標準に含まれる最も幅の広い「文字」で、アラビア語の文全体を1文字として表現します。この文字が標準に加わった理由は、複数のウルドゥー語文書で法的に必要とされる一方、そのキーボードレイアウトではアラビア語を直接入力できなかったためです。UIを作る人たちを困らせるにはうってつけのUnicodeの一例です。
ほとんどの言語で、図形的(そして論理的)な表現が、最終的な文字を構成するコードそのものと一致しないという問題を抱えています。これはさまざまな言語の合字や「文字の部品」の組み合わせに見られる現象ですが、絵文字の世界では、個々の人物を組み合わせて家族を作ることもできます。「👩👩👦👦」は、👩 + 👩 + 👦 + 👦という4つの構成要素からなる家族で、ここでの+は2人の女性と2人の男の子のあいだをつなぐ特殊な結合マーク(ゼロ幅接合子)です(古いブラウザや古いフォント、あるいは本書のPDF版で見ている場合は、家族ではなく4人がばらばらに見えるかもしれません)。このシーケンスをバイト単位やコードポイント単位で処理してしまうと、家族はばらばらになり、テキストの意味が変わってしまいます。
ロケールや言語ごとの細かなルールへの対応が伝統的に手厚いテキストエディタ(たとえば、言語が求める半角スペースや改行禁止スペースの自動処理をきちんとこなす数少ないソフトの1つであるMicrosoft Word)で編集する場合、「👩👩👦👦」に対してバックスペースを押すと、家族全体が1つの単位として消えます。一方FirefoxやChromeでこの「1文字」を消すには、7回バックスペースを押す必要があります。「人物」ごとに1回、ゼロ幅接合子ごとに1回という計算です。Slackはこれを単一の文字として扱いますし、Visual Studio Codeも(どちらもElectronアプリであるにもかかわらず)ブラウザと同じ挙動をします。反対にnotepad.exeや多くのターミナルエミュレータは、ゼロ幅接合子を暗黙に無視して4人分に展開してしまいます。
つまり、どのプログラミング言語を使っていようと、文字列を配列のように扱い、位置やインデックスで「文字」を取り出せると考えていると、深刻な問題を抱え込むことになります。
さらに厄介なことに、一部の「文字」はUnicode上で複数の表現方法を許容しています。文字「é」は、単一のコードポイント(U+00E9)としても、文字e(U+0065)に結合アクセント記号「´」(U+0301)を続けた形としても作れます。フランス語では論理的にはどちらも同じ文字「é」ですが、この2つの表現方式で作った文字列同士は等しいとは判定されません。そこでUnicodeは正規化という概念を導入し、NFC・NFD・NFKC・NFKDという4つの標準のいずれかに文字列の表現を揃える方法を定めています(どれを使うべきか迷ったら、NFCを選んでおけば間違いありません)。
文字列のソートにも照合順序という概念があり、ソート対象がどの言語かを知っている必要があります。
要するに、どのプログラミング言語を使うにせよ、プログラムでUnicodeをきちんと扱うには、文字列をUnicode対応のライブラリを通じてのみ操作する不透明なデータ型として扱う必要があります。それ以外の方法を取れば、実際に操作しているのはバイト列やコードポイント列であり、人間が読むテキストのレベルで思わぬ壊れ方をすることになります。
Erlangにおける文字列の扱い
Erlangの文字列サポートは、一見すると少し風変わりに見えます。専用の文字列型が存在しないためです。しかしUnicodeの複雑さを踏まえると、これはそれほど悪い話ではありません。あり得る表現方式の多さを考えれば、文字列型を1つだけ持つのも、まったく持たないのも、扱いにくさという点では大差ないのです。
複数のエンコーディングに対応した可変な文字列型を持つプログラミング言語を使っている人は、今の説明を読んで安心しているかもしれません。しかしErlangも全体としてはかなりまともなUnicodeサポートを備えており、欠けているのは照合順序くらいのものです。
データ型
Erlangでは、文字列について次のようなエンコーディングがあり得ることを把握しておく必要があります。
"abcdef":Unicodeのコードポイントをそのままリストにした文字列です。Erlangシェルで[16#1f914]と書けば、エンコーディングを意識せずそのまま"🤔"という文字列が得られます。実体は単方向連結リストです。<<"abcdef">>:<<$a, $b, $c, $d, $e, $f>>の省略形であるバイナリ文字列です。これはLatin-1の整数値の並びをバイナリに変換した古くからの標準的な形式です。この記法はデフォルトではUnicodeエンコーディングをサポートしておらず、ソースファイルに<<"🤔">>のように大きすぎる値(16#1f914など)を書くと、オーバーフローが起きて最終的には<<20>>というバイナリになってしまいます。これはErlangのバイナリ(実質的には不変のバイト配列)として実装されており、テキストに限らずあらゆる種類のバイナリデータを扱うためのものです。<<"abcdef"/utf8>>:UTF-8でエンコードされたバイナリのUnicode文字列です。絵文字にも対応できます。実装としては依然としてErlangのバイナリですが、/utf8という指定によって正しくUnicodeエンコーディングされます。<<"🤔"/utf8>>は<<240,159,164,148>>となり、これがUTF-8で表した絵文字を表す正しいバイト列です。<<"abcdef"/utf16>>:UTF-16でエンコードされたバイナリ文字列です。<<"🤔"/utf16>>は<<216,62,221,20>>になります。<<"abcdef"/utf32>>:UTF-32でエンコードされたバイナリ文字列です。<<"🤔"/utf32>>は<<0,1,249,20>>になります。["abcdef", <<"abcdef"/utf8>>]:複数の文字列形式を組み合わせられる、IoDataと呼ばれる特殊なリストです。リストの要素は通常どおりコードポイントでも構いませんが、含まれるバイナリはすべて同じエンコーディング(理想を言えばUTF-8)にそろえ、エンコーディングが混在する問題を避けるべきです。
Unicodeを含むコンテンツを扱うには、Erlangが提供する文字列関連の各モジュールを使うことになります。
1つ目はstringモジュールです。大文字小文字の区別や正規化を踏まえて文字列を比較するequal/2-4、部分文字列を検索するfind/2-3、書記素クラスタの個数を数えるlength/1、パターンで文字列を分割するlexemes/2、文字列を少しずつ取り出すnext_codepoint/1とnext_grapheme/1、置換を行うreplace/3-4、文字列を書記素クラスタのリストに変換するto_graphemes/1、そして大文字・小文字を扱うlowercase/1、uppercase/1、titlecase/1といった関数を備えています。ほかにもまだ多くの機能がありますが、代表的なものは以上です。
文字列形式やエンコーディング、正規化形式のあいだのあらゆる変換には、unicodeモジュールも使うことになります。正規表現モジュールのreはUnicodeを問題なく扱え(オプションのリストにunicodeアトムを渡すだけです)、ucpオプションを渡せばGeneric Character Typesも使えます。最後に、fileモジュールとioモジュールも、それぞれ専用のオプションでUnicodeにきちんと対応しています。
これらのモジュールはいずれも、バイナリ、整数のリスト、混在した表現といったあらゆる形式の文字列に対して動作します。文字列の扱いをこれらのモジュールに任せている限り、まず問題は起きません。
一つだけ忘れてはならない厄介な点があります。文字列のエンコーディングは暗黙的だということです。文字列がシステムに入ってくる時点で、そのエンコーディングを把握しておく必要があります。HTTPリクエストはたいていヘッダでエンコーディングを指定しますし、XMLも同様です。JSONやYAMLはUTF-8の使用を義務づけています。SQLデータベースを扱う場合、テーブルごとに独自のエンコーディングを指定できますが、データベースへの接続自体にもエンコーディングがあります。どこか1か所でも食い違えば、データは壊れます。
だからこそ、エンコーディングはできるだけ早い段階で特定し、きちんと追跡し続ける必要があります。これは言語にどのデータ型があるかという話にとどまらず、システム全体をどう設計し、ネットワーク越しのデータのやり取りをどう扱うかという話でもあります。
文字列についてもう1つ、効率よく変換する方法を見ておきましょう。
IoData
では、どの文字列型を使うべきでしょうか。選択肢は豊富にありますが、単純に決められるものではありません。
簡単な指針は次のとおりです。
- 用途の大半はUTF-8のバイナリで済ませる
- UTF-16やUTF-32を使うならバイナリを使う
- リストとしての文字列は実務ではあまり使われないが、コードポイント単位で操作したいときには非常に有効
- それ以外、とくに文字列を組み立てる用途にはIoDataを使う
バイナリ型の利点の1つは、部分スライスを効率よく作れることです。たとえば<<"hello there, Erlang!">>というバイナリのかたまりがあるとして、<<Txt:11/binary, _/binary>>のようなパターンマッチで部分スライスを取り出すと、Txtは元のバイナリと同じメモリ位置を指す<<"hello there">>になります。ただし、そこから元の文脈をプログラム的に取得する手段はなく、元の内容の一部を指す境界付きの参照になっているだけです。リストは再帰的に定義されているため、こうはいきません。
さらに、64バイトを超えるバイナリはプロセスのヒープをまたいで共有できるため、他のデータ構造で発生するようなコピーコストを払わずに、仮想マシン内で文字列の内容を安く移動させられます。
警告: バイナリ共有はプログラムの性能を上げる有効な手段になることが多いものです。しかし、一部の病的な使用パターンでは、バイナリ共有がメモリリークを引き起こすことがあります。詳しく知りたい場合は、Erlang in Angerのメモリリークに関する章、とくに7.2節を参照してください。
本当におもしろいのは、リストとバイナリを組み合わせたIoData表現です。これによって、不変な文字列を非常に安く組み立てられます。
Greetings = <<"Good Morning">>,
Name = "James",
[Greetings, ", ", Name, $!]
最終的なデータ構造は[<<"Good Morning">>, ", ", "James", 33]のようになり、バイナリの断片、コードポイントのリテラル、文字列、あるいは他のIoData構造が混在したリストになります。しかしVMの仕組みはこれをあたかも1つのフラットなバイナリ文字列であるかのように扱えます。ネットワークやディスクアクセスを含むIOの仕組みも、前の節で挙げた各モジュールも、この文字列をGood Morning, James!としてシームレスに扱い、Unicodeにもきちんと対応します。
つまり、文字列そのものを書き換えることはできませんが、もとの型が何であれ、定数時間で連結やマッチングができます。これは文字列を扱うライブラリを書く際に興味深い意味を持ちます。たとえば&をすべて&に置換したいとして、<<"https://example.org/?abc=def&ghi=jkl"/utf8>>から出発するなら、次のような連結リストを返すだけで済ませられます。
% リスト
[%% 元のURLの変更されていない部分のスライス
<<"https://example.org/?abc=def"/utf8>>,
%% 置換後の内容のリテラルリスト
"&amp",
%% 残りの部分スライス
<<"ghi=jkl"/utf8>>
]
これは実質的に3要素の連結リストであり、元の文字列の一部、置換後の内容、元の文字列の残りの部分から成り立っています。150MBをメモリ上に確保するような文書を相手に、置換箇所がまばらであれば、ほぼオーバーヘッドなしに全体を組み立てて編集できるということです。これはなかなか優秀です。
IoData文字列がおもしろい理由は他にもあります。その1つがUnicode表現です。先に述べたとおり、書記素クラスタは、プログラマだけが気にするバイナリ列としてではなく人間が扱うのと同じ感覚で文字列を操作したいときに重要な意味を持ちます。文字列をフラットなバイト配列で表現するプログラミング言語の多くには、文字列をうまく反復処理する手段がありませんが、Erlangのstringモジュールではstring:to_graphemes(String)を呼び出すことで書記素クラスタを扱えます。
erl +pc latin1 # Unicode解釈を無効化
1> [Grapheme | Rest] = string:next_grapheme(<<"ß↑õ"/utf8>>),
[223 | <<226,134,145,111,204,131>>]
2> string:to_graphemes("ß↑õ"),
[223,8593,[111,771]]
3> string:to_graphemes(<<"ß↑õ"/utf8>>),
[223,8593,[111,771]]
これによって、任意のUnicode文字列を、lists:map/2やリスト内包表記、パターンマッチで安全に反復処理できるリストに変換できます。これはIoDataを介してはじめて可能になることで、単なるデフォルトのUTF-8バイナリ文字列よりも扱いやすい形式だとさえ言えます。
ただし、ここでもパターンマッチにはまだ危うさが残ります。複数の方法でエンコードされうる文字を統一された表現に揃えるため、あらかじめ正規化を1回かけておくのが理想です。
ここまでで、Erlangの文字列にまつわる謎はだいぶ解消されたはずです。
時刻の扱い
時間は、生きるぶんには何でもない存在なのに、説明しようとすると途方もなく難しいものです。哲学者や科学者たちが何世紀もかけて議論を重ね、ようやく大まかな合意にたどり着いた話題であり、ソフトウェアの世界では「1970年1月1日からの経過秒数を数えれば十分」ということにしました。しかし実際には、それほど単純ではありません。暦の規則や変換、タイムゾーン、うるう秒といった話まで踏み込むと一般論として大きくなりすぎるため、ここでは深入りしません。ただし、壁時計時刻と単調時刻の重要な違い、そしてErlang仮想マシンがこの問題にどう役立つかは押さえておきます。
時刻に関する基礎知識
かなり単純化して言えば、時刻の計測には主に2つの用途があり、両者はしばしばまったく異なるものとして扱われます。
- 2つの出来事のあいだの経過時間、つまり「どれくらい時間がかかったか」を知ること。マイクロ秒や時間、年といった単位の単一の値が必要になります。
- ある出来事を時間軸上に配置し、「いつ起きたか」を特定できるようにすること。通常は日時によって表されます。
どちらも時間に関わる話ですが、計測の仕方はまったく異なります。
結論だけ言えば、Erlangでは経過時間の計算にerlang:monotonic_time/0-1を、システム時刻(UNIXタイムスタンプなど)にはerlang:system_time/0-1を使うべきです。その理由を知りたければ、この先を読み進めてください。
日時は通常、暦の日付(読者の多くはグレゴリオ暦を使っているはずです)に時・分・秒の値、タイムゾーン、場合によってはミリ秒やマイクロ秒といったより高い精度の値を組み合わせたものです。この値は人間が理解できる必要があり、ある期間が何を意味するかについて人々が合意した社会制度に完全に根ざしています。現在のグレゴリオ暦が導入されたのは1582年であり、それ以前の日付は厳密にはグレゴリオ暦のもとで起きたわけではない(ユリウス暦やマヤ暦のもとで過ごされていた)にもかかわらず、私たちは誰しも「西暦1000年」が時間軸上の特定の一点を指すことを知っています。この概念全体は、最終的には地球の公転や平均太陽日のような天文現象に結びついています。
一方、とくにコンピュータにおける時間の間隔は、数えられる周期的な出来事に基づいています。水時計や砂時計であれば、水滴や砂粒が落ちる一定の速さがあり、それが尽きたときに一定の時間が経過したことになります。現代のコンピュータはクロック信号や水晶発振子、あるいはもっと凝ったものなら原子時計といった仕組みを使います。こうした周期的で数えられる出来事を太陽日のような概念と同期させることで、計測した周期をより大きな基準点に合わせ、経過時間の計測と時刻の特定という2つの時間の扱い方をすり合わせられます。
私たち人間は、この2つの概念を同じ「時間」という値の2つの側面として捉えがちです。しかしコンピュータは、両方を同時にうまくこなすのがあまり得意ではありません。経過時間と絶対的な時刻は別物であり、別々に扱うべきものとして区別しておくと、実際にかなり助かります。
一例を挙げると、多くのプログラマはUnix時間(1970年1月1日からの経過秒数)を知っていますし、標準としてのUTCも知っています。しかし、UTCはうるう秒を扱うのに対しUnixタイムスタンプは扱わないため、両者の変換が実は厄介だと知っている開発者はあまりいません。この違いのせいで、奇妙で笑えないような問題が起きることがあります。両者を区別せずに使うと、ソフトウェアに気付きにくいバグが混入します。
とりわけ厄介なのは、コンピュータのクロックが長期間にわたって正確であり続けるのがあまり得意ではないという点です。これはクロックドリフトとして知られています。この結果、数分や数時間といった短い間隔ではかなり良い精度が得られる一方、数週間、数か月、数年という単位ではクロックは大きくずれていきます。コンピュータのクロックの周波数は多少変動するものの、全体としてはそれほど大きくは変わりません。短い時間の計算にはまったく問題ありませんが、より長い期間を追跡するには不十分です。そこで、ネットワーク越しにはるかに正確な(そして高価な)クロックとコンピュータのクロックを再同期するために、NTPのようなプロトコルが必要になります。
つまり、コンピュータの時刻が何の前触れもなく飛ぶことがあるのは、想定しておくべき事態だということです。運用担当者がタイムゾーンやシステム時刻をいじった場合はなおさらです。
Erlangにおける時刻の扱い
コンピュータが時刻を扱う仕組みは、先ほど述べたクロック、つまりハードウェアやOSに応じてマイクロ秒やミリ秒を単純に数え上げていくクロックに基づいています。これを人間にとって意味のある単位で表現するには、エポック(任意の起点)からある時刻標準(UTC)への変換を行います。コンピュータのクロックは時間とともにずれていくため、常に増加し続けるローカルクロックを人間にとって意味のある値へと補正するためのオフセットの値が保持されています。これは通常すべて隠蔽されており、どこを見ればよいか知らない限り、こうした処理が行われていることには気付きません。
Erlangのランタイムシステムも同種の仕組みを使っていますが、それを明示的に公開している点が異なります。公開されているクロックは2種類です。
- 単調クロック:常に増加する値を返すだけのカウンタ(同じマイクロ秒内に呼び出した場合は直前と同じ値を返します)です。高い精度を持ち、経過時間の計算に向いています。
- システムクロック:利用者が人間として気にする時刻を公開します。多くはUnixのPOSIX時間(1970年1月1日からの経過秒数)を使って実現されており、世界中のコンピュータで広く使われているため、他のあらゆる時刻形式への変換ライブラリも豊富に存在します。人間にとっての時刻表現における、いわば最大公約数的な存在です。
システム上のクロック全体をまとめると、次の図のようになります。

相対論的な効果を無視すれば、ほぼ一定だとみなせる実際の体感時間が存在し、コンピュータのクロックはそれに多少なりとも追従します。電圧、温度、湿度、ハードウェアの品質は、いずれもその信頼性に影響を与えます。Erlang VMは独自の単調クロックを提供しており、これはハードウェアクロック(存在する場合)と同期しつつ、後ほど説明するいくつかの追加の制御も可能にしています。
システム時刻は、その内部にある単調クロックからの一定のオフセットとして常に計算されます。目的は、ハードウェアクロックの任意個のティックを1970年からの経過秒数に変換し、そこからさらに他の形式へ変換できるようにすることです。
オフセットが常に0であれば、VMの単調時刻とシステム時刻は一致します。オフセットを正または負の方向に変更すると、Erlangの単調時刻を独立させたまま、Erlangのシステム時刻をOSのシステム時刻に合わせられます。実際には、単調クロックが大きな負の数になっている一方で、システムクロックがオフセットによって補正され、正のPOSIXタイムスタンプを表しているということも起こり得ます。
つまりErlangでは、用途に応じて次の関数を使い分けることになります。
erlang:monotonic_time/0-1は、Erlangの単調時刻を返します。非常に小さな負の数を返すこともありますが、それ以上小さくなることはありません。T0 = erlang:monotonic_time(millisecond), do_something(), T1 = erlang:monotonic_time(millisecond)のように使い、T1 - T0を計算すれば処理全体の所要時間が得られます。比較する際は時間の単位をそろえる必要があります(後述の注記を参照)。erlang:system_time/0-1は、UNIXタイムスタンプが必要なときに使う、オフセット適用後のErlangシステム時刻です。erlang:time_offset/0-1は、Erlangの単調クロックとシステムクロックの差を調べるために使います。calendar:local_time/0は、システム時刻をOSの現在のクロック(つまり利用者の現在のタイムゾーンと夏時間)に変換し、{{Year, Month, Day}, {Hour, Minute, Second}}形式で返します。calendar:universal_time/0は、システム時刻をUTCの現在時刻に変換し、同じく{{Year, Month, Day}, {Hour, Minute, Second}}形式で返します。
ヒント:
erlangモジュールで時刻を扱う関数のほとんどは、second、millisecond、microsecond、nanosecond、nativeのいずれかを取るUnit引数を受け付けます。デフォルトで返されるタイムスタンプの型はnative形式です。この単位は実行時に決まり、単位間の変換にはerlang:convert_time_unit(Time, FromUnit, ToUnit)が使えます。たとえばerlang:convert_time_unit(1, seconds, native)は1000000000を返します。
calendarモジュールには他にも便利な関数があり、日付の妥当性検証、RFC3339形式の日時文字列(2018-02-01T16:17:58+01:00)との相互変換、時間差の計算、日数や週数への変換、うるう年の判定などができます。
もう1つの道具として、時刻オフセットが飛んだことを検知できる新しい種類のモニタがあります。erlang:monitor(time_offset, clock_service)として呼び出すと参照が返り、時刻がドリフトすると{'CHANGE', MonitorRef, time_offset, clock_service, NewTimeOffset}というメッセージを受け取れます。
タイムワープ
これまで紹介した関数を適切な文脈で使えば、時刻の扱いで問題が起きることはまずありません。あとは、ホストコンピュータがスリープから復帰して新しいクロックで動き出す、システム管理者が時刻をいじる、NTPがクロックを前後に強制的に補正する、といった奇妙なケースにどう対処するかだけが問題になります。これらを無視すると、システムはひどく奇妙な振る舞いをすることになります。
幸い、Erlang VMではあらかじめ用意された戦略から選べるようになっており、適切な関数を適切な場面で使う(経過時間やベンチマークには単調クロック、出来事の時刻特定にはシステム時刻)という原則さえ守っていれば、自分にとってより適切だと感じる選択肢を選ぶだけで済みます。用途に応じて適切な関数を選べば、コードはタイムワープに対して安全になります。
これらの選択肢は、erl実行ファイルに+C(ワープモード)と+c(時刻補正)というスイッチを渡すことで指定します。ワープモード(+C)は単調時刻とシステム時刻のあいだのオフセットをどう扱うかを、時刻補正(+c)はシステムクロックが変化したときにVMが公開する単調クロックをどう調整するかを、それぞれ定めます。
+C multi_time_warp +c true:時刻オフセットは制限なく随時変更でき、精度の良いシステム時刻を提供します。Erlangの単調クロックの周波数もVMによって可能な限り正確になるよう調整されます。今どきのプラットフォームであれば基本的にこれを指定すべきで、性能やスケーラビリティ、挙動のいずれの面でも優れる傾向があります。+C no_time_warp +c true:時刻オフセットはVM起動時に決まり、以後変更されません。代わりに単調クロックの速度を上げ下げすることで、時刻のドリフトをゆっくりと補正します。後方互換性のためにデフォルトのモードになっていますが、正しい時刻の使い方により合った別のモードを選ぶ方がよいこともあります。+C multi_time_warp +c false:時刻オフセットは随時変更できますが、Erlangの単調クロックの周波数は信頼できないことがあります。OSのシステム時刻が前方に飛べば単調クロックも前方に飛び、後方に飛べばErlangの単調クロックが一瞬止まることがあります。+C no_time_warp +c false:時刻オフセットはVM起動時に決まり、以後変更されません。単調クロックは停止したり、大きく前方に飛んだりすることが許容されます。一般にはこのモードを選ぶべきではありません。+C single_time_warp +c true:ErlangがOSクロックの同期前に起動することがわかっている組み込みハードウェア(NTPによる同期が行われる前にソフトウェアを起動する場合など)向けの特殊なハイブリッドモードです。VM起動時はErlangの単調クロックをできるだけ安定させ、システム時刻の調整は行いません。OSレベルでの時刻同期が完了した時点でerlang:system_flag(time_offset, finalize)を呼び出すと、ErlangのシステムクロックはOSのシステム時刻に合わせて一度だけワープし、以後はno_time_warpと同等のクロックになります。+C single_time_warp +c false:こちらも、ErlangがOSクロックの同期前に起動することがわかっている組み込みハードウェア向けの特殊なハイブリッドモードです。ErlangのシステムクロックをOSのシステム時刻に同期させようとはせず、OSのシステム時刻の変化はErlangの単調クロックに影響を与えることがあります。OSレベルでの時刻同期が完了した時点でerlang:system_flag(time_offset, finalize)を呼び出すと、ErlangのシステムクロックはOSのシステム時刻に合わせて一度だけワープし、以後はno_time_warpと同等のクロックになります。
基本的には常に+c true(デフォルト)を選び、デフォルトではない+C multi_time_warpを明示的に指定するべきです。クロックの周波数を補正していた古いErlangシステムを再現したいなら+C no_time_warpを選び、最初のクロック同期が大きく時刻を飛ばし、その後は安定することが見込める組み込みシステムで、かつmulti_time_warpを使いたくない(本来は使うべきですが)場合にはsingle_time_warpを検討してください。
要するに、可能な限り+C multi_time_warp +c trueを選んでください。正確に時刻を扱ううえで、現状もっとも優れた選択肢です。
SSL設定
TLSの基礎知識
(準備中)
ErlangにおけるTLSの扱い
(準備中)