🧙‍♀️

LiveView: はじめの一歩 -- YouTube チャンネル namba.ex 第1回解説

2024/04/17に公開

動画

https://www.youtube.com/watch?v=asToexdgsjs

マクロ

動画の中で「アサインの名前にアットマークを付けるとアサインの値で置き換わる」という説明をしました。ここで、もう少し踏み込んだ解説を行います。

動画の内容と重複しますが、前提となる用語を2つ説明します。

まず、ソケット(socket)はPhoenix LiveViewの中核となる重要概念です。簡単に言うと、ブラウザがPhoenixアプリケーションとの間でLiveViewの仕組みを利用した接続を開始すると、1個のブラウザに対して1個のソケットを作ります。ソケットは「データを送受信する部品」ですが、データを格納する記憶領域も持っています。

ソケットの記憶領域に格納された個々のデータをアサイン(assign)と呼びます。LiveViewはHTMLテンプレートにこのアサインの値を埋め込むことによりHTMLドキュメントを生成し、ブラウザに差分を送ることで、ブラウザの画面を書き換えます。

動画の中では次の2つのソースコードを扱いました:

  • demo_live.ex
  • demo_live.html.heex

前者ではソケットの振る舞いが定義されています。その中では、socket.assigns.diameter のように書くと、アサイン diameter の値を参照できます。

後者はHTMLテンプレートです。その中では @diameter のように書くと、アサイン diameter の値を参照できます。

動画の中で小幡さんが私(黒田)にこのアットマーク記号の意味について質問し、私は「マクロです」と答えました。マクロとは、Elixirコンパイラがソースコードの一部を別のものに変換する仕組みです。Elixir コンパイラは、HTMLテンプレート中に @diameter という記述を見つけると、socket.assigns.diameter と同等のコードに置き換えてしまうのです。

より正確に言えば、<%= @diameter %><%= {:ok, v} = Access.fetch(assigns, :diameter); v %> に変換されます。これは <%= socket.assigns.diameter %> と同等です。詳しくはEExのドキュメントをご覧ください。

パターンマッチング

動画の中で次のような関数 handle_event/3 が紹介されました(/3 は引数の個数を示す)。

  def handle_event("change_state", _params, socket) do
    socket =
      socket
      |> assign(:diameter, socket.assigns.diameter + 50)
      |> assign(:bg_color, "")

    {:noreply, socket}
  end

聞き手の小幡さんが、この関数の第1引数に "change_state" という文字列が書いてあることについて質問し、私(黒田)が「これはパターンマッチングだ」と答えました。このことについて解説します。

Elixirでは、同一の名前と引数の個数を持つ関数を複数回並べて定義できます。例えば、次のような感じです:

def fib(0) do
  0
end

def fib(1) do
  1
end

def fib(n) do
  fib(n-1) + fib(n-2)
end

このとき、関数 fib/1 は3個の節(clauses)を持つと言います。引数が0のとき第1の節が呼ばれ、引数が1のとき第2の節が呼ばれ、引数がその他の値のとき第3の節が呼ばれます。

case を用いれば、次のように関数 fib/1 を単独の節で定義すること可能です:

def fib(n) do
  case n do
    0 -> 0
    1 -> 1
    n -> fib(n-1) + fib(n-2)
  end
end

しかし、経験を積んだ多くのElixirプログラマーは、複数の節を用いた関数定義を好みます。

関数 handle_event/3 に話を戻します。

この関数の役割は、ブラウザから送られてきたイベントを処理することです。動画の中で作ったアプリケーションの場合、イベントは "change_state" だけでしたが、複数の種類のイベントを扱うようにアプリケーションを拡張することができます。例えば、アプリケーションを初期状態に戻すためのイベント "reset_state" を導入し、ブラウザの画面の中にこのイベントを送信するためのボタンを設置することを考えましょう。

その場合、次のように関数 handle_event/3 の第2の節を定義することになります:

  def handle_event("reset_state", _params, socket) do
    socket =
      socket
      |> assign(:diameter, 100)
      |> assign(:bg_color, "")

    {:noreply, socket}
  end

タプル

多くのプログラミング言語には配列(array)という概念があり、複数の値を格納するためのデータ構造です。

プログラミング言語Elixirには、類似の概念としてリスト(list)とタプル(tuple)が存在します。配列という概念も存在しますが、ほとんど使われません。

リストが [1, 2, 3]["alice", "bob"] のようにコンマ区切りの値の列を大括弧(角括弧)で囲んで作るのに対し、タプルの方は {1, 2, 3}{"alice", "bob"} のように中括弧(波括弧)で囲みます。

どちらも、異なる型の要素を含むことが可能です。["alice", 1, true]{"alice", 1, true} は、リストとタプルの正しい例です。

リストとタプルにはそれぞれ得意不得意があります。

  • タプルはリストよりもメモリ効率が良い。
  • 要素数を調べたり、任意の位置にある要素を取り出したりする操作の効率でもタプルはリストに勝る。
  • 要素を加える操作はリストの方が効率よく行える。

動画の中では、関数 handle_event/3 の戻り値として {:noreply, socket} のようなタプルを使っています。Elixirの関数は複数の値を戻り値として返すことができないので、代わりに値の組をタプルとして返すのが定石です。

アトム

先述の関数 handle_event/3 の戻り値であるタプル {:noreply, socket} の第1要素である :noreply は、アトム(atom)です。

見かけ上、アトムは文字列と似ていますが、まったく異なる概念です。アトムは文字列よりもメモリ効率が非常に良いですが、文字を加える操作ができません。

アトムの用途は多岐にわたりますが、関数が戻り値に特別な意味を持たせるのに使われることがあります。

関数 handle_event/3 は、{:noreply, socket} または {:reply, map, socket} のいずれかのタプルを返すように実装しなければなりません。前者はイベントの送り主(ブラウザ)に情報を返さないことを、後者は返すことを意味します。

パイプ演算子

関数 handle_event/3 の中に次のような記述があります。

  socket =
    socket
    |> assign(:diameter, socket.assigns.diameter + 50)
    |> assign(:bg_color, "")

コード中に二度現れる |> という記号はパイプ演算子(pipe operator)と呼ばれます。

パイプ演算子の右辺には必ず関数呼び出しが置かれます。そして、左辺の値が右辺の関数呼び出しの第1引数となります。例えば、"alice" |> String.at(1) という式は String.at("alice", 1) という式と等価です。

パイプ演算子を使わないと先ほどのコードは次のように書くことになります。

  socket = assign(socket, :diameter, socket.assigns.diameter + 50)
  socket = assign(socket, :bg_color, "")

書き換え前のコードでは変数 socket が3回登場するのに対し、書き換え後のコードでは5回登場しています。その分、冗長です。

着目すべきは、書き換え後のコードの1行目で = の左辺にある変数 socket です。この変数は、2行目の式に値を渡すだけの役割しか果たしていません。この種の刹那的な変数は、パイプ演算子を用いると除去できます。

おそらくElixir初心者にとっては、書き換え後のコードの方が読みやすいでしょう。しかし、経験を積んだElixirプログラマーはパイプ演算子を好んで使いますので、早い段階でがんばって慣れてください。

Discussion