この記事は、「fukuoka.ex Elixir/Phoenix Advent Calendar 2020」の24日目になります。
東京だけど fukuoka.ex の YOSUKENAKAO.me です。
昨日は、「自作3Dライブラリ次回作の新しい武器を探してたらElixirに出会いました」
でした。 @emadurandal
普段は合同会社The Waggleで「教育」に関わるサービス作りのお仕事と学習教材の開発や
研修講座の企画開発をしています。
という事で、高校生に教えていたElixirの教材の一部を5時間ぶんくらい、クリスマスに出しておきます。
Mix Project の作成
Elixirを実行する
mix コマンドで servy プロジェクトを生成する
mix new servy
cd servy
code .
※mac userは「code .」 を利用する為にはVisual Studio Code からターミナルで呼び出すためのShellをインストールする必要があります。
方法は、こちらの記事参照 Link
Elixirファイルを実行するためにビデオで使用したコマンドの簡単な要約を次に示します。
- Elixirファイルの相対パスでelixirコマンドを 実行します。
$ elixir lib / servy.ex
- iex(インタラクティブ エリクサー の略)セッションを使用して、Cのヘルパー関数を利用してファイルをコンパイルして実行します。
$ iex
iex > c "lib/servy.ex"
Cのヘルパー機能(モジュール、メモリ内の指定されたファイルをコンパイルServyこの場合)セッションにロードされ、モジュールの任意のコードの外で解釈されます。
iexセッション を終了するには、Ctrl + Cを 2回押します。
- 別の方法として、ファイルの相対パスを渡すことで、開始時にElixirファイルを解釈するようにiexに指示することもできます。
$ iex lib/servy.ex
- 標準iexセッションを開始すると、mixプロジェクトのパスと依存関係を認識しません。したがって、プロジェクトのコンテキストでセッションを開始するには、-S mixオプションを渡す必要があります。
$ iex -S mix
- 最後に、iexでモジュールを再コンパイルするには、rヘルパー関数を使用し ます。
iex> r servy
これにより、指定されたモジュール( この場合はServy)が再コンパイルおよび再ロードされます。
演習
IExは次のように複数行にまたがって実行を待つ事が出来ます。
iex(1)> "文字列
...(1)>を複数行にまたがる"
"文字列\nを複数行にまたがる"
これは、引用符の途中で、次の引用符を見つけるまで、入力を受け付ける待機状態となります。
上記の例では、シェルは閉じ引用符を見つけるまで、より多くの入力を期待します。シェルがどの文字を期待しているのかが明らかでない場合があり、ユーザーはシェルを終了する以外に終了することができない不完全な表現の状態に閉じ込められていることがあります。
そこで、間違えて待機状態にしてしまった際に、特別なブレークトリガーとなるコマンドがあります。
そのコマンドを下記のリファレンスより探して、下記の問題を解決しなさい。
https://hexdocs.pm/iex/IEx.html
iex(1)> [ "文字列
...(1)> 調べたブレークコマンドを入力
** (TokenMissingError) iex:1: incomplete expression
高レベル変換
HTTPリクエストとレスポンス
request = """
GET / wildthings HTTP / 1.1
Host:example.com
User-Agent:ExampleBrowser / 1.0
Accept:* / *
「」
"""
予想されるレスポンスは以下です。
expected_response = """
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 20
Bears, Lions, Tigers
"""
アウトラインとしてのパイプライン
まず最初に、次の関数を書きます。
def handle(request) do
connectionValue = parse(request)
connectionValue = route( connectionValue)
formatResponse(connectionValue)
end
この関数は、単純にデータを渡して引き継いでるだけの
状態です。ここではまず、Elixirのパイプと呼ばれる書き方
の意味を理解してもらうための準備運動です。
handle
関数の引数 request
にデータを渡したら
connectionValue
という変数と parse
関数の引数に request
を渡します。 その後、connectionValue
に束縛された値を 次の route
関数の引数に渡して、返ってきた値を再度
connectionValue
へ束縛しています。そして最後に formatResponse
関数の引数に connectionValue
を渡して返ってきた値が handle
関数の戻り値となります。
Elixirでは、関数の最後の行に書かれている変数や関数呼び出しの戻り値が、関数の戻り値として扱われます。
上で書いた関数を1行で書く事もできます。1行で書くと第一引数を省略して書く事ができます。
formatResponse(route(parse(request)))
この書き方では、引き渡す為の変数を準備する必要が無いので、connectionValue
を省略する事ができます。
プログラミングコードの全体像を確認して見ましょう。
def handle(request) do
formatResponse(route(parse(request)))
end
このような状態です。
※ 今後も、プログラミングコードについては、説明部分にフォーカスする為に、一部のコード> について書いて説明しますが、必ずどの部分にフォーカスしているかを意識しながら読み進めてください。
途中で、どうしても迷ってしまったら、githubのgistにコードの全体像をUPしておきますので、そちらを確認して現在地を探して理解に勤めてください。
さて、主題に戻りますが、1行で書かれた関数の処理は、視認性が著しく悪く読みづらいです。
そこで、Elixirでは「|>」パイプ演算子というものがあります。これを使う事で、
関数を簡素にネストして読みやすいコードに書き換える事ができます。
このパイプでは最初に渡した引数を元に変換されたデータを次の関数に渡しています。
request
|> parse
|> route
|> formatResponse
この書き方で書かれたコードはコンパイル時に、ネストされた呼び出しの形に変換されます。
では、全体像を確認して見ましょう。
def handle(request) do
request
|> parse
|> route
|> formatResponse
end
Unix OSにおけるパイプラインのアナロジー
Unix 系のOSで、複数のコマンドを一緒にパイプしたことがある場合は、
Elixirのパイプ演算子は理解しやすいと思います。
Unixの経験が無ければ、例えるなら、伝言ゲームを思い出して下さい。
伝言する言葉が人を介する度に、少しづつ変換されて最後に答える人は違った意味になっている
ゲームです。
次は、リクエストを解析する部分に入ります。
全体的なコードの内容
defmodule Servy.Handler do
def handle(request) do
request
|> parse
|> route
|> formatResponse
end
def parse(request) do
# TODO: Parse the request string into a map:
connectionvalue = %{ method: "GET", path: "/wildthings", resp_body: "" }
end
def route(connectionvalue) do
# TODO: Create a new map that also has the response body:
connectionvalue = %{ method: "GET", path: "/wildthings", resp_body: "Bears, Lions, Tigers" }
end
def formatResponse(connectionvalue) do
# TODO: Use values in the map to create an HTTP response string:
"""
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 20
Bears, Lions, Tigers
"""
end
end
request = """
GET /wildthings HTTP/1.1
Host: example.com
User-Agent: ExampleBrowser/1.0
Accept: */*
"""
response = Servy.Handler.handle(request)
IO.puts response
Kernel
ここでは、アトム、パターンマッチについて学んで行く前に
Elixirの標準ライブラリや、組み込み型データ型について確認しておきます。
ElixirのドキュメントKernelの「Built-in types」と「Data types」を確認しましょう。
https://hexdocs.pm/elixir/Kernel.html
Built-in types
次のモジュールは、Elixirの組み込みデータ型を処理します。
Atom-文字名と定数(true、false及びnil原子です)
Float -浮動小数点精度の数値
Function- fn/1特別な形式 で作成されたコードチャンクへの参照
Integer -整数(分数ではない)
List -可変数の要素のコレクション(リンクリスト)
Map -キーと値のペアのコレクション
Process -実行の軽量スレッド
Port -外界と相互作用するメカニズム
Tuple -固定数の要素のコレクション
付属モジュールなしの2つのデータタイプがあります。
ビット文字列-で作成されたビットのシーケンスKernel.SpecialForms.<<>>/1
ビット数が8で割り切れる場合、それらはバイナリと呼ばれ、Erlangの:binaryモジュールで操作できます
参照-で作成されたランタイムシステムの一意の値 make_ref/0
Data types
Elixirは、上記のタイプの上に構築される他のデータタイプも提供します。それらのいくつかは次のとおりです。
Date- year-month-day指定されたカレンダーの構造体
DateTime -指定されたカレンダーのタイムゾーン付きの日付と時刻
Exception -エラーおよび予期しないシナリオから発生したデータ
MapSet -一意の要素の順不同のコレクション
NaiveDateTime -指定されたカレンダーのタイムゾーンなしの日付と時刻
Keyword -2つの要素のタプルのリスト。多くの場合、オプションの値を表します
Range -2つの整数間の包括的範囲
Regex -正規表現
String -文字を表すUTF-8エンコードバイナリ
Time- hour:minute:second指定されたカレンダーの構造体
URI -リソースを識別するURIの表現
Atom を key にした map
ElixirのAtom型には、名前の前に「:」コロンがつきます。
%{ :method => "GET", :path => "/wildthings" }
Elixirでは、上記のような 「 %{ key => value } 」形のデータをmap型として扱えます。
そして、keyの部分の値を Atom型を利用した際には、以下のような簡略化した書き方ができます。
%{ method: "GET", path: "/wildthings" }
このように、keyをAtom型にした場合に、=> を省略する事ができます。その時のルールとして、Atom型の特徴である名前の前のコロンを名前の後ろに変更する必要があります。
※ このように意味は同じでも簡略化した書き方ができるものを総称として「シンタックスシュガー」と呼ぶ
ちなみに、Keyの部分をアトム型ではなく、通常の例えば、文字列などを利用した場合は、以下のように=>を省略はできないので注意する
%{ "method" => "GET", "path" => "/wildthings" }
Atomの使い方
Atom型は、メモリの場所を固定してアクセスを早める事ができる。その代わりに、メモリを占有するので、自動的にアトム型を生成するようなプログラミングコードは書くべきではない。
予め固定で利用を限定する
Stringモジュール
requestの内容はドキュメントです。
そこでStringを扱う関数を調べる事にします。
"""
GET /wildthings HTTP/1.1
Host: example.com
User-Agent: ExampleBrowser/1.0
Accept: */*
"""
このように文字列なので、文字列を扱う関数を表示する
String関数のHelpを確認する
$ iex -S mix
iex(1)> h(String)
There are many other helpers available, here are some examples:
• b/1 - prints callbacks info and docs for a given module
• c/1 - compiles a file
• c/2 - compiles a file and writes bytecode to the given path
• cd/1 - changes the current directory
と、ヘルプ内容が長々と表示されます。
今回は String.split/2 関数を使いたいと思います。
Many functions in this module work with patterns. For example, String.split/2
can split a string into multiple strings given a pattern. This pattern can be a
string, a list of strings or a compiled pattern:
iex> String.split("foo bar", " ")
["foo", "bar"]
iex> String.split("foo bar!", [" ", "!"])
["foo", "bar", ""]
iex> pattern = :binary.compile_pattern([" ", "!"])
iex> String.split("foo bar!", pattern)
["foo", "bar", ""]
iexを起動して、次の処理をiexで実行してみましょう。
String.split/2
準備
iex(1)> request = """
...(1)> GET /wildthings HTTP/1.1
...(1)> HOST: examle.com
...(1)> User-Agent: ExampleBrowser/1.0
...(1)> Accept: */*
...(1)> """
requestに改行コード付きの文字列が入りました。
String.split/2 関数を利用して、改行コードで分割する事にします。
iex(2)> String.split(request,"\n")
["GET /wildthings HTTP/1.1", "HOST: examle.com",
"User-Agent: ExampleBrowser/1.0", "Accept: */*", ""]
[ "value1", "value2", "value3"]
値が複数入ったリスト形式の型として出力されました。ここから、parse関数でMap型で利用したいのは key となる :method の値 "GET" とkeyとなる :path の値となる "/wildthings" の文字列です。
"GET" と "/wildthings" の値が入っている箇所は、
["GET /wildthings HTTP/1.1", "HOST: examle.com",
"User-Agent: ExampleBrowser/1.0", "Accept: */*", ""]
の最初の値になります。そこで、List型についてのモジュールを
利用して、値を取得したいと思います。
ミニ演習
List型のデータを扱うモジュールにどのような関数が用意されているかHelpを利用して調べてください。
続きは、また随時出して行こうと思います。