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

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

一緒にインストールされるパッケージによってふるまいを変える 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 年目もよろしくお願いします。

Phoenix LiveView の assign_async と async_result

Phoenix LiveView で値を socket に assign するとき、その値が例えば Web API などで取得しなければならないとき、一旦待ち状態を設定して、それから Task.async などを利用して、結果が得られたら取得できた値を設定し直すという非同期の処理を行うわけですが。

そういった使い方が多かったためなのか、 LiveView 0.20.0 では非同期で assign をおこなう Phoenix.LiveView.assign_async/3 と、その状態を表示する Phoenix.Component.async_result/1 が追加されていました。 9 月末に追加されていたのですが、すっかり見落としていました。

関数の追加に合わせて、ドキュメントにも一節が追加されています。

今後、繰り返し利用することになりそうなので、これらの使い方を確認してみました。

assign_async & async_result

基本的な使い方はそれほど難しくありません。

まず、値の設定には assign/3 の代わりに assign_async/3 を利用します。

第 2 引数は assign/3 と同じようにキーを指定しますが、第 3 引数には非同期で実行する関数を指定します。 その関数は、戻り値として {:ok, result}{:error, reason} の形の値を返す必要があります。 また result の値は assign_async/3 の第 2 引数に渡したキーを持つマップでなければなりません。

assign_async(socket, :foo, fn ->
  # 非同期で実行したい処理

  {:ok, %{foo: 42}} # 処理に成功したばあい、第 2 引数に渡したキーと同じキーを持つマップを返す
end)

assign_async/3 の第 3 引数に渡した関数の結果は async_result/1 で受けることができます。

async_result/1assign で指定したキーを指定し、:let で値を受け取ります。 内部のブロックは関数が成功して値を返するまで表示されません。

また async_result/1:loading:failed の 2 つのスロットを持っています。

:loading は関数が完了するまでのあいだ表示さるものです。

:failed は関数がエラーを返したときに表示されます。 エラーの内容は :let で受けることができます。 内容は関数の戻り値そのままになっています。

これらのスロットは省略可能です。

defmodule MyAppWeb.PageLive do
  use MyAppWeb, :live_view

  def render(assigns) do
    ~H"""
    <.async_result :let={result} assign={@result}>
      result = <%= result %>
      <:loading>waiting...</:loading>
      <:failed :let={ {:error, reason} }><%= reason %></:failed>
    </.async_result>
    """
  end

  def mount(_params, _session, socket) do
    socket =
      socket
      |> assign_async(:result, fn ->
        Process.sleep(2_000)     # 時間のかかる処理の代わり
        case :rand.uniform(2) do # 2 分の 1 で成功/失敗を返す
          1 -> {:ok, %{result: 42}}
          2 -> {:error, "no result"}
        end
      end)

    {:ok, socket}
  end
end

ちなみに。 関数の戻り値をマップにしなければならない理由は、一回の結果で複数の値を返せるようにするためのようです。 次の例ように assign_async/3 の第 2 引数はリストで複数のキーを指定することができ、そのキーごとに async_result/1 を記述することができます。

  def render(assigns) do
    ~H"""
    <.async_result :let={foo} assign={@foo}><%= foo %></.async_result>
    <.async_result :let={bar} assign={@bar}><%= bar %></.async_result>
    <.async_result :let={baz} assign={@baz}><%= baz %></.async_result>
    """
  end

  def mount(_params, _session, socket) do
    socket =
      socket
      |> assign_async([:foo, :bar, :baz], fn ->
        Process.sleep(2_000)
        {:ok, %{foo: "Foo", bar: "Bar", baz: "Baz"}}
      end)
    {:ok, socket}
  end

ページを表示したあとに、ページ上のイベントで処理を実行したいばあいも assign_async/3 が利用できます。

defmodule MyAppWeb.PageLive do
  use MyAppWeb, :live_view

  def render(assigns) do
    ~H"""
    <.async_result :let={result} assign={@result}>
      result = <%= result %>
      <:loading>waiting...</:loading>
      <:failed :let={{:error, reason}}><%= reason %></:failed>
    </.async_result>

    <div>
      <.button phx-click="rerun">再実行</.button>
    </div>
    """
  end

  def mount(_params, _session, socket) do
    socket =
      socket
      |> assign_async(:result, &run/0)

    {:ok, socket}
  end

  # 再実行イベントのハンドラ
  def handle_event("rerun", _params, socket) do
    socket =
      socket
      |> assign_async(:result, &run/0)

    {:noreply, socket}
  end

  # 再実行イベントでも利用するので、独立した関数に分離した
  defp run do
    Process.sleep(2_000)

    case :rand.uniform(2) do
      1 -> {:ok, %{result: 42}}
      2 -> {:error, "no result"}
    end
  end
end

Phoenix.LiveView.AsyncResult

ちなみに assign_async/3 で割り当てられた値はどのようになっているかというと。

rener/1 の中に <%= inspect(@result) %> を挿入するなどしてその値を覗いてみます。

  • 実行中
%Phoenix.LiveView.AsyncResult{ok?: false, loading: [:result], failed: nil, result: nil}
  • 成功
%Phoenix.LiveView.AsyncResult{ok?: true, loading: nil, failed: nil, result: 42}
  • 失敗
%Phoenix.LiveView.AsyncResult{ok?: false, loading: nil, failed: {:error, "no result"}, result: nil}

構造体 Phoenix.LiveView.AsyncResult に値が格納されていることがわかります。

このことを理解しておくことが、もう一つの関数を利用するときに重要になります。

start_async & handle_async

今回のバージョンアップで、 assign_async/3async_result/1 に加えてもう一つ、関数 start_async/3 が追加されています。

start_async/3 を使うと assign_async/3 と比べべてより細かな制御ができるようになっています。

その代わり、構造体 Phoenix.LiveView.AsyncResult のキーへの割り当てと、結果を反映するためにハンドラ handle_async/3 は自分で記述しなければなりません。

start_async/3

まず、 Phoenix.LiveView.AsyncResult.loading/0-1 を使ってキーに割り当てる値を作成します。 ここで引数に任意の値を渡すことができます。

assign(socket, :result, Phoenix.LiveView.AsyncResult.loading())

# もしくは、引数に任意の値を指定する
assign(socket, :result, Phoenix.LiveView.AsyncResult.loading("実行中"))

引数で渡した値は、スロット :loading:let を使って受け取ることができます。

<:loading :let={loading}><%= loading %></:loading>

キーへの割り当てができたら、 start_async/3 で非同期処理を実行します。

    socket =
      socket
      |> assign(:result, Phoenix.LiveView.AsyncResult.loading("実行中"))
      |> start_async(:result, &run/0)

なお、今回は使用しませんが、構造体の値を更新する loading/2 も用意されています。 非同期処理を多段階で実行するときに活用できそうです。

socket
|> assign(:result, Phoenix.LiveView.AsyncResult.loading(1))

...

socket
|> update(:result, &Phoenix.LiveView.AsyncResult.loading(&1, &1.loading + 1))
<:loading :let={step}>ステップ <%= step %> を実行中</:loading>

handle_async/3

結果は、ハンドラ handle_async/3 で受け取ります。 第 1 引数には assign したキーを指定します。

タスク成功時

注意が必要なのは第 2 引数で、ここには非同期処理を実行したタスクの結果が渡されてきます。 {:ok, task_result} の形で受け取る値はタスクの実行結果であり、処理の実行結果ではありません。 処理の実行結果は task_result の内容を調べる必要があります。

結果を表示に反映するには、最初に割り当てた値を ok/2 または failed/2 を使って更新します。

ハンドラの全体は次のようになります。 成功時、失敗時、それぞれ扱う値がやや込み入っているので注意が必要です。

  def handle_async(:result, {:ok, task_result}, socket) do
    socket =
      case task_result do
        {:ok, %{result: result}} ->
          # 関数の成功時の処理
          socket
          |> update(:result, &Phoenix.LiveView.AsyncResult.ok(&1, result))

        {:error, _reason} = error ->
          # 関数の失敗時の処理
          socket
          |> update(:result, &Phoenix.LiveView.AsyncResult.failed(&1, error))
      end

    {:noreply, socket}
  end

タスク失敗時

handle_async/3 の第 2 引数はタスクの実行結果なので、処理が完了したときの結果は :ok のタプルで渡されてきますが、例外などで処理が完了しなかったばあいには :exit のタプルが渡されてきます。

そのときの第 2 引数は {:exit, reason} という形になり、原因が例外だったばあいには result{exception, stacktrace} の形になります。

ハンドリングした例外の内容は failed/2 で設定して表示することができます。

  def handle_async(:result, {:exit, {exception, _stacktrace}}, socket) do
    socket =
      socket
      |> update(:result, &Phoenix.LiveView.AsyncResult.failed(&1, {:error, Exception.message(exception)}))

    {:noreply, socket}
  end

まとめ

コードの全体はこのようになりました。

defmodule MyAppWeb.PageLive do
  use MyAppWeb, :live_view

  def render(assigns) do
    ~H"""
    <.async_result :let={result} assign={@result}>
      result = <%= result %>
      <:loading :let={loading}><%= loading %></:loading>
      <:failed :let={{:error, reason}}><%= reason %></:failed>
    </.async_result>

    <div>
      <.button phx-click="rerun">再実行</.button>
    </div>
    """
  end

  def mount(_params, _session, socket) do
    socket =
      socket
      |> assign(:result, Phoenix.LiveView.AsyncResult.loading("実行中"))
      |> start_async(:result, &run/0)

    {:ok, socket}
  end

  # 再実行イベントのハンドラ
  def handle_event("rerun", _params, socket) do
    socket =
      socket
      |> assign(:result, Phoenix.LiveView.AsyncResult.loading("再実行中"))
      |> start_async(:result, &run/0)

    {:noreply, socket}
  end

  # 関数完了イベントのハンドラ
  def handle_async(:result, {:ok, task_result}, socket) do
    socket =
      case task_result do
        {:ok, %{result: result}} ->
          socket
          |> update(:result, &Phoenix.LiveView.AsyncResult.ok(&1, result))

        {:error, _reason} = error ->
          socket
          |> update(:result, &Phoenix.LiveView.AsyncResult.failed(&1, error))
      end

    {:noreply, socket}
  end

  # 例外発生時のハンドラ
  def handle_async(:result, {:exit, {exception, _stacktrace}}, socket) do
    socket =
      socket
      |> update(:result, &Phoenix.LiveView.AsyncResult.failed(&1, {:error, Exception.message(exception)}))

    {:noreply, socket}
  end

  defp run do
    Process.sleep(2_000)

    case :rand.uniform(3) do # 3 分の 1 で成功/失敗/例外を返す
      1 -> {:ok, %{result: 42}}
      2 -> {:error, "no result"}
      3 -> raise "Boom"
    end
  end
end

いつか読むはずっと読まない:恐竜前、恐竜後

恐竜前の単弓類の時代と、恐竜後の単弓類の時代。