エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

分岐をハードコーディングしない方法についての考察

条件による分岐をハードコーディングせずに実現する方法について考えてみました。

ただし、ここで試してみた方法はライブラリやフレームワークとして再利用するというよりも、再実装せずにふるまいを変えることができる電子回路のジャンパピンのような仕組みのイメージです。

最初に分岐をハードコーディング

まずはハードコーディングして実現したいことを整理します。

例は入力で与えられる Map の中の path の値を元に呼び出す関数を切り替える簡単な実装です。

本体。 params.path の値によって Foo, Bar, Baz を呼び分けます。 該当しない場合は Error の関数を呼び出します。

defmodule MyApp do
  def create(params) do
    case params.path do
      "/foo/" <> _ -> MyApp.Foo.create(params)
      "/bar/" <> _ -> MyApp.Bar.create(params)
      "/baz/" <> _ -> MyApp.Baz.create(params)
      _ -> MyApp.Error.create(params)
    end
  end

  def update(params) do
    case params.path do
      "/foo/" <> _ -> MyApp.Foo.update(params)
      "/bar/" <> _ -> MyApp.Bar.update(params)
      "/baz/" <> _ -> MyApp.Baz.update(params)
      _ -> MyApp.Error.update(params)
    end
  end
end

分岐後に呼び出される関数を定義した MyApp.Foo の実装です。 Bar, Baz, Error は名前が異なるだけの同じ内容になるので省略します。

defmodule MyApp.Foo do
  def create(_params) do
    "Foo.create/1"
  end

  def update(_params) do
    "Foo.update/1"
  end
end

テストコード。

defmodule MyAppTest do
  use ExUnit.Case
  doctest MyApp

  describe "create/" do
    test "call create /foo/1", do: assert MyApp.create(%{path: "/foo/1"}) == "Foo.create/1"
    test "call create /bar/1", do: assert MyApp.create(%{path: "/bar/1"}) == "Bar.create/1"
    test "call create /baz/1", do: assert MyApp.create(%{path: "/baz/1"}) == "Baz.create/1"
    test "call create /abc/1", do: assert MyApp.create(%{path: "/abc/1"}) == "Error.create/1"
  end

  describe "update/1" do
    test "call update /foo/1", do: assert MyApp.update(%{path: "/foo/1"}) == "Foo.update/1"
    test "call update /bar/1", do: assert MyApp.update(%{path: "/bar/1"}) == "Bar.update/1"
    test "call update /baz/1", do: assert MyApp.update(%{path: "/baz/1"}) == "Baz.update/1"
    test "call update /abc/1", do: assert MyApp.update(%{path: "/abc/1"}) == "Error.update/1"
  end
end

ここから分岐をはがしてゆきます。

呼び出しを値に置き換える

最初に、関数の呼び出しを分岐から分離します。

分岐は利用するモジュールの取得のみにして、呼び出しには Kernel.apply/3 を利用するように変更しました。

defmodule MyApp do
  def create(params) do
    module =
      case params.path do
      "/foo/" <> _ -> MyApp.Foo
      "/bar/" <> _ -> MyApp.Bar
      "/baz/" <> _ -> MyApp.Baz
        _ -> MyApp.Error
      end

    apply(module, :create, [params])
  end

  def update(params) do
    module =
      case params.path do
      "/foo/" <> _ -> MyApp.Foo
      "/bar/" <> _ -> MyApp.Bar
      "/baz/" <> _ -> MyApp.Baz
        _ -> MyApp.Error
      end

    apply(module, :update, [params])
  end
end

分岐を値に置き換える(1)

次に、分岐を表の検索に置き換えます。

条件と利用するモジュールの対応を表として分離し、条件を元にモジュールを検索することで分岐を置き換えます。

defmodule MyApp do
  def create(params) do
    table = 
      [
        [~r"^/foo/", MyApp.Foo],
        [~r"^/bar/", MyApp.Bar],
        [~r"^/baz/", MyApp.Baz],
        [~r".*",  MyApp.Error]
      ]

    [_, module] =
      Enum.find(table, fn [pattern, _] ->
        params.path =~ pattern
      end)

    apply(module, :create, [params])
  end

  def update(params) do
    table = 
      [
        [~r"^/foo/", MyApp.Foo],
        [~r"^/bar/", MyApp.Bar],
        [~r"^/baz/", MyApp.Baz],
        [~r".*",  MyApp.Error]
      ]

    [_, module] =
      Enum.find(table, fn [pattern, _] ->
        params.path =~ pattern
      end)

    apply(module, :update, [params])
  end
end

分岐を値に置き換える(2)

二つの関数でそれぞれ表を作成しましたが、これを一段入れ子を深くして一つの表にまとめてしまいます。

defmodule MyApp do
  @table %{
    create: [
        [~r"^/foo/", MyApp.Foo, :create],
        [~r"^/bar/", MyApp.Bar, :create],
        [~r"^/baz/", MyApp.Baz, :create],
        [~r".*",  MyApp.Error, :create]
    ],
    update: [
        [~r"^/foo/", MyApp.Foo, :update],
        [~r"^/bar/", MyApp.Bar, :update],
        [~r"^/baz/", MyApp.Baz, :update],
        [~r".*",  MyApp.Error, :update]
    ]
  }

  def create(params) do
    dispatch(:create, params)
  end

  def update(params) do
    dispatch(:update, params)
  end

  def dispatch(action, params) do
    [_, module, function] =
      Enum.find(@table[action], fn [pattern, _, _] ->
        params.path =~ pattern
      end)

    apply(module, function, [params])
  end
end

値を設定ファイルに移動する

最後に、分離した表をモジュールの実装から移動します。

ここで、移動先は config ファイルにします。 中でも config/runtime.exs はアプリケーションの起動時に評価されるので、再コンパイルすることなしにふるまいを変更するのに最適です。

defmodule MyApp do
  def create(params) do
    dispatch(:create, params)
  end

  def update(params) do
    dispatch(:update, params)
  end

  def dispatch(action, params) do
    table = Application.fetch_env!(:my_app, :routing_table)

    [_, module, function] =
      Enum.find(table[action], fn [pattern, _, _] ->
        params.path =~ pattern
      end)

    apply(module, function, [params])
  end
end

config/runtime.exs の定義です。

import Config

config :my_app, :routing_table, %{
  create: [
    [~r"^/foo/", MyApp.Foo, :create],
    [~r"^/bar/", MyApp.Bar, :create],
    [~r"^/baz/", MyApp.Baz, :create],
    [~r".*", MyApp.Error, :create]
  ],
  update: [
    [~r"^/foo/", MyApp.Foo, :update],
    [~r"^/bar/", MyApp.Bar, :update],
    [~r"^/baz/", MyApp.Baz, :update],
    [~r".*", MyApp.Error, :update]
  ]
}

抜き身の Map を使う素朴な実装で、ライブラリなどで利用する仕組みとしてはあまりよいものではありません。 それでも、冒頭の話のようにジャンパピンとして割り切れば悪くはなさそうです。

一緒にインストールされるパッケージによってふるまいを変える Elixir の Req パッケージの覚書

ネット上の CSV データを、Req パッケージを使ってダウンロードし、NimbleCSV でデコードしようとしていたのですが。

hex.pm hex.pm

二つのパッケージをインストールして、Req のレスポンスのボディを NimbleCSV でデコードしたら失敗し、ボディがテキストデータでないことに気がつきました。 なにやらボディの内容がリストになっています。

最初は iodata になっているのかと勘違いしたのですが、実はレスポンスのボディがすでに CSV としてデコードされ、リストのリストになっているのでした。

よくよく Req のドキュメントを調べてみると、JSON や ZIP などは自動的にデコードする仕組みになっているのですが、NimbleCSV を一緒にインストールした場合には CSV も自動的なデコードの対象になる仕組みになっていることがわかりました。

実現方法はいたって単純で、

  • Code.ensure_loaded?/1 で NimbleCSV がロードされているか調べる
  • ロードされていれば NimbleCSV を使ってボディをデコードする、ロードされていなければ何もしない

というもの。 ただし、これだけではコンパイル時に NimbleCSV が見つからないと警告が出てしまいます。 警告を抑えるために、

  • mix.exsproject/0:xref を使って対象外にする

という細工がされていました。 なお :xref の仕様を調べきれていないので具体的な機序はまだ確認できていません。

とはいえ。 実現方法がわかり実践することは可能なので、サンプルを書いてみることにしました。

最初に、二つのパッケージを利用する MyApp と、利用される Foo, Bar を作成します。

$ mix new my_app
$ mix new foo
$ mix new bar

my_app/lib/my_app.ex を編集して Foo を利用する関数を追加します。

defmodule MyApp do
  def do_something do
    Foo.do_something() |> IO.puts()
  end
end

my_app/mix.exs の依存パッケージの記述に Foo を追加します。

defmodule MyApp.MixProject do
  use Mix.Project

  # 略

  defp deps do
    [
      {:foo, path: "../foo"}
    ]
  end
end

次に foo/lib/foo.ex を編集します。

Bar がロードされているか Code.ensure_loaded?/1 で判定します。 ロードされていれば Bar の関数を実行し、そうでなければ自分で値を返します。

defmodule Foo do
  def do_something do
    if Code.ensure_loaded?(Bar) do
      Bar.do_something()
    else
      "do something by Foo"
    end
  end
end

最後に bar/lib/bar.ex を編集します。

defmodule Bar do
  def do_something do
    "do something by Bar"
  end
end

my_app ディレクトリに移動して MyApp.do_something/0 を実行します。

$ cd my_app
$ mix run -e 'MyApp.do_something()'

Foo.do_something/0 が呼び出され、do something by Foo が表示されますが、Bar が未定義であることの警告が表示されてしまいます。

==> foo
Compiling 1 file (.ex)
    warning: Bar.do_something/0 is undefined (module Bar is not available or is yet to be defined)
    │
  4 │       Bar.do_something()
    │           ~
    │
    └─ (foo 0.1.0) lib/foo.ex:4:11: Foo.do_something/0

Generated foo app
do something by Foo

そこで foo/mix.exs を編集して以下の設定を追加します。

defmodule Foo.MixProject do
  use Mix.Project

  def project do
    [
      # 略
      xref: [
        exclude: [
          Bar
        ]
      ],
      # 略
    ]
  end
  # 略
end

これで再度実行すると Foo が再コンパイルされますが、未定義の警告は表示されなくなりました。

$ mix run -e 'MyApp.do_something()'
==> foo
Compiling 1 file (.ex)
Generated foo app
do something by Foo

もう一度 my_app/mix.exs を編集して依存パッケージの記述に Bar を追加します。

defmodule MyApp.MixProject do
  use Mix.Project

  # 略

  defp deps do
    [
      {:foo, path: "../foo"},
      {:bar, path: "../bar"}
    ]
  end
end

もう一度再実行します。

MyApp や Foo はコンパイルされず、Bar だけがコンパイルされるのがわかります。 そして MyApp.do_something/0 から Foo.do_something/0 が、Foo.do_something/0 から Bar.do_something/0 が呼び出され、最終的に do something by Bar が表示されました。

$ mix run -e 'MyApp.do_something()'
==> bar
Compiling 1 file (.ex)
Generated bar app
do something by Bar

Elixirの演算子多重定義に関する覚書

Elixir には演算子を再定義できる機能が備わっています。

defmodule Foo do
  def lhs + rhs do
    String.to_integer(Integer.to_string(lhs) <> Integer.to_string(rhs))
  end
end

普通の関数と同じように利用することができます。

Foo.+(123, 456)

モジュール名で修飾することなく、一般的な演算子として利用するには import する必要があります。

import Foo, only: [+: 2]

しかしこれだけでは正しく機能しません。

123 + 456
#=> error: function +/2 imported from both Foo and Kernel, call is ambiguous

Kernel.+/2 も同時に定義されているために、どちらの演算子を利用したらよいか判別がつかないからです。

再定義する演算子の引数の型を限定すれば、型を元に判別ができるのではと思うのですが、

defmodule Foo do
  defstruct [:value]

  def new(value \\ 0) do
    %__MODULE__{value: value}
  end

  def lhs + rhs when is_struct(lhs, Foo) and is_struct(rhs, Foo) do
    new(Kernel.+(lhs.value, rhs.value))
  end
end

残念ながら結果は同じです。

import Foo, only: [+: 2]
Foo.new(123) + Foo.new(456)
#=> error: function +/2 imported from both Foo and Kernel, call is ambiguous

このようなばあい、衝突している関数の import を解除することで解消することができます。

import Kernel, except: [+: 2]
import Foo, only: [+: 2]
Foo.new(123) + Foo.new(456)
#=> %Foo{value: 579}

ただし、当然ですが、解除した関数を利用することができなくなります。

123 + 456
#=> ** (FunctionClauseError) no function clause matching in Foo.+/2    

これを解消する方法として、再定義した演算子を適用しない「その他のケース」のばあいに元の関数を呼び出せばよさそうです。

defmodule Foo do
  defstruct [:value]

  def new(value \\ 0) do
    %__MODULE__{value: value}
  end

  def lhs + rhs when is_struct(lhs, Foo) and is_struct(rhs, Foo) do
    new(Kernel.+(lhs.value, rhs.value))
  end

  def lhs + rhs when is_struct(lhs, Foo) and is_number(rhs) do
    new(Kernel.+(lhs.value, rhs))
  end

  def lhs + rhs when is_number(lhs) and is_struct(rhs, Foo) do
    new(Kernel.+(lhs, rhs.value))
  end

  # その他のケース
  def lhs + rhs do
    Kernel.+(lhs, rhs)
  end
end
import Kernel, except: [+: 2]
import Foo, only: [+: 2]

Foo.new(123) + Foo.new(456)
#=> %Foo{value: 579}

Foo.new(123) + 456
#=> %Foo{value: 579}

123 + Foo.new(456)
#=> %Foo{value: 579}

123 + 456
#=> 579

最後に。 import Kernelimport Foo はいつも組で利用するので、一つの記述で済ませられるようにマクロを定義すると便利です。

defmodule Foo do
  defstruct [:value]

  defmacro __using__(_) do
    quote do
      import Kernel, except: [+: 2]
      import Foo, only: [+: 2]
    end
  end

  # 略
end

これで use するだけで利用できるようになりました。

use Foo

Foo.new(123) + Foo.new(456)
#=> %Foo{value: 579}

123 + 456
#=> 579

PureScript と Erlang、と Elixir、の覚書

PureScript というプログラミング言語があります。

Haskell のような構文で記述でき JavaScript を出力できる、ということを半年ほど前に知ったのですが。

www.purescript.org

最近になって、バックエンドを切り替えれば Erlangソースコードを出力できるということを知りました。

元々は Phoenix のフロントエンドのプログラミングで PureScript を使うための方法を調べようとしていたのですが、あまりに面白そうだったので先にこちらに手を出した次第。

なお、ここから先は PureScript の開発環境は別途準備できている前提で話をしてゆきます。

Alternate backends

PureScript に利用できるバックエンドは、ドキュメントにまとめられています。

github.com

Erlang をターゲットにした purerl は今も開発が続けられ、現時点では一つ前のバージョン PureScript 0.15.14 まで対応されています。

github.com

Installation

purerl のインストールは、利用する環境のバイナリを GitHub からダウンロードするのが今のところ一番簡単な方法のようです。

github.com

ダウンロードできたら、 purerl コマンドを実行できるようにパスを設定するかリンクを作成するなどします。

purerl を利用した PureScript プロジェクトのサンプル

PureScript のパッケージマネジャの spago を使って、新しい PureScript プロジェクトを作成します。

$ mkdir example
$ cd example
$ spago init

purerl を設定する

spago.dhall を編集してバックエンドの指定を追加します。

{ name = "my-project"
, packend = "purerl" -- この行を追加する
, dependencies = [ "console", "effect", "prelude" ]
, packages = ./packages.dhall
, sources = [ "src/**/*.purs", "test/**/*.purs" ]
}

packages.dhall を編集して利用するパッケージの指定を purerl のものに変更します。

let upstream =
      https://github.com/purerl/package-sets/releases/download/erl-0.15.3-20220629/packages.dhall
        sha256:48ee9f3558c00e234eae6b8f23b4b8b66eb9715c7f2154864e1e425042a0723b

spago run コマンドを実行すると、パッケージのインストールやビルドが実行され、 src/Main.purs に書かれたコードの実行結果が出力されることを確認できると思います。

$ spago run
... パッケージのインストールやビルドのログ ...
🍝

生成された Erlang のコードは output/ に出力されています。

$ ls output/Main/ 
corefn.json     externs.cbor        main.hrl        main@foreign.hrl    main@ps.erl

また .beam ファイルは ebin/ に出力されます。 src/Main.pursコンパイル結果は main@ps.beam に出力されています。

$ ls ebin/main*       
ebin/main@ps.beam

インストールされたパッケージのソースコードやバイナリも output/ebin/ に格納されていることが確認できると思います。

Erlang から利用する

erl を起動します。 このとき、検索対象のパスに .beam ファイルが格納されたディレクトリを指定します。

$ erl -pa ebin

.beam ファイルのファイル名から、モジュール名は main@ps とわかるので、main@ps:main を実行してみます。

1> main@ps:main().
#Fun<effect_console@foreign.0.108104793>

main の型は Effect Unit と定義されていますが、 Effect モナドの値は Erlang からは関数に見えるようです。

戻り値の関数を実行してみます。

2> (main@ps:main())().
🍝
ok

Elixir から利用する

iex からも利用しています。 やり方は erl と同じです。

$ iex -pa ebin     
iex(1)> :main@ps.main()
#Function<0.108104793/0 in :effect_console@foreign.log/1>
iex(2)> :main@ps.main().()
🍝
:ok

elixir コマンドで直接実行することもできます。

$ elixir -pa ebin -e ':main@ps.main().()'
🍝

purerl を利用した Elixir プロジェクトのサンプル

Hex を検索すると、Elixir から purerl を利用するためのパッケージ purerlex を登録してくださっている方がいます。

hex.pm

これを利用させてもらうことにしました。

まず Elixir のプロジェクトを作成します。

$ mix new my_app
$ cd my_app

続いて同じディレクトリで PureScript のプロジェクトを作成します。

$ spago init

先の purerl のサンプルと同様に spago.dhallpackages.dhall を編集してバックエンドとパッケージの取得先を指定します。

purerlex を利用する

mix.exs を次のように編集します。

defmodule MyApp.MixProject do
  use Mix.Project

  def project do
    [
      app: :my_app,
      version: "0.1.0",
      elixir: "~> 1.16",
      start_permanent: Mix.env() == :prod,
      erlc_paths: ["output"],                  # 追加: Erlang のソースコードのパスとして output を指定
      compilers: [:purerl] ++ Mix.compilers(), # 追加: Elixir のコンパイル時に purerl の実行を指定
      deps: deps()
    ]
  end

  def application do
    [
      extra_applications: [:logger]
    ]
  end

  defp deps do
    [
      {:purerlex, "~> 0.12.2"}                 # 追加
    ]
  end
end

ちなみに。 ここでコンパイラに指定している purerl は、 purerl コマンドではなく、purerlex が定義しているタスクです。

パッケージを取得します。

$ mix deps.get

iex を起動します。

$ iex -S mix                                                                                  

-S mix を指定して iex を起動すると自動的にコンパイルが実行されますが、このとき PureScript のパッケージの取得やビルドも実行されていることが確認できると思います。

iex(1)> :main@ps.main()
#Function<0.108104793/0 in :effect_console@foreign.log/1>
iex(2)> :main@ps.main().()
🍝
:ok

Elixir から利用する

Elixir のプロジェクトを作成したときに作成される MyApp.hello/0 を PureScript で書いたものに置き換えてみます。

MyApp.hello/0 はアトムを返すので、アトムを利用できるように PureScript のパッケージを追加します。 このパッケージは packages.dhall で指定した package-sets を元に検索されます。

$ spago install purescript-erl-atom

新しく src/MyApp.purs を作成します。

module MyApp where

import Erl.Atom (atom)

hello = atom "world"

コンパイル

$ mix compile

コンパイルの結果出力される .beam ファイルが格納される _build/dev/lib/my_app/ebin/ を確認すると myApp@ps.beam という名前で出力されていることがわかります。

実行してみます。

$ iex -S mix
iex(1)> :myApp@ps.hello()
:world

期待する値が得られることが確認できました。

MyApp の呼び出しを委譲してみます。

defmodule MyApp do
  defdelegate hello, to: :myApp@ps
end

当然ですが期待する結果がえられますしテストもパスします。

$ iex -S mix
iex(1)> MyApp.hello()
:world
$ mix test
1 test, 0 failures

Elixir のプロジェクトで PureScript を利用できることが確認できました。

フォントデータを ETS で保存する

前回の続きです。

前回は BDF ファイルをパースして読み込む話をしました。

そして、BDF ファイルは単純なテキストファイルだし、Nerves アプリケーションへそのまま持っていっても大丈夫だろう、と高を括っていたのですが。

Raspberry Pi ZERO W の非力さを甘くみていました。

起動時に BDF ファイルを読み込ませるようにしたら、電源を入れてもなかなか入力に反応しない。 何かを壊したかとあせりもしたのですが、結局テキストのパースに時間がかかっている様子でした。

一旦読み込み終えてしまえば、あとはメモリ上のアクセスのみになるので、その後の動作には影響しません。 しかし電源を入れてから使えるようになるまで時間がかかるのは問題です。 そのため、読み込んだデータを別の形式で保存しておきすぐに読み出せるようにできる方法を模索しました。

せっかくなら文字コードをキーにアクセスできるように、key-value ストレージのようなものでなにか扱いやすいもの。

Hex も検索してみたのですが。 よく考えてみればあるではないですか、標準装備のストレージが。

ETS です。

www.erlang.org elixirschool.com

どのように利用するとデータが扱いやすくなるのか、いくつか格納方法を変えて試してみました。

今回もフォントデータには 16 ドットの東雲フォントを利用しています。 ファイルサイズは約 1.1 M バイト。

-rw-r--r--@ 1 matsumotoeiji  staff  1135668  9 15  2004 shnmk16.bdf

構造体で保存する

Erlang Term Storage の名の通り、Erlang Term であればなんでも格納できるとあって、まずは構造体をそのまま格納。

BDF モジュールは前回の記事で登場したモジュールです。

{:ok, fonts} = BDF.load("shinonome-0.9.11/bdf/shnmk16.bdf")

table = :ets.new(:shnmk16, [:set])

Enum.each(fonts, fn font ->
  :ets.insert(table, {font.encoding, font})
end)

:ets.tab2file(table, ~c"priv/fonts/shnmk16.ets")

できたファイルのサイズは約 2 M バイト。

$ mix run bdf2ets-1.exs
$ ls -l priv/fonts/shnmk16.ets
-rw-r--r--  1 matsumotoeiji  staff  2080992  2 27 19:44 priv/fonts/shnmk16.ets

tab2file/2 で書き出したデータは、file2tab/1 で簡単に復元でき、lookup/2 で検索できます。

iex(1)> {:ok, table} = :ets.file2tab(~c"priv/fonts/shnmk16.ets")
{:ok, #Reference<0.513635613.2690514945.38635>}
iex(2)> :ets.lookup(table, 0x4E6E)
[
  {20078,
   %BDF.Font{
  encoding: 0x4E6E,
  dwidth: %BDF.Font.DWIDTH{dwx0: 16, dwy0: 0},
  bbx: %BDF.Font.BBX{bbw: 16, bbh: 16, bbxoff0x: 0, bbyoff0y: -2},
  bitmap: [0x0000, 0x3FFC, 0x0100, 0x7FFE, 0x4102, 0x7D7A, 0x4F3E, 0x0000, 0x1FF8, 0x0000, 0x7FFE, 0x0248, 0x1248, 0x0A50, 0x7FFE, 0x0000]
}
}
]

ただしこれは BDF モジュールが定義されている環境のばあい。

モジュールが定義されていない環境で読み込んでみると。

iex(1)> {:ok, table} = :ets.file2tab(~c"priv/fonts/shnmk16.ets")
{:ok, #Reference<0.217574499.542769155.19734>}
iex(2)> :ets.lookup(table, 0x4E6E)
[
  {20078,
   %{
     encoding: 20078,
     __struct__: BDF.Font,
     dwidth: %{__struct__: BDF.Font.DWIDTH, dwx0: 16, dwy0: 0},
     bbx: %{
       __struct__: BDF.Font.BBX,
       bbh: 16,
       bbw: 16,
       bbxoff0x: 0,
       bbyoff0y: -2
     },
     bitmap: [0, 16380, 256, 32766, 16642, 32122, 20286, 0, 8184, 0, 32766, 584,
      4680, 2640, 32766, 0]
   }}
]

読み込めないわけではないものの、構造体が定義されていないので、内部構造が丸見えになったマップとして扱われています。

タプルで保存する

一度構造体に格納したものを、ただのタプルにしてしまうのもどうかと思いましたが。 内容の単純さや利用シーンを考えるとタプルでもさほど不便はないかと思い直すなど。

{:ok, fonts} = BDF.load("shinonome-0.9.11/bdf/shnmk16.bdf")

table = :ets.new(:shnmk16, [:set])

Enum.each(fonts, fn font ->
  :ets.insert(
    table,
    {
      font.encoding,
      font.dwidth.dwx0,
      font.dwidth.dwy0,
      font.bbx.bbw,
      font.bbx.bbh,
      font.bbx.bbxoff0x,
      font.bbx.bbyoff0y,
      font.bitmap
    }
  )
end)

:ets.tab2file(table, ~c"priv/fonts/shnmk16.ets")
$ mix run bdf2ets-2.exs
$ ls -al priv/fonts/shnmk16.ets
-rw-r--r--  1 matsumotoeiji  staff  767103  2 27 19:52 priv/fonts/shnmk16.ets

約 767 k バイト。構造体で保存した時の 3 分の 1 程度。

構造から名前が失われ、読みだせばたただの数字の羅列。

iex(1)> {:ok, table} = :ets.file2tab(~c"priv/fonts/shnmk16.ets")
{:ok, #Reference<0.2786847046.275644418.110991>}
iex(2)> :ets.lookup(table, 0x4E6E)
[
  {20078, 16, 0, 16, 16, 0, -2,
   [0, 16380, 256, 32766, 16642, 32122, 20286, 0, 8184, 0, 32766, 584, 4680,
    2640, 32766, 0]}
]

しかしタプルの中の位置がわかっていればよいので、これでも構わない気もしてくる。

バイナリで保存する

フォントデータはビット操作で加工したりするわけだし、いっそのこと全体をバイナリにしてしまっても構わないのでは? と思ってやってみました。

ETS に格納できるのは先頭の要素をキーとしたタプルのみなので、文字コードだけをそのままに、残りのデータを一つのバイナリに詰め込み。

{:ok, fonts} = BDF.load("shinonome-0.9.11/bdf/shnmk16.bdf")

table = :ets.new(:shnmk16, [:set])

Enum.each(fonts, fn font ->
  bitmap = for line <- font.bitmap, into: <<>>, do: <<line::size(font.bbx.bbw)>>

  :ets.insert(
    table,
    {
      font.encoding,
      <<
        font.dwidth.dwx0::8,
        font.dwidth.dwy0::8,
        font.bbx.bbw::8,
        font.bbx.bbh::8,
        font.bbx.bbxoff0x::8,
        font.bbx.bbyoff0y::8,
        bitmap::binary
      >>
    }
  )
end)

:ets.tab2file(table, ~c"priv/fonts/shnmk16.ets")
$ mix run bdf2ets-3.exs
$ ls -l priv/fonts/shnmk16.ets
-rw-r--r--  1 matsumotoeiji  staff  406276  2 27 20:01 priv/fonts/shnmk16.ets

約 406 k バイト。タプルで保存したときの半分強くらい。構造体で保存したときの 5 分の 1 くらい。 コンパクトにはなりました。

読みだせば、値と値の区切りも喪失した、タプルで格納したときよりもさらに面妖な状態。

iex(1)> {:ok, table} = :ets.file2tab(~c"priv/fonts/shnmk16.ets")
{:ok, #Reference<0.3552940925.8519682.187654>}
iex(2)> :ets.lookup(table, 0x4E6E)
[
  {20078,
   <<16, 0, 16, 16, 0, 254, 0, 0, 63, 252, 1, 0, 127, 254, 65, 2, 125, 122, 79,
     62, 0, 0, 31, 248, 0, 0, 127, 254, 2, 72, 18, 72, 10, 80, 127, 254, 0, 0>>}
]

ベンチマークは未実施

ライブラリの関数を使ってデータを一括で読み込むので時間もかからず、格納のしかたによってファイルサイズを小さくできることもわかりました。 が。 利用時の効率はまだ測れていません。

Elixir がバイナリ操作を得意としているとはいえ、構造を持つタプルの操作と比べると、バイナリの操作には余分に時間がかかるのではと想像します。 その差は微々たるものなのでしょうが、Raspberry Pi でテキストファイルを読み込ませたら存外時間が取られたという前科があるので、油断はできません。 あるいは、表示デバイスとの IO の方の影響の方がずっと大きくて、バイナリの操作にかかる時間は気にするほどのことでないのかもしれません。

こればかりはベンチマークするしかないので、もう少し調べてみたいと思います。

Glyph Bitmap Distribution Format (BDF) を Elixir で読み込む

Glyph Bitmap Distribution Format (BDF) というフォントフォーマットがあります。

en.wikipedia.org

記事の最後の方に書いたような理由があって、 BDF を読み込むパッケージを書いています。

道半ばなのですが、お試しで使えるくらいにはまとまったので、一旦出力しておこうと思った次第。

github.com

今回書いた BDF をパースする方法は、仕様上は安全でない可能性があるのですが、それに関しては記事を改めて考察することにしたいと思います。

BDF をコンソールに表示してみる

これを使って実際に文字を表示してみます。 まずは iex でお手軽に試します。

パッケージをインストールする

まだ hex.pm に公開していないので、GitHubリポジトリを指定してインストールしてください。

mix.exs でインストールする場合:

  defp deps do
    [
      {:bdf, github: "mattsan/bdf"}
    ]
  end

iex やスクリプトファイルでインストールする場合:

Mix.install([{:bdf, github: "mattsan/bdf"}])

フォントファイルを入手する

BDF ファイルを用意して読み込みます。

今回はパブリックドメインで公開されている東雲フォントを利用させていただきました。

openlab.ring.gr.jp

表示したいフォントデータを取得する

東雲フォントは文字コードに JIS を利用しています。 必要に応じて JIS X 0213のコード対応表 などを利用して表示したい文字の文字コードを確認し、フォントデータを取得します。

今回はサンプルということで、特に効率などを考慮せず Enum.find/2 で線形検索しています。

{:ok, fonts} = BDF.load("path/to/shnmk16.bdf") # 入手した BDF ファイルのパスを指定します

font = Enum.find(fonts, & &1.encoding == 0x4E6E)

表示する

取得したフォントデータは、点の一つ一つが 1 ビットで表現されているので、それらのビットを見える形に展開して表示します。

Enum.each(font.bitmap, fn row ->
  for <<dot::1 <- <<row::size(font.bbx.bbw)>> >> do
    case dot do
      0 -> " ."
      1 -> "@@"
    end
  end
  |> Enum.join()
  |> IO.puts()
end)

結果。

 . . . . . . . . . . . . . . . .
 . .@@@@@@@@@@@@@@@@@@@@@@@@ . .
 . . . . . . .@@ . . . . . . . .
 .@@@@@@@@@@@@@@@@@@@@@@@@@@@@ .
 .@@ . . . . .@@ . . . . . .@@ .
 .@@@@@@@@@@ .@@ .@@@@@@@@ .@@ .
 .@@ . .@@@@@@@@ . .@@@@@@@@@@ .
 . . . . . . . . . . . . . . . .
 . . .@@@@@@@@@@@@@@@@@@@@ . . .
 . . . . . . . . . . . . . . . .
 .@@@@@@@@@@@@@@@@@@@@@@@@@@@@ .
 . . . . . .@@ . .@@ . .@@ . . .
 . . .@@ . .@@ . .@@ . .@@ . . .
 . . . .@@ .@@ . .@@ .@@ . . . .
 .@@@@@@@@@@@@@@@@@@@@@@@@@@@@ .
 . . . . . . . . . . . . . . . .

BDF を Livebook で表示してみる

Livebook を使えば簡単に画像で確認することもができるので、これもやってみます。

パッケージをインストールする

mattsan/bdf の他に、画像を表示すために kino と、PNG データを生成するために拙作の pngex も合わせてインストールします。

Mix.install([
  {:kino, "~> 0.12.3"},
  {:pngex, "~> 0.1.2"},
  {:bdf, github: "mattsan/bdf"}
])

BDF ファイルを読み込む

読み込みは iex で試したときと変わりません。 ダウンロードした BDF ファイルを読み込みます。

{:ok, fonts} = BDF.load("path/to/shnmk16.bdf")

フォントデータを画像に変換する

pngex を使って画像に変換します。 ここではパレットカラーを使い、 1 ビット / ドットの画像を生成しています。

palette = [
  {255, 255, 255}, # 背景: 白
  {0, 0, 0}        # 文字: 黒
]

font1 = Enum.find(fonts, & &1.encoding == 0x4E6E)
font2 = Enum.find(fonts, & &1.encoding == 0x4C74)

bitmap =
  for row <- font1.bitmap ++ font2.bitmap, into: <<>> do
    <<row::size(font1.bbx.bbw)>>
  end

Pngex.new()
|> Pngex.set_type(:indexed)
|> Pngex.set_depth(:depth1)
|> Pngex.set_size(font1.bbx.bbw, font1.bbx.bbh + font2.bbx.bbh)
|> Pngex.set_palette(palette)
|> Pngex.generate(bitmap)
|> IO.iodata_to_binary()

Livebook での表示の様子。

表示が小さいので縦横 4 倍のサイズにしてみます。

palette = [
  {255, 255, 255}, # 背景: 白
  {0, 0, 0}        # 文字: 黒
]

font1 = Enum.find(fonts, & &1.encoding == 0x4E6E)
font2 = Enum.find(fonts, & &1.encoding == 0x4C74)

bitmap =
  for row <- font1.bitmap ++ font2.bitmap, into: <<>> do
    line =
      for <<dot::1 <- <<row::size(font1.bbx.bbw)>> >>, into: <<>> do
        case dot do
          0 -> <<0::4>>
          1 -> <<0xF::4>>
        end
      end
    String.duplicate(line, 4)
  end

Pngex.new()
|> Pngex.set_type(:indexed)
|> Pngex.set_depth(:depth1)
|> Pngex.set_size(font1.bbx.bbw * 4, (font1.bbx.bbh + font2.bbx.bbh) * 4)
|> Pngex.set_palette(palette)
|> Pngex.generate(bitmap)
|> IO.iodata_to_binary()

解像度の低いディスプレイに似合いそうな表示が得られました。

そんなわけで、Nerves 再起動

開発が進んでいる様子のコメントがされつつも、なかなか公開に至っていなかった Circuits.GPIO ですが、満を持してバージョン 2.0 が今月公開されました。

hex.pm

それに刺激を受けて、5 年ほど放置していた Nerves プログラミングを再開。

nerves-project.org

(デバイスRPI-ZERO-WH Raspberry Pi Zero WH【ピンヘッダ実装済】 + WaveShare 13891 1.44インチ 128×128 LCDディスプレイHAT for RaspberryPi

そのうち、何かおもしろいものを出力できるといいな。 そのうち。

いつか読むはずっと読まない:神経網計画

nextpublishing.jp pragprog.com pragprog.com

40年のソフトウェア的愛情〜または私は如何にして心配するのを止めてプログラマであったか

以前、プログラミングを始めて 30 年が経ちましたという記事を書きました。

blog.emattsan.org

それから 10 年が過ぎました。 気づくと 40 年。

職業プログラマに転向して 10 年。 今もプログラマを続けています。

好きでプログラミングを続けているとはいえ、仕事をしていると疲労もストレスも感じます。 そんなときは好きなプログラミングで疲れを癒す。

そんな日々を送っています。 たぶんこれからもそんな日々を続けるのだろう、と思いをはせる年の瀬。

41 年目もよろしくお願いします。