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

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

Discord Webhook で Ruby からファイルを POST する

昨年の 10 月に Discord Webhook で Elixir からファイルを POST する方法を記事に書きましたが、その Ruby 版です。

テキストをPOSTする

テキストのみの場合、net/httpNet::HTTP.post_form メソッドを使うことで簡単に POST できます。 パラメータとして、本文は content 、ユーザ名は username をキーにした Hash を渡します。 このとき content は必須です。

そのほかのフィールドは Discord のドキュメントを参照してください。

ここでは Webhook の URL は次のように環境変数 WEBHOOK_URL に設定してあることにします。

$ export WEBHOOK_URL=https://discord.com/api/webhooks/****/*******

あとは Webhook の URL 、JSON エンコードした body 、ヘッダにコンテントタイプを指定して POST すれば OK です。

require 'net/http'

Net::HTTP.post_form(
  URI.parse(ENV['WEBHOOK_URL']),
  {
    username: 'emattsan',
    content: 'Hello'
  }
)

ファイルを POST する

前回の Elixir の記事でも書きましたが、ファイルをアップロードするときは JSON でなくて multipart/form-data で POST する必要があります。

multipart-post gem を使う

ありがたいことに multipart を扱うための gem が公開されています。

multipart で POST するためのクラス Net::HTTP::Post::Multipart と、ファイルを扱うためのヘルパクラス UploadIO を使います。

まず POST したいファイルから UploadIO のオブジェクトを作成します。

次に New::HTTP::Post::Multipart のオブジェクトを作成します。 ここでドキュメントにあるように、パラメータ の Hash にキーを file にして UploadIO のオブジェクトを渡します。

あとは Net::HTTP::Post を利用するばあいと同じ手順で POST できます。

require 'json'
require 'net/http'
require 'net/http/post/multipart'

url = URI.parse(ENV['WEBHOOK_URL'])

file =
  UploadIO.new(
    File.new('./images/image.jpg'), # POST するファイル
    'image/jpeg',                   # ファイルの Content-Type
    'image.jpg'                     # POST されたときのファイル名
  )

req =
  Net::HTTP::Post::Multipart.new(
    url.path,
    username: 'emattsan',
    content: 'Hello',
    file: file
  )

http = Net::HTTP.new(url.host, url.port)

http.use_ssl = true

http.request(req)

自力で multipart を書く

有用な gem があるのでわざわざ自力で書かなくてもよいのですが、multipart の理解のため、また万が一公開されている gem を利用できないケースのため、一例を上げておきます。

multipart ではバウンダリで区切られた複数のコンテンツを POST の body に記述します。

えー…、詳しくは公式の資料とか Multipart messages - MIME - Wikipedia とか参照してみてください。

require 'net/http'

boundary = 'DiscordMultipartMessage'

url = URI.parse(ENV['WEBHOOK_URL'])

req =
  Net::HTTP::Post.new(
    url.path,
    'Content-Type': "multipart/form-data; boundary=#{boundary}"
  )

# body にバウンダリで区切ったフィールドを記述する
# このときファイルは読み込んだファイルの内容を挿入します
req.body = <<~BODY
--#{boundary}
Content-Disposition: form-data; name="username"

emattsan

--#{boundary}
Content-Disposition: form-data; name="content"

Hello

--#{boundary}
Content-Disposition: form-data; name="file"; filename="image.jpg"

#{File.read('./images/image.jpg')}

--#{boundary}--
BODY

http = Net::HTTP.new(url.host, url.port)

http.use_ssl = true

http.request(req)

いつか読むはずっと読まない:組合せ数学

組合せ論って面白いですよね。 読んでいるといつの間にか「どう書く」の問題を考えてしまっています。

Raspberry Pi 用タッチスクリーンを Elixir で利用する

以前購入した Raspberry Pi 用のタッチスクリーンを Elixir から利用する方法をまとめましたので、ご報告がてらの記事です。

今回、表示周りには Scenic を、入力周りには InputEvent を利用しています。 それぞれの詳細については、各々の資料をご参照ください。

Raspberry Pi のセットアップについても各所によい資料がありますので、そちらのご参照をお願いします。

また入力のドライバの記述は scenic_driver_nerves_touch を参考にしています。

…と言いますか、元々は scenic_driver_nerves_touch で済ませるつもりだったんですが、購入したタッチスクリーンに対応していなかったので自分で書き直したというのが本当のところ。

今回の作業はすべて Raspberry Pi 上で行っています。

また最終的に記述したコードは GitHub にも push しています。

InputEvent - タッチスクリーンの入力を得る

まず InputEvent を使って入力を取得するところから始めます。

適当なプロジェクトを用意します。

$ mix new touch_screen
$ cd touch_screen

mix.exs を編集して、depsinput_event を追加します。

  defp deps do
    [
      {:input_event, "~> 0.4"}
    ]
  end

取得できるイベントを確認する

依存パッケージを取得して iex を起動します。

$ mix desp.get
$ iex -S mix

InputEvent.enumerate/0 で利用できるデバイスの一覧を取得します。

iex(1)> InputEvent.enumerate()
[
  {"/dev/input/event0",
   %InputEvent.Info{
     bus: 0,
     input_event_version: "1.0.1",
     name: "ADS7846 Touchscreen",
     product: 0,
     report_info: [
       ev_abs: [
         abs_x: %{flat: 0, fuzz: 0, max: 4095, min: 0, resolution: 0, value: 0},
         abs_y: %{flat: 0, fuzz: 0, max: 4095, min: 0, resolution: 0, value: 0},
         abs_pressure: %{
           flat: 0,
           fuzz: 0,
           max: 65535,
           min: 0,
           resolution: 0,
           value: 0
         }
       ],
       ev_key: [:btn_touch]
     ],
     vendor: 0,
     version: 0
   }}
]

ここから次のようなことがわかります。

これらの知識を持って、実際に入力を確認します。

イベントを取得する

InputEvent は InputEvent.start_link/1 でプロセスを起動すると、起動した親プロセスにメッセージでイベントを送るので、GenServer で親プロセスを実装し、イベントを受けたらログに出力するようにしてみます。

lib/touch_screen.ex を編集します。

defmodule TouchScreen do
  use GenServer

  require Logger

  def start_link(device) do
    GenServer.start_link(__MODULE__, [device: device])
  end

  def init(opts) do
    {path, _} =
      InputEvent.enumerate()
      |> Enum.find(fn {_, %{name: name}} -> name == opts[:device] end)

    InputEvent.start_link(path)

    {:ok, %{path: path}}
  end

  def handle_info(message, state) do
    Logger.debug(inspect(message))

    {:noreply, state}
  end
end

起動時にデバイス名を指定できるようにしました。

バイス名を指定してプロセスを起動します。 起動したらタッチスクリーンに触れて動かしてみてください。 次のようなログが出力されると思います。

iex(1)> TouchScreen.start_link("ADS7846 Touchscreen")
{:ok, #PID<0.189.0>}
iex(2)> 
10:30:45.185 [debug] {:input_event, "/dev/input/event0", [{:ev_key, :btn_touch, 1}, {:ev_abs, :abs_x, 3246}, {:ev_abs, :abs_y, 1151}, {:ev_abs, :abs_pressure, 64778}]}
10:30:45.214 [debug] {:input_event, "/dev/input/event0", [{:ev_abs, :abs_x, 3252}, {:ev_abs, :abs_y, 1162}, {:ev_abs, :abs_pressure, 64753}]}
10:30:45.214 [debug] {:input_event, "/dev/input/event0", [{:ev_abs, :abs_x, 3249}, {:ev_abs, :abs_y, 1196}, {:ev_abs, :abs_pressure, 64745}]}
10:30:45.215 [debug] {:input_event, "/dev/input/event0", [{:ev_abs, :abs_x, 3293}, {:ev_abs, :abs_y, 1247}, {:ev_abs, :abs_pressure, 64724}]}
10:30:45.233 [debug] {:input_event, "/dev/input/event0", [{:ev_abs, :abs_x, 3156}, {:ev_abs, :abs_y, 1341}, {:ev_abs, :abs_pressure, 64777}]}
10:30:45.250 [debug] {:input_event, "/dev/input/event0", [{:ev_abs, :abs_x, 3050}, {:ev_abs, :abs_y, 1453}, {:ev_abs, :abs_pressure, 64695}]}
10:30:45.270 [debug] {:input_event, "/dev/input/event0", [{:ev_abs, :abs_x, 2944}, {:ev_abs, :abs_y, 1546}, {:ev_abs, :abs_pressure, 64835}]}
10:30:45.290 [debug] {:input_event, "/dev/input/event0", [{:ev_abs, :abs_x, 2894}, {:ev_abs, :abs_y, 1603}, {:ev_abs, :abs_pressure, 64888}]}
10:30:45.310 [debug] {:input_event, "/dev/input/event0", [{:ev_abs, :abs_x, 2872}, {:ev_abs, :abs_y, 1626}, {:ev_abs, :abs_pressure, 64895}]}
10:30:45.330 [debug] {:input_event, "/dev/input/event0", [{:ev_abs, :abs_x, 2882}, {:ev_abs, :abs_y, 1618}, {:ev_abs, :abs_pressure, 64886}]}
10:30:45.350 [debug] {:input_event, "/dev/input/event0", [{:ev_abs, :abs_x, 2886}, {:ev_abs, :abs_y, 1609}, {:ev_abs, :abs_pressure, 64898}]}
10:30:45.368 [debug] {:input_event, "/dev/input/event0", [{:ev_key, :btn_touch, 0}, {:ev_abs, :abs_pressure, 0}]}

メッセージは次の 3 つの値のタプルで構成されていることが確認できます。

  • :input_event
  • バイスのパス
  • イベントのリスト
    • 先に確認した touch, x, y, pressure の 4 種類のイベントのリストになっています

handle_info/2 の引数を次のように書くことで、指定したデバイスの InputEvent のメッセージのみをハンドリングすることができそうです。

  def handle_info({:input_event, path, events}, %{path: path} = state) do
    # ...
  end

一つのイベントに畳み込む

イベントは touch や x や y とった要素に別れた状態で受け取りますが、利用するばあいには「(x, y) の位置にタッチされた」といった一つのイベントになっていた方が扱いやすくなります。

Enum.reduce/2 を使って、複数のイベントの要素を一つのマップに畳み込みます(このあと pressure の値を使う予定がないので、pressure は畳み込みの対象から外しています)。

加えて、畳み込んだ結果の :touch の値を調べて 1 であれば down0 であれば up 、それ以外であれば move と扱うようにしました。 これら down, up, move のイベントは、あとで Scenic の入力になります。

  def handle_info({:input_event, path, events}, %{path: path} = state) do
    events
    |> Enum.reduce(%{touch: nil, x: 0, y: 0}, fn
      {:ev_abs, :abs_x, x}, event -> %{event | x: x}
      {:ev_abs, :abs_y, y}, event -> %{event | y: y}
      {:ev_key, :btn_touch, touch}, event -> %{event | touch: touch}
      _, event -> event
    end)
    |> case do
      %{touch: 1, x: x, y: y} ->
        Logger.debug("down (#{x}, #{y})")

      %{touch: 0, x: x, y: y} ->
        Logger.debug("up (#{x}, #{y})")

      %{x: x, y: y} ->
        Logger.debug("move (#{x}, #{y})")

      event ->
        Logger.debug("unknown event #{inspect(event)}")
    end

    {:noreply, new_state}
  end

Scenic - タッチスクリーンに表示する

次に Scenic を使ってタッチスクリーンに表示してみます。

プロジェクトを用意する

まず Scenic のプロジェクトを作成するために、 scenic_new をインストールします。

hex.pm

$ mix archive.install hex scenic_new

mix scenic.new というタスクが追加されるので、これを使って新しいプロジェクトを作成します。

$ mix scenic.new touch_screen_scenic
$ cd touch_screen_scenic

mix.exsdeps/0 を編集します。

Raspberry Pi の環境ですので、 scenic_driver_glfwscenic_driver_nerves_rpi を置き換えます。

また入力では input_event を利用するので、これも追加しておきます。

なお、この記事を書いている 2021-05-23 現在、font_metrics の最新は 0.5.1 なのですが Scenic がこのバージョンに対応していない様子でした。 このため、font_metrics のバージョンを 0.3.x に固定するために font_metricsdeps/0 に記述しておきます。

  defp deps do
    [
      {:scenic, "~> 0.10"},
      {:scenic_driver_nerves_rpi, "~> 0.10.1"},
      {:input_event, "~> 0.4"},
      {:font_metrics, "~> 0.3.0"}
    ]
  end

依存するパッケージを取得しコンパイルします。

$ mix deps.get
$ mix deps.compile

もし C のコードのコンパイルに失敗するばあいには、include と lib のパスを追加して試してみてください。

$ export C_INCLUDE_PATH=/opt/vc/include:$C_INCLUDE_PATH
$ export LIBRARY_PATH=/opt/vc/lib:$LIBRARY_PATH

config を編集する

config/config.exs を編集し設定をデバイスに合わせます。

:size を利用している表示のサイズに合わせます。 サイズは /boot/config.txthdmi_cvt で設定されていると思いますのでその値にします。 わたしは 480x320 で利用しているので、size: {480, 320} と設定しています。

また、ドライバを scenic_driver_nerves_rpi に置き換えましたので、ドライバに指定するモジュールも Scenic.Driver.Nerves.Rpi に変更します。

config :touch_screen_scenic, :viewport, %{
  name: :main_viewport,
  size: {480, 320},
  default_scene: {TouchScreenScenic.Scene.Home, nil},
  drivers: [
    %{
      module: Scenic.Driver.Nerves.Rpi
    }
  ]
}

表示を確認します。

iex -S mix もしくは mix scenic.run で起動してみてください。 問題がなければスクリーンに lib/scenes/home.ex に記述された文字列( This is a very simple starter application. 等)が表示されると思います。

入力用のモジュールを用意する

先ほど入力を確認するために書いたモジュールを再利用します。 このあとドライバとして利用する予定ですので、lib/touch_screen_scenic/driver.ex にコピーしてモジュール名も TouchScreenScenic.Driver としておきます。

defmodule TouchScreenScenic.Driver do
  use GenServer

  # ...
end

iex -S mix で起動し、モジュールが機能することを確認しておきます。

iex(1)>TouchScreenScenic.Driver.start_link("ADS7846 Touchscreen")
{:ok, #PID<0.203.0>}
iex(2)> 
10:46:21.065 [debug] down (2921, 1721)
10:46:21.081 [debug] move (2879, 1718)

calibration

先にタッチスクリーンの入力は x, y それぞれ 0〜4095 ということを確認しました。 一方で表示領域のサイズは 480x320 です。

これをタッチした位置と表示の位置が一致するようにキャリブレーションします。

キャリブレーションの方法

結論から言うと、タッチした位置 (x_touch, y_touch) から表示の位置 (x_screen, y_screen) は、次のような式で求めることができます。

x_screen = ax * x_touch + bx * y_touch + dx
y_screen = ay * x_touch + by * y_touch + dy

この 6 つの定数 ax, bx, dx, ay, by, dy は 3 点での対応がわかれば算出することができます。

Texas Instruments の ADS7846 のデータシートのページからキャリブレーションの方法についてのドキュメントのリンクがあります。 詳しくはこれらの資料を確認してみてください。

タッチの位置と表示の位置の対応を調べる

スクリーン上に指定した位置に表示した図形をタッチし、その時の入力を確認することで、タッチした位置と表示の位置の対応を調べます。

ここでは、スクリーンの上下左右それぞれ 20 ピクセルの位置に縦横 2 本ずつの線を表示します。

lib/scenes/home.ex の init/2 を次のように編集します。

  def init(_, opts) do
    graph =
      Graph.build(font: :roboto, font_size: @text_size)
      |> line({{0, 20}, {480, 20}}, stroke: {1, :white})
      |> line({{0, 300}, {480, 300}}, stroke: {1, :white})
      |> line({{20, 0}, {20, 320}}, stroke: {1, :white})
      |> line({{460, 0}, {460, 320}}, stroke: {1, :white})

    {:ok, graph, push: graph}
  end

iex -S mix で起動すると 4 本の線が表示され、交点が 4 つできます。 このうちの 3 点をタッチしそのときのログを確認します。

わたしが確認したときは次のような値を得ることができました。

表示上の点 タッチした点
左上 (20, 20) (3653, 3777)
右上 (460, 20) (3682, 369)
右下 (460, 300) (425, 352)

ここから行列演算すれば欲しい値が得られるわけですが、計算はそれらが得意な言語にまかせることにします。

おもむろに Julia を起動して計算します。

julia> a = [3653 3777 1; 3682 369 1; 425 352 1]
3×3 Matrix{Int64}:
 3653  3777  1
 3682   369  1
  425   352  1

a の逆行列をスクリーン上の 3 点の x 座標にかけると、ax, bx, dx が求まります。

julia> inv(a) * [20; 460; 460]
3-element Vector{Float64}:
   0.000673852686974119
  -0.1291022471455627
 505.15760360327397

同様に ay, by, dy を求めます。

julia> inv(a) * [20; 20; 300]
3-element Vector{Float64}:
  -0.0859648647083078
  -0.0007315085318488684
 336.7925585042416

4 桁目で丸めるてだいたいこれくらい。

ax = 0.0006739
bx = -0.1291
dx = 505.2
ay =-0.08596
by = -0.0007315
dy = 336.8

入力を扱う TouchScreenScenic.Driverキャリブレーションのコードを追加します。

calibrate/1 を追加して、受け取ったイベントの x, y の値を変換します。

あらためて iex -S mix で起動し直します。 TouchScreenScenic.Driver.start_link("ADS7846 Touchscreen") で入力プロセスを起動しスクリーンにタッチすると、今度はスクリーンの位置とタッチした位置がおおよそ一致することが確認できると思います。

  def handle_info({:input_event, path, events}, %{path: path} = state) do
    events
    |> Enum.reduce(%{touch: nil, x: 0, y: 0}, fn
      {:ev_abs, :abs_x, x}, event -> %{event | x: x}
      {:ev_abs, :abs_y, y}, event -> %{event | y: y}
      {:ev_key, :btn_touch, touch}, event -> %{event | touch: touch}
      _, event -> event
    end)
    |> case do
      %{touch: 1, x: x, y: y} ->
        Logger.debug("down #{inspect(calibrarte({x, y}))}")

      {%{touch: 0, x: x, y: y}, _} ->
        Logger.debug("up #{inspect(calibrarte({x, y}))}")

      %{x: x, y: y} ->
        Logger.debug("move #{inspect(calibrarte({x, y}))}")

      event ->
        Logger.debug("unknown event #{inspect(event)}")
    end

    {:noreply, new_state}
  end

  defp calibrate({x, y}) do
    ax = 0.0006739
    bx = -0.1291
    dx = 505.2
    ay =-0.08596
    by = -0.0007315
    dy = 336.8

    {
      ax * x + bx * y + dx,
      ay * x + by * y + dy
    }
  end

Scenic 用ドライバを書く

ここまででスクリーンへの表示とタッチ入力の取得ができるようになりました。 最後に TouchScreenScenic.Driver が Scenic のドライバとして利用できるように、手を加えていきます。

TouchScreenScenic.Driver を Scenic のドライバに指定する

まず TouchScreenScenic.Driver を Scenic のドライバとして登録します。

すでに Scenic.Driver.Nerves.Rpi の登録で見たように、ドライバとして登録するには config/config.exs を編集し、:drivers のリストに設定を追加します。

今回はドライバに渡す設定(デバイス名とキャリブレーションの数値)も一緒に記述します。 ドライバの設定はマップからできていて、キー :module にモジュールの識別子を指定します。

次にキー :opts に、ドライバに渡したい値を記述します。 ここでは :device にデバイス名を、 :calibrationキャリブレーションの数値を記述します。 この :opts の値は後述の init/3 に第 3 引数としてそのまま渡ります。

config :touch_screen_scenic, :viewport, %{
  name: :main_viewport,
  size: {480, 320},
  default_scene: {TouchScreenScenic.Scene.Home, nil},
  drivers: [
    %{
      module: Scenic.Driver.Nerves.Rpi
    },
    %{
      module: TouchScreenScenic.Driver,
      opts: [
        device: "ADS7846 Touchscreen",
        calibration: {
          {0.0006739, -0.1291, 505.2},
          {-0.08596, -0.0007315, 336.8}
        }
      ]
    }
  ]
}

Scenic.ViewPort.Driver を use する

Scenic のドライバとして利用するには、use で GenServer の代わりに Scenic.ViewPort.Driver を指定します。

defmodule TouchScreenScenic.Driver do
  use Scenic.ViewPort.Driver

  # ...
end

実装を確認すると Scenic.ViewPort.DriverGenServer として起動され、メッセージを受信したらそれを use したモジュールに転送する、というしくみになっています。

このようなしくみため、 GenServer を起動するための関数 start_link/1 が不要になります。 Scenic.ViewPort.Driver から削除します。

init/3 - ドライバを初期化する

初期化のときは GenServerinit/2 に代わり init/3 がコールバックで呼び出されるようになります。

init/3 は 3 つの引数をとります。

  1. Scenic.ViewPort プロセスの pid 。入力したイベントをこのプロセスに送ります
  2. スクリーンサイズ。config/config.exs で :size に指定した値です。今回は利用しません
  3. config/config.exs で :opts に設定したオプション。config/config.exs で設定したデバイス名とキャリブレーションの値をここから取り出します

また ViewPort の pid とキャリブレーションの値はタッチ入力のイベントを受けたときに利用しますので、state に格納しておきます。

  def init(viewport, {_, _}, opts) do
    {path, _} =
      InputEvent.enumerate()
      |> Enum.find(fn {_, %{name: name}} -> name == opts[:device] end)

    InputEvent.start_link(path)

    {:ok, %{path: path, viewport: viewport, calibration: opts[:calibration]}}
  end

handle_info/2 & calibrate/2

最後に handle_info/2 でイベントを受け取ったら Scenic.ViewPort.input/2 に送る処理を書きます。

down, up, move と書いたイベントは、 Scenic.ViewPort.input/2 のイベントに対応づけることができます。

入力 Scenic.ViewPort.input/2 に送るイベント
down :cursor_button:press
up : cursor_button:release
move :cursor_pos

:cursor_button はボタンの種類(左、右、中央)を指定できますが、ここでは左ボタン :left として扱うことにします。

他にどのようなイベントを指定できるかはScenic.ViewPort.input/2 のドキュメントを参照してみてください。

またキャリブレーションは、state に格納した :calibration の値を渡して計算するように修正します。

  def handle_info({:input_event, path, events}, %{path: path} = state) do
    events
    |> Enum.reduce(%{touch: nil, x: 0, y: 0}, fn
      {:ev_abs, :abs_x, x}, event -> %{event | x: x}
      {:ev_abs, :abs_y, y}, event -> %{event | y: y}
      {:ev_key, :btn_touch, touch}, event -> %{event | touch: touch}
      _, event -> event
    end)
    |> case do
      %{touch: 1, x: x, y: y} ->
        pos = calibrate(state.calibration, {x, y})
        Logger.debug("down #{inspect(pos)}")
        Scenic.ViewPort.input(state.viewport, {:cursor_button, {:left, :press, 0, pos}})

      %{touch: 0, x: x, y: y} ->
        pos = calibrate(state.calibration, {x, y})
        Logger.debug("up #{inspect(pos)}")
        Scenic.ViewPort.input(state.viewport, {:cursor_button, {:left, :release, 0, pos}})

      %{x: x, y: y} ->
        pos = calibrate(state.calibration, {x, y})
        Logger.debug("move #{inspect(pos)}")
        Scenic.ViewPort.input(state.viewport, {:cursor_pos, pos})

      event ->
        Logger.debug("unknown event #{inspect(event)}")
    end

    {:noreply, state}
  end

  defp calibrate({{ax, bx, dx}, {ay, by, dy}}, {x, y}) do
    {
      (ax * x) + (bx * y) + dx,
      (ay * x) + (by * y) + dy
    }
  end

TouchScreenScenic.Driver 全体像

最終的に TouchScreenScenic.Driver は次のようになりました。

defmodule TouchScreenScenic.Driver do
  use Scenic.ViewPort.Driver

  require Logger

  def init(viewport, {_, _}, opts) do
    {path, _} =
      InputEvent.enumerate()
      |> Enum.find(fn {_, %{name: name}} -> name == opts[:device] end)

    InputEvent.start_link(path)

    {:ok, %{path: path, x: 0, y: 0, viewport: viewport, calibration: opts[:calibration]}}
  end

  def handle_info({:input_event, path, events}, %{path: path} = state) do
    events
    |> Enum.reduce(%{touch: nil, x: 0, y: 0}, fn
      {:ev_abs, :abs_x, x}, event -> %{event | x: x}
      {:ev_abs, :abs_y, y}, event -> %{event | y: y}
      {:ev_key, :btn_touch, touch}, event -> %{event | touch: touch}
      _, event -> event
    end)
    |> case do
      %{touch: 1, x: x, y: y} ->
        pos = calibrate(state.calibration, {x, y})
        Logger.debug("down #{inspect(pos)}")
        Scenic.ViewPort.input(state.viewport, {:cursor_button, {:left, :press, 0, pos}})

      %{touch: 0, x: x, y: y} ->
        pos = calibrate(state.calibration, {x, y})
        Logger.debug("up #{inspect(pos)}")
        Scenic.ViewPort.input(state.viewport, {:cursor_button, {:left, :release, 0, pos}})

      %{x: x, y: y} ->
        pos = calibrate(state.calibration, {x, y})
        Logger.debug("move #{inspect(pos)}")
        Scenic.ViewPort.input(state.viewport, {:cursor_pos, pos})

      event ->
        Logger.debug("unknown event #{inspect(event)}")
    end

    {:noreply, state}
  end

  defp calibrate({{ax, bx, dx}, {ay, by, dy}}, {x, y}) do
    {
      (ax * x) + (bx * y) + dx,
      (ay * x) + (by * y) + dy
    }
  end
end

Scenic で利用する

最後に、本当に Scenic でイベントを受け取れるか、確認します。

lib/scenes/home.ex を次のように編集します。

defmodule TouchScreenScenic.Scene.Home do
  use Scenic.Scene
  require Logger

  alias Scenic.Graph

  import Scenic.Primitives
  import Scenic.Components

  @text_size 24

  def init(_, _opts) do
    graph =
      Graph.build(font: :roboto, font_size: @text_size)
      |> button("Greet", id: :btn_greet, translate: {200, 140}, width: 80, height: 40)
      |> text("", id: :text_greet, translate: {240, 200}, text_align: :center)

    {:ok, %{graph: graph, text: ""}, push: graph}
  end

  def filter_event({:click, :btn_greet}, _, %{graph: graph, text: text} = state) do
    {caption, text} =
      case text do
        "" -> {"Clear", "Hello!"}
        _ -> {"Greet", ""}
      end

    graph =
      graph
      |> Graph.modify(:btn_greet, &button(&1, caption))
      |> Graph.modify(:text_greet, &text(&1, text))

    {:noreply, %{state | graph: graph, text: text}, push: graph}
  end

  def handle_input(event, _context, state) do
    Logger.info("Received event: #{inspect(event)}")
    {:noreply, state}
  end
end

実行すると、Greet と書かれたボタンが一つスクリーンの中央に表示されます。

ボタンを押すと、ボタンの下に Hello! と表示されるはずです。 ボタンの表示は Clear に替わり、もう一度ボタンを押すと Hello! の文字が消えます。

ドライバがきちんと設定できていれば、ボタンが表示されている領域を押したときだけボタンが反応してくれます。

いつか読むはずっと読まない:知恵の箱、金の鍵、勇気の棒、願いの杖、…

「知恵の箱」 自分にとって何が正しくて何が正しくないかを見分ける能力。

「金の鍵」 新たに学習し、実践する分野への扉を開き、それが今の自分に合わなければ閉める能力。

「勇気の棒」 新しいことに挑戦し、失敗の危険を犯す勇気。

「願いの杖」 自分の欲しいものを求め、必要とあれば、それが手に入らなくてもどうにかする能力。

何年も前に手にした本です。 当時もこの本に勇気付けられた。

あらためてページを開くと、当時よりもより響く感じがします。 当時よりものごとが見えるようになったから、と思いたいところ。

あらためて読み返してみようと思います。

コンサルタントの道具箱

コンサルタントの道具箱

ビットとバイトとストリングと in Elixir

Elixir でビット操作を行うコードを書いたんですが、使うべき関数やガードで混乱しそうになったので、明日の自分のためにまとめてみました。

ガード

# 入力 is_binary/1 is_bitstring/1
1 "あいう" true true
2 <<12, 34>> true true
3 <<12::5, 34::5>> false true

is_binary/1 はバイト列(ビット長が 8 の倍数になっているビット列)の場合に true になります。 上記の 3 番目は 10 ビットのビット列なので false を返します。

全体で 8 の倍数であれば true になりますので、次のように書いても true になります。

iex> is_binary(<<12::3, 34::5>>)
true

パタンマッチング

パタンマッチングでも binary で受ける場合には、そのビット長が 8 の倍数になっている必要があります。 端数があるとエラーになります。

iex> <<a, rest::binary>> = ""
""

iex> a
227

iex> rest
<<129, 130>>
iex> <<a, rest::binary>> = <<123, 234>>
<<123, 234>>

iex> a
123

iex> rest
<<234>>
iex> <<a, rest::binary>> = <<123::5, 234::5>>
** (MatchError) no match of right hand side value: <<218, 2::size(2)>>

任意の長さのビット列を受けるには bitstring を指定します。

iex> <<a, rest::bitstring>> = ""
""

iex> a
227

iex> rest
<<129, 130>>
iex> <<a, rest::bitstring>> = <<123, 234>>
<<123, 234>>

iex(79)> a
123

iex(80)> rest
<<234>>
iex(81)> <<a, rest::bitstring>> = <<123::5, 234::5>>
<<218, 2::size(2)>>

iex(82)> a
218

iex(83)> rest
<<2::size(2)>>

長さとサイズ

# 入力\関数 String.length/1 byte_size/1 bit_size/1
1 "あいう" 3 9 72
2 <<123, 234>> 2 2 16
3 <<12::5, 34::5>> エラー*1 2 10

*1 ** (FunctionClauseError) no function clause matching in String.Unicode.length/1

String.lenth/1UTF-8 としての文字数を返します。 ビット列の長さが 8 の倍数でない場合、エラーが発生します。

byte_size/1 はバイト数を返しますが、興味深いのはビット列の長さが 8 の倍数でないばあいでもエラーにはならず、8 で割って切り上げた数字を返す点です。

ちなみに lengthsize という名前について。 実行が線形時間のものには length を、定数時間のものには size をつけるようにしてるとのこと。

また、byte_size/1bit_size/1 はガードに記述できるためか、ドキュメントでは Functions でなく Guards に記載されています。

# 内容
1 binary() バイト列(長さが 8 の倍数のビット列)
2 bitstring() ビット列
3 String.t() 文字列(binary()エイリアス
4 string() Erlang の文字列(文字リスト charlist() に同じ)

"string" という単語が 3 回も出てくるので、うろ覚えでいると足をすくわれそうです。

String.t()binary() と同じものをさしていますが、値が文字列(UTF-8 エンコーディングのバイナリ)であることをドキュメントで明確にしたいばあいのために用意されているようです。

また string()Erlang の文字列型であるため、Elixir 内では charlist() を使うように勧められています。

やりたかったこと

「任意のビット列を任意のサイズで分割し、左詰めでバイト列にしたリストを返す」という機能を実現すべく四苦八苦していました。

defmodule Bin do
  @spec chunk_every(bitstring(), pos_integer()) :: [binary()]
  def chunk_every(bin, n) when is_bitstring(bin) and is_integer(n) and n > 0 do
    padding_size = rem(8 - rem(n, 8), 8)

    bin
    |> Stream.unfold(fn
      <<>> ->
        nil

      <<chunk::size(n), rest::bitstring>> ->
        {<<chunk::size(n), 0::size(padding_size)>>, rest}


      chunk ->
        chunk_size = bit_size(chunk)
        padding_size = n - chunk_size + padding_size
        {<<chunk::bitstring, 0::size(padding_size)>>, <<>>}
    end)
    |> Enum.to_list()
  end
end

たとえば。 60 ビットのビット列を 12 ビットごとに分割し、分割したそれぞれのビットを左詰めしたバイト列のリストとして取得したいばあい。

このばあい、12 ビットのビット列は 16 ビット = 2 バイトのバイト列に収まるので、2 バイトのバイト列のリストが返ります。

iex> Bin.chunk_every(<<0x123456789abcdef::60>>, 12)                    
[<<18, 48>>, "E`", <<120, 144>>, <<171, 192>>, <<222, 240>>]

結果がわかりにくいので、パタンマッチングで確認。

iex> Bin.chunk_every(<<0x123456789abcdef::60>>, 12) == [<<0x12, 0x30>>, <<0x45, 0x60>>, <<0x78, 0x90>>, <<0xab, 0xc0>>, <<0xde, 0xf0>>]
true

いつか読むはずっと読まない: ≥ 1.6

第 2 版の邦訳が出版されていることを、つい最近になって知りました。 不覚。 買わねば。

プログラミング Elixir(第2版)

プログラミング Elixir(第2版)

  • 作者:Thomas,Dave
  • 発売日: 2020/12/01
  • メディア: 単行本

ElixirでPNGを出力するための覚書

defmodule PNG do
  @moduledoc """
  PNG イメージを作成します

  cf. https://en.wikipedia.org/wiki/Portable_Network_Graphics
  """

  @magic_number [0x89, "PNG", 0x0D, 0x0A, 0x1A, 0x0A]

  @width 32
  @height 8

  @depth 8
  @color_type 3 # indexed

  @compression_method 0
  @filter_method 0
  @interlace_method 0

  @header <<
    @width::32,
    @height::32,
    @depth,
    @color_type,
    @compression_method,
    @filter_method,
    @interlace_method
  >>

  @palette [
    0x00, 0x00, 0x00, # パレット番号0 - R:0, G:0, B:0
    0xFF, 0xFF, 0xFF  # パレット番号1 - R:255, G:255, B:255
  ]

  @image [
    [ 0, 0, 0, 0,   0, 0, 0, 0,   0, 0, 0, 0,   0, 0, 0, 0,
      1, 0, 0, 1,   0, 0, 0, 0,   0, 0, 0, 0,   0, 0, 0, 0 ],
    [ 0, 1, 1, 0,   0, 1, 1, 0,   1, 0, 0, 0,   1, 1, 0, 0,
      1, 0, 0, 1,   0, 0, 1, 1,   0, 0, 1, 1,   0, 1, 1, 0 ],
    [ 1, 0, 0, 1,   0, 1, 0, 1,   0, 1, 0, 1,   0, 1, 0, 1,
      1, 1, 1, 1,   1, 1, 0, 0,   0, 1, 0, 1,   0, 1, 0, 1 ],
    [ 1, 0, 0, 1,   0, 1, 0, 1,   0, 1, 0, 1,   0, 1, 0, 0,
      1, 0, 0, 1,   0, 1, 0, 0,   0, 1, 0, 1,   0, 1, 0, 1 ],
    [ 1, 1, 1, 1,   0, 1, 0, 1,   0, 1, 0, 1,   0, 1, 0, 0,
      1, 0, 0, 1,   0, 0, 1, 0,   0, 1, 0, 1,   0, 1, 0, 1 ],
    [ 1, 0, 0, 0,   0, 1, 0, 1,   0, 1, 0, 1,   0, 1, 0, 0,
      1, 0, 0, 1,   0, 0, 0, 1,   0, 1, 0, 1,   0, 1, 0, 1 ],
    [ 1, 0, 0, 1,   0, 1, 0, 1,   0, 1, 0, 1,   0, 1, 0, 0,
      1, 0, 0, 1,   0, 0, 0, 1,   0, 1, 0, 1,   0, 1, 0, 1 ],
    [ 0, 1, 1, 0,   0, 1, 0, 1,   0, 1, 0, 0,   1, 0, 1, 0,
      0, 1, 0, 0,   1, 1, 1, 0,   0, 0, 1, 0,   1, 1, 0, 1 ]
  ]

  @scanline_filter_none 0

  def write_image(filename) do
    rows =
      Enum.map(@image, fn row ->
        [@scanline_filter_none, row]
      end)

    File.open(filename, [:write], fn io ->
      IO.binwrite(io, @magic_number)
      write_chunk(io, "IHDR", @header)
      write_chunk(io, "PLTE", @palette)
      write_chunk(io, "IDAT", compress(rows))
      write_chunk(io, "IEND", "")
    end)
  end

  def write_chunk(io, type, data) do
    length = :erlang.iolist_size(data)
    crc = :erlang.crc32([type, data])

    IO.binwrite(io, <<length::big-size(32)>>)
    IO.binwrite(io, type)
    IO.binwrite(io, data)
    IO.binwrite(io, <<crc::big-size(32)>>)
  end

  @doc """
  Erlang の zlib モジュールを利用してデータを圧縮します

  see http://erlang.org/doc/man/zlib.html
  """
  def compress(data) do
    zip = :zlib.open()
    :ok = :zlib.deflateInit(zip)
    compressed = :zlib.deflate(zip, data, :finish)
    :ok = :zlib.deflateEnd(zip)
    :ok = :zlib.close(zip)

    compressed
  end
end
$ elixirc png.ex
$ elixir -e 'PNG.write_image("emattsan.png")'

ElixirからErlangのASN.1のライブラリを利用するための覚書

Erlang には、 ASN.1 で記述されたデータ構造のコンパイルと、エンコード/レコードをおこなうライブラリが標準で用意されています。

erlang.org

ja.wikipedia.org

かつて通信機器の開発が仕事だったころにあつかっていたことを思い出しつつ、Elixir で利用する方法を記録しておきます。

とはいえ。 ASN.1 については Erlangチュートリアルそのままで、生成した Erlang のモジュールをどのように Elixir から利用するか、というお話がすべてです。

Erlang で ASN.1 のモジュールを生成する

Erlang のマニュアルの Getting Started にしたがって、モジュールを作成します。

まず、定義ファイル People.asn を作成します。

People DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
  Person ::= SEQUENCE {
    name PrintableString,
    location INTEGER {home(0),field(1),roving(2)},
    age INTEGER OPTIONAL
  }
END

次にこれをコンパイルしてモジュールを生成します。 Elixir から Erlang のモジュールや関数を利用するばあいは、特に引数の記述の違いに注意してください。

iex> :asn1ct.compile('People', [:per])
:ok

コンパイルに成功すると、次のファイルが生成されます。

People.asn1db
People.beam
People.erl
People.hrl

生成されたモジュールの関数を Elixir から呼び出す

コンパイルを実行した iex から、あるいは生成されたファイルがあるディレクトリで iex を起動してそこから、次の関数を実行します。

iex> {:ok, bin} = :People.encode(:Person, {:Person, 'Some Name', :roving, 50})
{:ok, <<128, 9, 83, 111, 109, 101, 32, 78, 97, 109, 101, 1, 2, 1, 50>>}

ここで Erlang のモジュール名が People であるため、Elixir からは :People という名前で参照することに注意してください。

続いてエンコードした結果をデコードしてみます。

iex> :People.decode(:Person, bin)
{:ok, {:Person, 'Some Name', :roving, 50}}

元のデータに復元できることが確認できました。

生成されたモジュールを Elixir のプロジェクトに含める

生成した Erlang のモジュールを mix new コマンドで作成する Elixir のプロジェクトで利用できるようにします。

Elixir のプロジェクトでは、コンパイル対象にする Erlang のソースファイルの格納パスが src/ に設定されています。 このため作成したプロジェクト内に src/ というディレクトリを作成し、そこに先に生成された Erlang のソースファイルとヘッダファイルをコピーするだけでモジュールを利用できるようになります。

my_app/
└── src/
    ├── People.erl
    └── People.hrl

コンパイル対象にするパスは、 mix.exsproject/0:erlc_paths を設定することで変更することができます。 くわしくは Mix.Project — Mix v1.11.3mix compile.erlang — Mix v1.11.3 を参照してみてください。

ファイルのコピーができたら、エンコード/デコードができることを確認してみます。

my_app $ iex -S mix

iex(1)> {:ok, bin} = :People.encode(:Person, {:Person, 'Some Name', :roving, 50})
{:ok, <<128, 9, 83, 111, 109, 101, 32, 78, 97, 109, 101, 1, 2, 1, 50>>}

iex(2)> {:ok, result} = :People.decode(:Person, bin)
{:ok, {:Person, 'Some Name', :roving, 50}}

Rustlerを使ってElixirのNIFを書たときに異なる型の要素を含むリストをあつかいたいばあいの覚書

結論: Vec<Term<'a>> 型にデコードするとよい

リスト全体は Vec<T> 型で受けるとして、T になにを書けばよいのかしばし悩んだのですが、Erlant Term のままあつかうのでよければ Term<'a> 型で受ければよいということに気がつきました。

fn count<'a>(env: Env<'a>, args: &[Term<'a>]) -> Result<Term<'a>, Error> {
    let list: Vec<Term<'a>> = args[0].decode()?;

    Ok(list.len().encode(env))
}

各要素の具体的な値を利用したいばあいは、おのおの必要になった時点でデコードすればよさそうです。

実例

結論を書いてしまったので、以下は Rustler を使う手順の一般的な覚書です。

Rustler を導入する

Rustler は Rust で NIF を書くための便利なライブラリです。 上に書いたコードのように、とても簡単に Rust と Elixir をインタフェースしてくれます。

hex.pm

プロジェクトを作成し、依存するパッケージに Rustler を追加します。

$ mix new my_app
$ cd my_app
  defp deps do
    [
      {:rustler, "~> 0.21"}
    ]
  end

パッケージを導入すると mix rustler.new コマンドが利用できるようになります。

$ mix do deps.get, deps.compile
$ mix help
...
mix rustler.new         # Creates a new Rustler project
...

NIF の雛形を生成する

追加された mix rustler.new コマンドを使っって NIF の雛形を生成します。

モジュール名を訊かれるので入力します。

続けてライブラリ名を訊かれます。 モジュール名から生成した名前が示されるので、その名前でよければそのまま決定します。

native という名前のディレクトリの下に指定したライブラリ名でパッケージが作成されます。

$ mix rustler.new
This is the name of the Elixir module the NIF module will be registered to.
Module name > MyApp.List
This is the name used for the generated Rust crate. The default is most likely fine.
Library name (myapp_list) >
* creating native/myapp_list/.cargo/config
* creating native/myapp_list/README.md
* creating native/myapp_list/Cargo.toml
* creating native/myapp_list/src/lib.rs

Rust のコードを編集する

native/myapp_list/src/lib.rs を編集します。 ここではリストを受け取って要素の数を返す関数を書きます。

要素の数だけを知りたいので、今回は Erlang Term のままであつかい個々の要素はデコードしません。 このため先に書いたとおり、受け取った引数のリストを Vec<Term<'a>> という型にデコードします。

use rustler::{Encoder, Env, Error, Term};

fn count<'a>(env: Env<'a>, args: &[Term<'a>]) -> Result<Term<'a>, Error> {
    let list: Vec<Term<'a>> = args[0].decode()?;

    Ok(list.len().encode(env))
}

rustler::rustler_export_nifs! {
    "Elixir.MyApp.List",
    [
        ("count", 1, count)
    ],
    None
}

NIF をマウントする Elixir のモジュールを作成する

Rust のコードに書かれたモジュール名の Elixir のモジュールを作成します。

モジュールでは Rustler を use し、オプションで OTP アプリケーション名と、Rust のライブラリ名を指定します。

defmodule MyApp.List do
  use Rustler, otp_app: :my_app, crate: :myapp_list

  def count(_list), do: :erlang.nif_error(:nif_not_loaded)
end

コンパイラとパッケージを指定する

mix.exs を編集してコンパイラに Rustler を追加し、利用するパッケージの指定を追加します。

  def project do
    [
      app: :my_app,
      version: "0.1.0",
      elixir: "~> 1.11",
      start_permanent: Mix.env() == :prod,
      compilers: [:rustler | Mix.compilers()], # 追加 - コンパイラに Rustler を追加する
      rustler_crates: [myapp_list: []],        # 追加 - myapp_list を利用するパッケージに追加する
      deps: deps()
    ]
  end

実行

実行します。 コンパイラに Rustler を追加しているので、Elixir のコードがコンパイルされるのと一緒に Rust のパッケージもコンパイルされます。

$ mix run -e 'MyApp.List.count([1, :ok, {}, [], "ABC"]) |> IO.inspect()'
Compiling NIF crate :myapp_list (native/myapp_list)...
    Finished release [optimized] target(s) in 0.19s
Compiling 2 files (.ex)
Generated my_app app
5

いつか読むはずっと読まない:ラスト・モンスターに泣かされるレベル1パーティー

なけなしのお金で購入した金属製のアイテムをことごとく錆びつかせるラスト・モンスターが、キャリオン・クロウラーと並んで低レベルパーティーの憎き敵役だったのもよい思い出。

ダンジョンズ&ドラゴンズ スターター・セット第5版

ダンジョンズ&ドラゴンズ スターター・セット第5版

  • 発売日: 2017/12/18
  • メディア: おもちゃ&ホビー

実践Rust入門[言語仕様から開発手法まで]

実践Rust入門[言語仕様から開発手法まで]

Phoenix LiveView で temporary_assigns を設定したときにどのように DOM を削除するか

動作確認に利用したコードは GitHub に push しました。 この記事で解説しているコードは このコミット にまとまっています。

temporary_assigns を設定したときの削除問題

LiveView では、一部が更新されたときに全体を再描画するのではなく、更新された部分だけを再描画するように設定することができます。

hexdocs.pm

ただし可能なのは、新規の DOM の追加と既存の DOM の更新だけで、削除はできません。 削除するには、該当する DOM を削除した全体をサーバからクライアントに送り、再描画する必要があります。 要素の数が少なければそのような方法も使えますが、数が多くなると送信のコストも再描画のコストも無視できなくなります。

これは Elixir Forum でも質問にあがっていた内容です。

elixirforum.com

解決策としては、Forum のこの質問にも回答されているように、JavaScript のフックを使って削除されたことの情報を持っている DOM を削除するというのが現実的なようです。

hexdocs.pm

と、いうわけで。 具体的に実装してみました。

実装のアイディア

アイディアはいたって単純です。

削除したい要素の DOM を更新して、削除の情報を設定します。 DOM の更新後に呼び出されるフック関数で、削除の情報が設定された DOM を削除します。

前提として。

ここでは Ecto ででデータベースを利用していることを想定しています。 ですが、対応するデータが削除されたことが DOM に反映されるようになっていれば、Ecto でなくてもデータベースのデータでなくても、同じように処理できます。

Ecto.Schema の状態を調べる

Ecto.Schema には :__meta__ という名前でメタデータが格納され、そこには :state という名前で状態が格納されています。

hexdocs.pm hexdocs.pm

メタデータの値を取得する Ecto.get_meta/2 が用意されているので、これで状態を確認できます。

Hoge というプロジェクトの Foos というコンテクストに Bar というスキーマが設定され bars というテーブルがあるような状況で、状態を確認してみます。

iex(1)> {:ok, bar} = Hoge.Foos.create_bar(%{name: "hoge"})
{:ok,
 %Hoge.Foos.Bar{
   __meta__: #Ecto.Schema.Metadata<:loaded, "bars">,
   id: 1,
   inserted_at: ~N[2020-12-30 23:23:13],
   name: "hoge",
   updated_at: ~N[2020-12-30 23:23:13]
 }}

iex(2)> Ecto.get_meta(bar, :state)
:loaded
iex(3)> {:ok, bar} = Hoge.Foos.get_bar!(1) |> Hoge.Foos.delete_bar()
{:ok,
 %Hoge.Foos.Bar{
   __meta__: #Ecto.Schema.Metadata<:deleted, "bars">,
   id: 1,
   inserted_at: ~N[2020-12-30 23:23:13],
   name: "hoge",
   updated_at: ~N[2020-12-30 23:23:13]
 }}

iex(4)> Ecto.get_meta(bar, :state)                                  
:deleted

state の値は次の 3 種類です。

状態
:built データはメモリ上に構築されているがデータベースに保存されていない状態
:loaded データはデータベースに保存されている状態
:deleted データはデータベースから削除されている状態

DOM にスキーマの状態を持たせる

これを利用して。 例えば data-state という属性を追加して、DOM にスキーマの状態を追加します。 また phx-hook でフックを設定します。

<div id="bars" phx-update="append">
  <%= for bar <- @bars do %>
    <div id="bar-<%= bar.id %>" data-state="<%= Ecto.get_meta(bar, :state) %>" phx-hook="Bar">
      <span><%= bar.id %></span>: <span><%= bar.name %></span>
    </div>
  <% end %>
</div>

JavaScript のフックで削除された状態の DOM を削除する

フックでは、DOM が更新された後に呼び出される関数 updated の中で data-state の内容を調べて、"deleted" になっていれば、状態が更新された要素を削除します。

let Hooks = {}

Hooks.Bar = {
  updated() {
    if (this.el.dataset.state == "deleted") {
      this.el.remove()
    }
  }
}

let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, params: {_csrf_token: csrfToken}})

Hotwire のことを少し

先日 Hotwire が公表されました。 サーバで DOM を構築して送り出すことでページを更新するという、LiveView とも似ている部分があります。

さっそくさわってみました。

気がついたことはいくつかあるのですが、最初に目がいったのは部分的な DOM の削除の箇所。 予想どおりですが、削除のアクションが用意されていて、メソッド呼び出しひとつで指定した DOM を削除できるようになっていました。

www.rubydoc.info

実は LiveView での部分的な DOM の削除は、以前から気になっていた箇所です。 気になりつつも先送りしていたのですが、今回 Hotwire でそれができると知り、がぜん解決したくなったというのが今回の記事を書く動機になりました。

仕事では Ruby on Rails を主戦場にしていることもあり、ゆくゆくは Hotwire を使う機会があるかもしれません。 JavaScript 嫌いマン、JavaScript 書きたくないマン であるわたしにとっては LiveView や Hotwire のようなフレームワークが普及することを願って止みません。

Hotwire については。もう少し使ってみてから記事にしようと思います。

いつか読むはずっと読まない:YOUはSHOCK

日々仕事でデータベースを扱っていて。 SQL以外にもデータをうまくあつかえる手段を手に入れたい、という思いが高まり。 一度は R に手を出したものの挫折。

構文が肌に合わなかったのかもしれないと、Julia に手を出し。 現在挫折中。

構文云々ではなく、まずなにより「データをあつかう」という領域のメンタルモデルが必要なのかもしれない、と感じる今日このごろ。

1から始める Juliaプログラミング

1から始める Juliaプログラミング

そんなわけで。 みなさん、よいお年を。