c4se記:さっちゃんですよ☆

.。oO(さっちゃんですよヾ(〃l _ l)ノ゙☆)

.。oO(此のblogは、主に音樂考察Programming に分類されますよ。ヾ(〃l _ l)ノ゙♬♪♡)

音樂は SoundCloud に公開中です。

考察は現在は主に Scrapbox で公表中です。

Programming は GitHub で開發中です。

Clojerl で Erlang の pattern match を行ふ。guard を使ふ

Clojerl は Erlang VM (BEAM) で動く Clojure である。

Erlang/Elixir では pattern match を頻繁に使ふ。一つは條件分岐する爲。慣れてゐる Elixir で書こう。

defmodule Example do
  def example1(:a, v), do: v + 1
  def example1(_, v), do: v

  def example2(v) do
    case v do
      {:a, v} -> v + 1
      {_, v} -> v
    end
  end
end

example1 函數では第一引數が :a であれば加算しさうでなければ何もしない。example2 函數の樣に data の内部構造にも match 出來る。pattern match で條件分岐するのは主に tuple に對して效果が大きい。tuple は位置と意味を混淆 (complect) すると云ふ事で Clojure には無く、全くその通りなのだが Erlang では頻用するから pattern match をしたい。

Simple Made Easy

これなら迂遠ではあっても Clojure でも書ける。しかし次のは書けない。

defmodule Example do
  def example({:x, v, 42}, [%{x: v, y: y} | rest]), do: {y, rest}
end

一つ目の tuple の中の v と、二つ目の list の中の map の中の v とが同じ値でなければこれには match しない。この樣に Erlang の pattern match は非線形である。また變數だけでなく値も書ける。既に束縛 (a.k.a. 代入) 済みの變數を patter の中に書いた場合には再束縛はされず、その値の data にしか match しない。これを Clojure で書くのは不可能ではないものの、pattern match library を一通り書かなければならない。

pattern match は他にも返り値を檢査するのに使ふ。

{:ok, pid} = GenServer.start_link(Example, nil)

この場合 pid 變數に代入するのだが、:ignore{:error, reason} が返って來たら error として crush させたいのだ。これも Clojure で書くのは面倒だ。

pattern match は擴張出來ず再利用も出來ないと云ふ事で Clojure には無く、multi method や分配束縛を使ふのだが、Erlang/OTP には behaviour や recieve 等 pattern match を前提とした機能が頻發するので pattern match を書きたい。また先の缺點には Scala の unapply や Egison 等の例外が在る。

Why no pattern matching? clojure.org clojure.org

さて Clojerl で pattern match する方法だが document には無い。この document には何が有るんだ? 果たして GitHub repository の examples に在った。

github.com

fn*let* 特殊形式を使ふ。返り値の檢査は let* で行ふ。

(let* [#erl[:ok pid] (gen_server/start_link example nil)]
      pid)

束縛せず檢査だけするなら以下で好い。

(let* [#erl[:ok _] (gen_server/start_link example nil)])

fn* は無名函數を作るもので、callback として渡す事も在るが、主に def と組み合はせて使ふ。

(def example
  (fn* ([:a v]
        (+ v 1))
       ([_ v]
        v)))

case や recieve もそれぞれ case*recieve* 特殊形式として書ける。

(case* v
       #erl[:a v] (+ v 1)
       #erl[_ v] v)

(recieve*
  #erl[:ex1 v 42 #erl{:x v :y y}] v
  #erl[:ex2 message] message
  _ #erl[:error "Unknown message"]
  (after 1000 #erl[:error "Timeout"]))

函數定義で使ふ pattern match と似た機能に Erlang には guard が在る。引數が條件に當て嵌まる時だけ本體を實行させる。これも Clojerl の document には無く examples に在った。

github.com

fn* 特殊形式の機能である。

(fn* ([v] {:when (erlang/is_pid v)}
      :pid)
     ([v] {:when (erlang/is_integer v)}
      :integer))

Clojerl から Erlang を呼ぶ: How to interoperate with Erlang and Clojerl

何でこの document が無いんだ。無かったから書いた。英語にしたいが氣力が無い。

Clojure Advent Calendar 2020 が空いてゐたので參加しやう。

やってみる: Getting started

先づ rebar3_clojerl で project を作ろう。First, create a project by rebar3_clojerl.

rebar3 を install する。

github.com

$ sudo wget -O /usr/local/bin/rebar3 https://s3.amazonaws.com/rebar3/rebar3
$ sudo chmod +x /usr/local/bin/rebar3
$ rebar3 local install

Clojerl project の雛形を作る爲に global な rebar.config に rebar3_clojerl を追加する。

github.com

$ cat ~/.config/rebar3/rebar.config
{plugins, [rebar3_clojerl]}.

project を作る。

$ rebar3 new clojerl_app example
===> Writing example/src/example/core.clje
===> Writing example/src/example/app.clje
===> Writing example/src/example/sup.clje
===> Writing example/src/example.app.src
===> Writing example/test/example/core_test.clje
===> Writing example/rebar.config
===> Writing example/.gitignore
===> Writing example/LICENSE
===> Writing example/README.md
$ cd example

REPL を起動してみよう。雛形の rebar3_clojerl の ver. が古いので上げる。

diff --git a/rebar.config b/rebar.config
index f7132a2..62590c3 100644
--- a/rebar.config
+++ b/rebar.config
@@ -1,4 +1,4 @@
 {erl_opts, [debug_info]}.

 {deps, [{clojerl, "0.7.0"}]}.
-{plugins, [{rebar3_clojerl, "0.8.3"}]}.
+{plugins, [{rebar3_clojerl, "0.8.4"}]}.

そして起動する。

$ rebar3 clojerl repl
===> Fetching rebar3_clojerl v0.8.4
===> Analyzing applications...
===> Compiling rebar3_clojerl
===> Verifying dependencies...
===> Fetching clojerl v0.7.0
===> Analyzing applications...
===> Compiling clojerl
===> Analyzing applications...
===> Compiling example
===> Getting log of git repo failed in /Users/ne-sachirou/dev/example. Falling back to version 0.0.0
===> Clojerl Compiling clojerl
===> Compiling clojure/core.clje...
===> Compiling clojure/pprint.clje...
===> Compiling clojure/core/reducers.clje...
===> Compiling clojure/core/server.clje...
===> Compiling clojure/data.clje...
===> Compiling clojure/zip.clje...
===> Compiling clojure/repl.clje...
===> Compiling clojure/test.clje...
===> Compiling clojure/xml.clje...
===> Compiling erlang/core.clje...
===> Clojerl Compiling example
===> Compiling example/core.clje...
===> Compiling example/sup.clje...
===> Compiling example/app.clje...
=INFO REPORT==== 24-Dec-2020::00:17:04.695885 ===
    application: clojerl
    exited: stopped
    type: permanent

Clojure 0.7.0
clje.user=>

Clojure の函數を呼んでみよう。

clje.user=> (str "momonga" "MOMONGA")
"momongaMOMONGA"
clje.user=> (-> "momonga" (str "MOMONGA"))
"momongaMOMONGA"
clje.user=> (let [m1 "momonga" m2 "MOMONGA"] (str m1 m2))
"momongaMOMONGA"
clje.user=> (let [[m1 m2] ["momonga" "MOMONGA"]] (str m1 m2))
"momongaMOMONGA"
clje.user=> (let [{m1 :m1 m2 :m2} {:m1 "momonga" :m2 "MOMONGA"}] (str m1 m2))
"momongaMOMONGA"
clje.user=> (let [{:keys [m1 m2]} {:m1 "momonga" :m2 "MOMONGA"}] (str m1 m2))
"momongaMOMONGA"

Clojure の標準 library は入ってゐないが、基本的な函數は使へる。

いよいよ Erlang の函數を呼んでみよう。

clje.user=> (math/log2 65536)
16.0
clje.user=> (base64/encode "momonga")
"bW9tb25nYQ=="

OTP を Clojerl の標準 library として使へる訣だ。

Erlang の supervisor を使ってみよう。inets module を使って簡單な HTTP server を立てる。inets application が起動してゐる必要があるから src/example.app.src に書く。

diff --git a/src/example.app.src b/src/example.app.src
index 1b8fb83..678771e 100644
--- a/src/example.app.src
+++ b/src/example.app.src
@@ -6,6 +6,7 @@
     , [ stdlib
       , kernel
       , clojerl
+      , inets
       ]
     }
   , {mod, {'example.app', []}}

4000 番 port で待ち受け、priv/public を公開する HTTP server は Erlang ではこう書く。

PublicDir = filename:join(code:priv_dir(:example), "public"),
{ok, _} = inets:start(
  httpd,
  [
    {port, 4000},
    {server_name, "example"},
    {server_root, PublicDir},
    {document_root, PublicDir},
    {bind_address,"localhost"}
  ]
).

そのまま Clojerl に直すとこうだ。

(let [public-dir (binary/bin_to_list (filename/join (code/priv_dir :example) "public"))]
  (inets/start :httpd
               #erl(#erl[:port 4000]
                    #erl[:server_name #erl"example"]
                    #erl[:server_root public-dir]
                    #erl[:document_root public-dir]
                    #erl[:bind_address #erl"localhost"])))

これを supervisor にぶら下げよう。src/example/core.clje に實裝を書き、src/example/sup.clje に有る supervisor にぶら下げる。

diff --git a/priv/public/index.html b/priv/public/index.html
new file mode 100644
index 0000000..2e52729
--- /dev/null
+++ b/priv/public/index.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<title>Clojerl</title>
+<p>Clojerl</p>
diff --git a/src/example/core.clje b/src/example/core.clje
index a0c7d74..9dc114f 100644
--- a/src/example/core.clje
+++ b/src/example/core.clje
@@ -1 +1,17 @@
 (ns example.core)
+
+(defn start-link []
+  (gen_server/start_link #erl[:local :example.core]
+                         :example.core
+                         #erl()
+                         #erl()))
+
+(defn init [_]
+  (let [public-dir (binary/bin_to_list (filename/join (code/priv_dir :example) "public"))]
+    (inets/start :httpd
+                 #erl(#erl[:port 4000]
+                      #erl[:server_name #erl"example"]
+                      #erl[:server_root public-dir]
+                      #erl[:document_root public-dir]
+                      #erl[:bind_address #erl"localhost"])))
+  #erl[:ok nil])
diff --git a/src/example/sup.clje b/src/example/sup.clje
index 4c5f198..f8e58fd 100644
--- a/src/example/sup.clje
+++ b/src/example/sup.clje
@@ -4,7 +4,11 @@
                     :intensity 1
                     :period    5})

-(def child-specs #erl())
+(def child-specs #erl(#erl{:id :example.core
+                           :start #erl[:example.core
+                                       :start-link
+                                       #erl()]
+                           :restart :permanent}))

 (defn start-link []
   (supervisor/start_link #erl[:local :example.sup]

REPL を再起動し、application を呼び出さう。

clje.user=> (example.app/start :normal nil)

http://localhost:4000/index.html を見ると先の index.html の中身が見える筈だ。

Hex.pm の library も使へる。

Recon を入れて VM の樣子を覗いてみよう。Recon は Erlang 實行環境の樣子を安全に知る事のできる debug tool だ。Erlang VM を運用するなら是非入れておくといい。

ferd.github.io

diff --git a/rebar.config b/rebar.config
index 90fa363..e476f18 100644
--- a/rebar.config
+++ b/rebar.config
@@ -1,5 +1,6 @@
 {erl_opts, [debug_info]}.

-{deps, [{clojerl, "0.7.0"}]}.
+{deps, [{clojerl, "0.7.0"},
+        {recon, "2.5.1"}]}.
 {plugins, [{rebar3_clojerl, "0.8.4"}]}.

install する。

$ rebar3 upgrade
===> Analyzing applications...
===> Verifying dependencies...
===> Fetching recon v2.5.1
===> No upgrade needed for clojerl
===> No upgrade needed for recon

REPL を立ち上げて呼んでみよう。今囘は VM の樣子を覗いてみる。普通に呼ぶと Erlang の term が返ってくる。

clje.user=> (recon/node_stats_list 1 1000)
#erl(#erl[#erl(#erl[:process_count 81] #erl[:run_queue 0] #erl[:memory_total 63948120] #erl[:memory_procs 20665656] #erl[:memory_atoms 622772] #erl[:memory_bin 1882056] #erl[:memory_ets 6677800]) #erl(#erl[:bytes_in 0] #erl[:bytes_out 0] #erl[:gc_count 7] #erl[:gc_words_reclaimed 110416] #erl[:reductions 60219810] #erl[:scheduler_usage #erl(#erl[1 0.008229647869447678] #erl[2 0.0016643060189602482] #erl[3 0.0010973392240004188] #erl[4 0.0014953839650194726] #erl[5 0.0013679809490133251] #erl[6 0.0015487337521235827] #erl[7 0.0010311711991118494] #erl[8 0.0015200885780010886] #erl[9 0.0012850169590631723] #erl[10 0.00136601901759231] #erl[11 0.0014677412982497728] #erl[12 0.0011447580649143999] #erl[13 0.0013334281548910145] #erl[14 0.0013403316357774714] #erl[15 0.0013136567958034098] #erl[16 0.0012386041504103987] #erl[17 0.0] #erl[18 0.0] #erl[19 0.0] #erl[20 0.0] #erl[21 0.0] #erl[22 0.0] #erl[23 0.0] #erl[24 0.0] #erl[25 0.0] #erl[26 0.0] #erl[27 0.0] #erl[28 0.0] #erl[29 0.0] #erl[30 0.0] #erl[31 0.0] #erl[32 0.0])])])

erl->clj 函數で Clojure の data に變換できる。

clje.user=> (erl->clj (recon/node_stats_list 1 1000))
([([:process_count 81] [:run_queue 0] [:memory_total 63989136] [:memory_procs 20700568] [:memory_atoms 622926] [:memory_bin 1885320] [:memory_ets 6679160]) ([:bytes_in 0] [:bytes_out 0] [:gc_count 1] [:gc_words_reclaimed 27412] [:reductions 117717] [:scheduler_usage ([1 3.095390962856307e-5] [2 7.938116639390832e-4] [3 2.2965689260245194e-5] [4 2.7958342070315232e-5] [5 2.895673863248307e-5] [6 2.795823040377676e-5] [7 3.0954095077000806e-5] [8 3.794331269083989e-5] [9 2.9955276771779733e-5] [10 3.1952295223231713e-5] [11 3.794331269083989e-5] [12 3.594629623342726e-5] [13 3.095378599750572e-5] [14 3.09535696455317e-5] [15 3.095369327486081e-5] [16 3.295070574420667e-5] [17 0.0] [18 0.0] [19 0.0] [20 0.0] [21 0.0] [22 0.0] [23 0.0] [24 0.0] [25 0.0] [26 0.0] [27 0.0] [28 0.0] [29 0.0] [30 0.0] [31 0.0] [32 0.0])])])

pretty print するには OTP の io module を使ふ。

clje.user=> (io/fwrite "~120p~n" #erl((recon/node_stats_list 1 1000)))
[{[{process_count,81},
   {run_queue,0},
   {memory_total,64171600},
   {memory_procs,20845064},
   {memory_atoms,623317},
   {memory_bin,1903656},
   {memory_ets,6685032}],
  [{bytes_in,0},
   {bytes_out,0},
   {gc_count,1},
   {gc_words_reclaimed,25919},
   {reductions,120052},
   {scheduler_usage,[{1,5.4930677485013915e-5},
                     {2,9.128479858418676e-4},
                     {3,2.2971010584642182e-5},
                     {4,3.495588567228158e-5},
                     {5,3.2958406491008346e-5},
                     {6,2.696596894718865e-5},
                     {7,4.694076075992098e-5},
                     {8,2.896344812846188e-5},
                     {9,5.0935719122467445e-5},
                     {10,3.096058717252937e-5},
                     {11,2.5967151553285093e-5},
                     {12,5.1934354975311204e-5},
                     {13,6.491736020046481e-5},
                     {14,2.596697001414201e-5},
                     {15,2.896336134789491e-5},
                     {16,2.9962337341961154e-5},
                     {17,0.0},
                     {18,0.0},
                     {19,0.0},
                     {20,0.0},
                     {21,0.0},
                     {22,0.0},
                     {23,0.0},
                     {24,0.0},
                     {25,0.0},
                     {26,0.0},
                     {27,0.0},
                     {28,0.0},
                     {29,0.0},
                     {30,0.0},
                     {31,0.0},
                     {32,0.0}]}]}]
:ok

VM の樣子がよく解る。

Erlang との遣り取り

data 型

Erlang の函數に渡せる data は Clojerl では以下の樣に書く。

Erlang Clojerl Elixir
整數 -42 -42 -42
浮動小數点數 -42.29e3 -42.29e3 -42.29e3
atom atom or 'atom' :atom :atom
charlist "charlist" #erl"charlist" 'charlist'
binary <<"binary">> "binary" "binary" or <<"binary">>
list [Item, Item] #erl(item item) [item, item]
tuple {Element, Element} #erl[element element] {element, element}
map #{Key => Value} #erl{key value} %{key => value}

list は Clojure の list '(item item) に由來し、tuple は vector [item item] に、map は map {key value}#erl reader macro を附けたものである。

clj->erl 函數と erl->clj 函數で變換してもいい。

函數を data にするには arity を指定しなければならない。

clje.user=> math/log.1
#<math/log>
clje.user=> example.app/start.2
#<example.app/start>

data にした函數は高階函數に渡せる。

clje.user=> (lists/map (fn [x] (math/sin x)) #erl(1 2))
#erl(0.8414709848078965 0.9092974268256817)
clje.user=> (lists/map #(math/sin %) #erl(1 2))
#erl(0.8414709848078965 0.9092974268256817)
clje.user=> (lists/map math/sin.1 #erl(1 2))
#erl(0.8414709848078965 0.9092974268256817)

函數の呼び出し

函數の呼び出しは自然である。a.b module の f 函數は、

(a.b/f arg1 arg2)

で呼べる。Clojerl で定義した函數も Erlang で定義した函數も同じである。

project 管理

rebar3 を使ふといい。rebar3_clojerl を入れておけば便利な command を使へる。

github.com

$ rebar3 new --help
app (built-in): Complete OTP Application structure.
clojerl_app (plugin): Complete Clojerl application structure
clojerl_escript (plugin): Complete Clojerl escriptized application structure
clojerl_lib (plugin): Complete Clojerl library (no processes) structure
clojerl_release (plugin): Clojerl release structure for executable programs
cmake (built-in): Standalone Makefile for building C/C++ in c_src
escript (built-in): Complete escriptized application structure
lib (built-in): Complete OTP Library application (no processes) structure
plugin (built-in): Rebar3 plugin project structure
release (built-in): OTP Release structure for executable programs
umbrella (built-in): OTP structure for executable programs (alias of 'release' template)
$ rebar3 help clojerl

clojerl <task>:
  compile           Compile clojerl project
  escriptize        Generate escript archive.
  release           Build a release for the Clojerl project.
  repl              Start a clojerl repl
  run               Run the project's -main function.
  test              Test clojerl project

asdf を管理する Ansible module を書いた

書いたから何だと云ふ話ではあるが。

github.com

今迄 asdf の設定を Ansible で管理してあり、長く弄ってきたのが安定したから手順を module にした。publish する積もりは無い。

この module がするのは、

  • asdf の plugin を入れる
  • 指定した version の實行環境を入れる
  • global に有效な實行環境の version を設定する
  • 舉げられてゐない version の實行環境を消す

各實效環境に豫め入れておきたい library 等 (rbenv-default-gems の樣な役割) は他に御任せする。

復た Ansible module を書きたいと思ふ時の忘備であった。もっと昔に書いたものを探すのに手間取ったからそれも載せておく。

github.com

正字で組版する技術

遅れながらであるがはてなエンジニア Advent Calendar 2020 13 日目である。

近頃趣味を遂行するのに組版をする機會が度々有りその事を書く。例へば以下を發刊する爲等だ。

c4se.booth.pm

具体的な組版の手順は Adobe InDesign に流し込んで注意深く校訂するだけだから省く。自分向けの手順が以下に記してある。

scrapbox.io

さてこの組版では、できるだけ正字で組む事を拘はってゐる。その爲に行使する電算技術と、その技術の今後の展開がこの記事の内容となる。

正字とは

そもそも正字とは何かと云ふ說明をしなければならない。正字は一般に舊字・舊字體と呼ばれてゐるものに似るが、概念としては全く異なるものである。舊字とは昭和初期迄日本國で公式に用ゐられてゐた漢字字體である。今我々が用ゐてゐる漢字は、内閣から告示された常用漢字 (新字体) を代表とし、常用漢字に無い舊字體から受け継いだ字、常用漢字に有る字の舊字體、それに多種類に亙る非公式の字を合はせた集合である。この記事で使ってゐる字が概ね舊字體に近い。

正字の反對語は俗字である。誤字は單に使ふべきでない字を使ってゐる事を指すが、文脈に依っては俗字を含む廣い概念である。

或る字には幾つかの下位範疇が有る。先づ字種、これ等は同一の字であると云ふ集合が在る。次に字體、或る字種をどう云ふ筆劃で書くかを決めるもので、異體字 (e.g. 体は體の異體字) はこの範疇の區別である。次に字形、字體の筆劃を、拂ひや跳ね、長さや繋がり等筆法を決めるものである (e.g. 空󠄁と空。font に依っては區別が附かない)。次に書體、篆書・隷書・草書・行書・楷書の五體を代表とし、明朝體やゴシック體等或る字體をどう云ふ見た目で書くかを決めるものだ。最後に具體的な書影が在る。この順序では特に書體の位置附けは曖昧で、字種の次に置いてもよささうである。また字の地域差等、字種も嚴密に定めるのは文脈に依っては困難な事がある。正字は、或る字種の中で具體的な書影より上の抽象的な字體・字形・書體の中でどれが正であり殘りが俗であるかを決めるものだ。五體の中では隋以降は楷書が正字とされる。隋頃に正字として決められ以後通用した書體を楷書と呼ぶのである。

或る字種の中でどれが正字かを決めるやり方には三通り考へられる。

  1. 字源に依るもの。甲骨文や金石文を含む古い資料から字源を特定し、字源と楷書の對應から字形を決める。對應は字の歴史的變化に依存する。字源を調べるのが先づ難しい。字源と楷書の對應も自明ではない。對應規則適用の漏れや過剰適用、また正統な對應と俗な對應との區別を如何にするかが問題となる。
  2. 權威有る書籍に依るもの。康煕字典や說文解字に依る事が多い。何を權威と見做すか何を權威でないと見做すかに問題が有り、また權威同士で矛盾する事もある。
  3. 權威有る傳統に依るもの。權威有る書籍に漏れてゐる、またはそれに反しても傳統的に通用した字體や字形を正と見做す事がある。これも何を權威と見做すか、また權威同士の矛盾が問題となる。

三つの決め方のそれぞれに問題が有る事に加え、三つが互ひに矛盾する事もある。

例を舉げよう。

今「月」と書く部位は三つの字源が混同されてゐる。一つは天體の月、肉を表すにくづき、それに舟を表すふなづきである。この三つは字源も形も異なるが、よく似る爲混同もされてきた。「有」と云ふ字の月は說文解字では天體の月に由來すると考へられ、康煕字典もこれを踏襲してゐた。字體は有󠄁 󠄁である。しかし近年甲骨文資料等から、これはにくづきである事が判った。これに從ふならば字體は有󠄀である。どちらをとるか迷う事になる。

他にもと云ふ字は康煕字典にはこの形で載ってゐる。しかし說文解字にはの形で載ってゐる。これは說文解字より後に變化した字であり、どちらの權威をとるか迷う。

𠏹は地名の𠏹沢に使はれる字だ。これは佛の異體字であるから、基本的には佛に直されるべきものだ。しかし地名の𠏹沢を佛沢とするべきだらうか。この場合は固有名詞を例外とする事でよいかもしれないが、全ての固有名詞を例外とするべきであらうか。また𠏹の字が使はれてゐた文脈に於いて論じてゐる場合、佛に直せば誤りと成るかもしれない。

正字は、かう云ったややこしい例を研究しながら正字とは何かを考へ直して決めるものである。何かの database を參照すればよいものではない訣だ。

正字の概念は可能かと云ふ問題も有る。歴史的に正字概念が成立してきた事から見て、細部で大きく破綻しつつも大域的には成り立つのではないかと楽觀してゐる。

正字に關はる Unicode の知識

正字 - 俗字を電算機で處理する時普通は Unicode で扱ふ事になる。その時知っておくべき知識が在る。

先づは IVS (Ideographic Variation Sequence) の存在。漢字の codepoint に IVS と呼ぶ codepoint を續ける事で字形を變へられる。二つの codepoint で一つの字を表す仕組みは、繪文字を處理し慣れてゐる皆さんには親しい仕組みだらう。正字は基本的に IVS 附きで表す事になる。

次に CJK 互換漢字と SVS (Standardized Variants Sequence) の存在。今は CJK 互換漢字無しで漢字の處理をできる樣になってゐ、混亂の元だから避けるべきである。

最後に國別 font の事。例へば日本・中國・臺灣で使ふ字形は大きく異る。Unicode ではこれを區別せず、font 側で實裝する事になってゐる。同じ Adobe Source Han font を使ってゐても、國毎に異なる font file が用意されてゐる。

正字を一覧する data 構造

先づ字種の中でどれが正字で殘りが俗字かを一覧する必要がある。JSON schema で書くと以下の data 構造が在ればよいだらう。

segzi.yaml

type: array
items:
    type: object
    required: [segzi, konkyo, zokuzi]
    properties:
        segzi:
            description: |
                IVS が附けられるものには附ける。SVS は使はない
                IVS と 5 桁以上の Unicode の字を直接書くのは避け、U~と表記する
            type: string
            pattern: ".|(U\\+[0-9A-F]+(U\\+E01[0-9A-F]{2})?)"
        konkyo: {type: string}
        zokuzi:
            type: array
            items: {type: string, pattern: ".|(U\\+[0-9A-F]+(U\\+E01[0-9A-F]{2})?)"}
        CJK_unified_ideographs: {type: string, pattern: ".|(U\\+([0-9A-F]{4}))"}
        JIS_4th: {type: string, pattern: ".|(U\\+[0-9A-F]+)"}
        JIS_2nd: {type: string, pattern: ".|(U\\+[0-9A-F]+)"}

逆に一つの俗字に複數の正字が合流した場合も一覧しておくとよい。例へば弁には弁・辨・辯・瓣・辮が合流してゐる。

kakinahosi.yaml

type: array
items:
    type: object
    required: [zokuzi, segzis]
    properties:
        zokuzi:
            type: string
        segzis:
            type: array
            items:
                type: object
                required: [segzi, jogxafu]
                properties:
                    segzi: {type: string}
                    jogxafu: {type: string}

變換 program

Python で書くとこうなる。

"""Force 俗字 to 正字."""
import re
import typing as t


class Segzify(object):
    """Force 俗字 to 正字."""

    itaizi_selectors: t.List[str] = [
        "\U000E0100",
        "\U000E0101",
        "\U000E0102",
        "\U000E0103",
        "\U000E0104",
   ]

    table: t.Dict[str, str] = {
        "\u4E07": "\u842C",  # 万
        "\u4E08": "\u4E08\U000E0101",  # 丈
        # ここに俗字 -> 正字を一覧する
    }

    def force(self, content: str) -> str:
        """Force 俗字 to 正字."""
        for (zokuzi, segzi) in self.__class__.table.items():
            regex = "{}(?:[{}]?)".format(
                zokuzi,
                "".join(self.__class__.itaizi_selectors),
            )
            content = re.sub(regex, segzi, content)
        return content

    # __pragma__("skip")
    def force_file(self, filename: str) -> bool:
        """Force 俗字 to 正字 in the file. Return True if the file had some 俗字."""
        with open(filename, "r+") as f:
            original_content = content = f.read()
            content = self.force(content)
            if original_content == content:
                return False
            f.seek(0)
            f.truncate()
            f.write(content)
        return True

    # __pragma__("noskip")

Segzify().force("俗字") と呼ぶ。或る codepoint を IVS で區別したものは同じ字體であって、その中に正字は一つだけと前提してゐる。

識別子のローマ字は Segsyoxafu である。

segsyoxafu.wordpress.com

與太話として上記 code で Markdown を變換する作業をしてゐる時に Prettier が IVS を認識できない事が判った。以下の pull request で直してある。

github.com

Adobe InDesign 上での變換

ここ迄が既に實現してゐる事だ。今組版作業は、Markdown 上で俗字を直し、Adobe InDesign に地道に流し込んでゐる。流し込みが大變手間なので、正字の間違ひが見附かった時に直すのが難しい。InDesign 上で正字を直せればこの問題はなくなる。

InDesignJavaScript で擴張を書ける。これを利用する。

Python から JavaScript への transpile には Transcrypt を使ふ。因みに今の Transcrypt は壞れてゐ、Python 3.7.x で使はなければならない。

github.com

asdf local python 3.7.9
poetry init
poetry add -D Transcrypt
npm init
npm install --save-dev transcrypt-loader webpack webpack-cli

撰擇した範圍を正字に變換する擴張はこうなる。

import typing as t

from segzify import Segzify

alert: t.Any = 0  # __:skip


def do_wrok(text):
    text.contents = Segzify().force(text.contents)


def main():
    if len(app.documents) == 0:
        alert("Error: No document.")
        return
    if len(app.selection) == 0:
        alert("Error: No selection.")
        return
    if len(app.selection) != 1:
        alert("Error: Too many {0} selection.".format(len(app.selection)))
        return
    selection = app.selection[0]
    constructor = selection.constructor.name
    if constructor in [
        "Character",
        "InsertionPoint",
        "Line",
        "Paragraph",
        "Story",
        "Text",
        "TextColumn",
        "TextStyleRange",
        "Word",
   ]:
        do_wrok(selection)
    elif constructor == "TextFrame":
        do_wrok(selection.texts.item(0))
    else:
        alert("Error: The selected {0} is not a text.".format(constructor))


main()

太字や縱中橫等をうまく扱へないのが今の困りどころである。

これを JavaScript に transpile しよう。Webpack を設定する。

"use static";

module.exports = {
  entry: {
    InDesign: "./InDesign.py",
  },
  output: {
    filename: "dist/[name].jsx",
    path: __dirname,
  },
  module: {
    rules: [
      {
        test: /\.py$/,
        loader: "transcrypt-loader",
        options: {},
      },
    ],
  },
  target: "node",
};

poetry run npx webpack で transpile する。これを InDesign の擴張 directory に置けば使へる。

自宅の通信制限 (?) を Mackerel で可視化する

こんにちは。Mackerel Advent Calendar 2020 11 日目である。

最近は自宅で仕事をしてゐて、自宅の network が相當惡い事に氣附いた。頻繁に通信が途切れる時間帯が在る。特に夕方から夜更けにかけて、まるで通信制限でもかかってゐるかのうやうにずっと遲い。多くの Web site で畫像を讀み込めなく成る程に遲い。これに氣附いたのは歸宅作業で體力を使ひ果たさず遊ぶ樣に成ったからであらう。周りの家の方々も同じく夜に通信が増えたのかもしれない。歸宅と食事は疲れる。歸宅と食事を止めよう。

どれ程遲いか確認してみた。speedtest-cli を使ふ。

sivel/speedtest-cli: Command line interface for testing internet bandwidth using speedtest.net

brew install speedtest-cli
speedtest

殆ど通信が停まってゐる時間に實行すると、1 Mbps を下囘ってゐる事が判った。判ったところでどうしようもない。囘線契約を替へるか引っ越すかするだけである。それ等は今放っておくとして、囘線狀況を可視化しやうと思ふ。speedtest の結果を Mackerel に投稿しよう。多分既に一億人くらいがやってゐらっしゃると思ふ。

speedtest を mackerel-agent に叩かせる事を考へる。さうすると毎分數 10 Mb の通信が行はれ、これが囘線を圧迫してしまふ。そこで數 10 分に 1 囘、例へば 10 分毎に speedtest を實行する。mackerel-agent にはこの機能が無いから適當に wrap してやる。どうせ speedtest の出力をメトリック形式に整へたり MACKEREL_AGENT_PLUGIN_META を處理する爲に wrap はするのである。

#!/usr/bin/env ruby

require 'json'

# Metric for Mackerel.
class Metric
  def self.from_s(expression)
    name, value_s, time_s = expression.split("\t")
    new(name, value_s.to_f, Time.at(time_s.to_i))
  end

  attr_reader :name, :value, :time

  def initialize(name, value, time)
    @name = name
    @value = value
    @time = time
  end

  def to_s
    "#{@name}\t#{@value}\t#{@time.to_i}"
  end
end

# Runner for speedtest-cli.
class Speedtest
  def measure(time)
    out_r, out_w = IO.pipe
    err_r, err_w = IO.pipe
    pid = spawn('/usr/local/bin/speedtest --json --secure', out: out_w, err: err_w)
    Process.wait(pid)
    out_w.close
    err_w.close
    raise err_r.read unless $?.success?

    result = JSON.parse(out_r.read)
    [
      Metric.new('speedtest.speed.download', result['download'] / 8, time),
      Metric.new('speedtest.speed.upload', result['upload'] / 8, time),
      Metric.new('speedtest.ping.ping', result['ping'], time)
    ]
  end
end

# Runner for speedtest-cli. Cache the result.
class SpeedtestWithCache
  CACHE_FILE = '/tmp/mackerel-plugin-speedtest'.freeze

  def measure(time)
    metrics = Speedtest.new.measure(time)
    IO.write(CACHE_FILE, metrics.map(&:to_s).join("\n"), mode: 'w', encoding: Encoding::UTF_8)
    metrics
  end

  def prev_metrics
    return [] unless File.exist?(CACHE_FILE)

    IO.readlines(CACHE_FILE, mode: 'r', encoding: Encoding::UTF_8, chomp: true)
      .map { |line| Metric.from_s(line) }
  end
end

def print_meta
  puts '# mackerel-agent-plugin'
  puts({
    graphs: {
      'speedtest.speed': {
        label: 'Speedtest',
        unit: 'bytes',
        metrics: [
          { name: 'download', lebel: 'download' },
          { name: 'upload', lebel: 'upload' }
        ]
      },
      'speedtest.ping': {
        label: 'Speedtest Ping',
        unit: 'float',
        metrics: [
          { name: 'ping', lebel: 'ping' } # seconds
        ]
      }
    }
  }.to_json)
end

# Should run speedtest-cli once in a 10 minutes.
def should_measure?(current_time, prev_metrics)
  duration = 10
  prev_metrics.empty? ||
    prev_metrics[0].time + duration * 60 < current_time ||
    (
      prev_metrics[0].time + 1 * 60 > current_time &&
      (current_time.to_i / 60 % duration).zero?
    )
end

if ENV['MACKEREL_AGENT_PLUGIN_META'] == '1'
  print_meta
  exit 0
end
speedtest = SpeedtestWithCache.new
prev_metrics = speedtest.prev_metrics
time = Time.now
metrics =
  if should_measure?(time, speedtest.prev_metrics)
    speedtest.measure(time)
  else
    prev_metrics.map { |metric| Metric.new(metric.name, metric.value, time) }
  end
metrics.each { |metric| puts metric }

speedtest を實行したら結果を file に書き出し、10 分間は同じ値を投稿する。これを mackerel-agent で實行するやう設定する。

[plugin.metrics.speedtest]
command = "$HOME/.local/bin/mackerel-plugin-speedtest"
timeout_seconds = 50

default では command の實行に 30 秒以上かかると mackerel-agent は command が timeout したと見做す。普段は 20 秒程で完了し支障がないが、通信がほぼ停まってゐる時には 45 秒程かかるので timeout_seconds を 50 秒に延ばしてある。

こうするとグラフが見られる。

f:id:Kureduki_Maari:20201211122026p:plain

ひどい時にはほぼ 0 Mbps である事が見える。

アラートは仕掛けてゐない。通信が遲く成ってゐる事が判っても「遲く成ってるなぁ」と眺めるだけだからだ。即時に行動 (長期的傾向であれば ticket を登録する等を行ふ) を起こさないアラートを仕掛けてはいけない。

mac が sleep するとグラフが途切れる。また他のメトリックは投稿されるのに speedtest のグラフが途切れてゐると、50 秒でも timeout_seconds が足りなかった事も判る。

美しく夕方から夜更け迄通信が遲くなってゐる樣子はこれだ。

f:id:Kureduki_Maari:20201211122052p:plain

寢るか offline で遊んでろって事だ。