852 Views
March 10, 25
スライド概要
- 関数型プログラミングとは
- 関数型言語 Elixir
- 関数型言語の典型的な処理
- 並行処理
機械学習・音声認識・プログラミングに関する書籍を執筆しています。
プログラミング〈新〉作法 ~これからプログラムを書く人のために ~ 5. Elixir: 関数型 関数型プログラミングとは 関数型言語 Elixir 関数型言語の典型的な処理 並行処理 1
5.1 関数型プログラミングとは 手続き型プログラミング 大きな問題を小さな問題に分割する段階的詳細化が基本的なアイディア 処理の中身は構造化プログラミングで記述 処理は,関数外部のデータを書き換えることで進んでゆく 関数型プログラミング 大きな問題を関数の合成で実現するのが基本的なアイディア 副作用を伴わず,入力が決まれば常に同じ出力を返す純粋関数が基本 関数の入出力となるデータは原則としてイミュータブル(不変) 2
5.1 関数型プログラミングとは 関数型プログラミングの考え方 ⼿続き型 段階的詳細化 関数型 問題 関数 副作⽤の排除 パイプライン 関数1 関数 関数 関数 構造化プログラミング 順次実⾏ 条件分岐 関数 関数2 データ 繰り返し 関数3 データ 部品としての関数 基本操作 強⼒な パターンマッチ Map Filter Reduce データ構造 3
5.1 関数型プログラミングとは 関数型プログラミングのポイント 関数は第一級オブジェクトなので,関数を変数に代入したり,関数を関数の 引数や返却値にできる 関数に関数を渡す基本的なパターンとして,Map, Filter, Reduce などが用意 されている 関数の合成は,基本的にはパイプラインで記述される if 文や for 文などを含めて,基本的にすべての処理が値を持つ 4
5.2 関数型言語 Elixir は Erlang VM 上で動作する関数型言語 Erlang VM は並行処理をサポートするための仮想マシン 通信を伴う分散処理に強い 稼働中のシステムを停止せずにコードを更新できるホットリロード機能な ど,高い障害耐性を持つ Elixir の構文の特徴 オブジェクト指向スクリプト言語 Ruby に似た,可読性が高い構文 関数型言語としての基本的機能が簡潔に記述できる Elixir 5
5.2
関数型言語 Elixir
Elixir
の基本 : 入力・演算・出力( code2-1.c の例を Elixir で書き換え)
defmodule Calculator do
def main do
price = 150
IO.write "How many do you need?: " #
amount = IO.gets("") |> String.trim |> String.to_integer
total = price * amount
IO.puts "Total : #{total} yen"
#
end
end
改行しない出力
出力後に改行
Calculator.main
How many do you need?: 10
Total : 1500 yen
6
5.2 関数型言語 Elixir パイプライン パイプライン演算子 |> を使って,関数の出力を次の関数の入力に渡すこと ができる パイプラインの先頭要素(データソース)には変数名を書くこともできる 関数1 データ 関数2 関数3 データ 7
5.2 関数型言語 Elixir データ構造 (1/2) アトム :name のようにコロンで始まる名前を持ち,この表記そのものが定数 リスト 要素の並びを表現するデータ構造.パターンマッチを用いて,容易に分 割ができる list = [1, 2, 3, 4, 5] new_list = [0 | list] # 先頭に要素を追加した新たなリストを作成 8
5.2
関数型言語 Elixir
データ構造 (2/2)
タプル
固定長の要素の並びを表現するデータ構造
tuple = {15, "middle"}
{num, _} = tuple #
IO.puts num
# 15
パターンマッチによる要素の取り出し
マップ
キーと値のペアを表現するデータ構造.キーはイミュータブル
map = %{:name => "Alice", :age => 30}
IO.puts map[:name] # Alice
9
5.2 関数型言語 Elixir 制御構造と関数定義 条件分岐は通常の制御式を用いた分岐以外に,パターンマッチを使った case 式を用いることができる ループは末尾再帰関数を使って実現することが多い 関数は, def でグローバル関数, defp でプライベート関数として定義する 10
5.2 関数型言語 Elixir による平方根計算の例 Lisiting 5.3 定数定義と main/0 関数の定義 Elixir defmodule SquareRoot do @eps 0.0001 def main do read_positive_float() |> sqrt() |> IO.inspect(label: "Result") end 11
5.2
関数型言語 Elixir
数値を読み込むプライベート関数の定義
defp read_positive_float do
IO.write "Enter a positive number: "
input = IO.gets("") |> String.trim()
case Float.parse(input) do
{number, ""} when number > 0.0 ->
number
_ ->
IO.puts "Input error!"
read_positive_float() #
end
end
末尾再帰によるループ
12
5.2 関数型言語 Elixir 平方根を求めるプライベート関数の定義と実行部分 defp sqrt(number), do: sqrt_iter(number, number) defp sqrt_iter(number, guess) do new_guess = (guess + number / guess) / 2 if close_enough?(guess, new_guess) do new_guess else sqrt_iter(number, new_guess) end end defp close_enough?(a, b), do: abs(a - b) < @eps end SquareRoot.main 13
5.3 関数型言語の典型的な処理 高階関数による処理 関数を引数に取る関数を高階関数と呼ぶ 一見,処理が複雑になりそうだが,処理対象のデータがイミュータブルなリ ストで表現されていることを前提とすると,以下のように一般化できる Map: リストの各要素に対して関数を適用し,その結果からなる新たなリ ストを作成する Filter: リストの各要素に対して真偽値を返却値とした関数を適用し,真 を返した要素からなる新たなリストを作成する Reduce: リストの各要素に対して順次関数を適用し,その返却値を1つの 値にまとめる 14
5.3 関数型言語の典型的な処理 Map, Filter, Reduce の処理イメージ Map Filter Reduce #, # ## 15
5.3 関数型言語の典型的な処理 Map の例 : 文字列の長さからなるリストを作成 第2引数に既存の名前つき関数を指定するときは,関数名の前にキャプチャ演 算子 & を付け,関数名の後にアリティを明示した形で指定する defmodule Map1 do def main do name_list = ["Alice", "Bob", "Caroline", "David", "Eve"] len_list = Enum.map(name_list, &String.length/1) IO.inspect(len_list) end end Map1.main [5, 3, 8, 5, 3] 16
5.3 関数型言語の典型的な処理 Filter の例 : 文字列に "a" を含む要素からなるリストを作成 defmodule Filter1 do def main do name_list = ["Alice", "Bob", "Caroline", "David", "Eve"] filtered_list = Enum.filter(name_list, &has_a?/1) IO.inspect(filtered_list) end defp has_a?(s), do: String.contains?(s, "a") end Filter1.main ["Caroline", "David"] 17
5.3 関数型言語の典型的な処理 高階関数に渡す関数の書き方 無名関数 fn と end で囲んだブロックで表現 Enum.filter(name_list, fn s -> String.contains?(s, "a") end) クロージャ 無名関数の外側の変数を参照することができる char = "e" ... filtered_list = Enum.filter(name_list, fn s -> String.contains?(s, char) end) 18
5.3 関数型言語の典型的な処理 Reduce の例 : リストの各要素の平方和を求める defmodule Reduce1 do def main do num_list = [0, 1, 2, 3, 4, 5] result = Enum.reduce(num_list, 0, &sum_of_squares/2) IO.puts result end defp sum_of_squares(x, y), do: y + x * x end Reduce1.main 55 19
5.4 並行処理 並行処理とは 複数のタスクを「同時に進行しているように見せる」手法 OSはプロセスを切り替えて並行処理し,リソースを有効活用している 並列処理は実際にマルチコアCPUや複数のコンピュータで同時実行する手法 並列処理はタスクを分割して実行し,処理速度の向上を目的とする Elixirでの並行処理 軽量プロセスとメッセージパッシングで並行処理を実現し,競合を防いでい る 20
5.4 並行処理 単純なプロセス生成 並行処理を行う単位 プロセス : メモリを分離することで安全性が高い スレッド : プロセス内でメモリを共有して軽量に動作する Elixir での実現 Erlang VM が管理することで,OSが提供するプロセスよりもはるか に消費リソースが少ない軽量プロセスを使う 21
5.4 並行処理 プロセスの生成とメッセージの送受信 main 関数 spawn(fn) プロセス ID send(pid, メッセージ) pid: 軽量プロセス fn の実⾏ recieve マッチしたメッセ ージで定義された 処理を実⾏ 22
5.4
並行処理
プロセス生成とメッセージ送受信を行うプログラムの例 Listing 5.10
defmodule Process1 do
def main do
#
receiver_pid = spawn(fn ->
receive do
#
{:ok, message} -> IO.puts("Received message: #{message}")
end
end)
send(receiver_pid, {:ok, "Hello from sender"}) #
end
end
Process1.main
プロセスの起動
メッセージを受信したときの処理
メッセージを送信
Received message: Hello from sender
23
5.4
並行処理
複数のプロセス生成とメッセージ送受信の例 Listing 5.11
メッセージを受信すると,ランダムな時間待機して,送信元にランダムに選
択したメッセージを返すプロセス wait_and_respond() を,複数並行して実
行
複数のプロセスを生成して,リストに格納
#
pids = for _ <- 1..5 do
spawn(fn -> wait_and_respond() end)
end
生成したプロセスにメッセージを送信
#
Enum.each(pids, fn pid ->
send(pid, {:msg, self(), "S: Hello. "})
end)
24
5.4 並行処理 プロセスの監視 並行処理の難点は,プロセスが異常終了した際に適切な処理が必要なこと Elixir では,異常終了を検知し対処できるプロセス監視機能を提供している spawn_link/1 を使うと,リンクされたプロセスが異常終了した場合 に,呼び出したプロセスも強制終了する spawn_monitor/1 を使うと,呼び出されたプロセスが異常終了した場合 は,呼び出したプロセスは :DOWN メッセージを受信できる 監視機能を活用し,プロセスの異常終了を適切に処理できる設計が重要 25
5.4 並行処理 エージェント 並行処理において状態を管理するための簡単で効果的な仕組み プロセス間で共有されるメモリ上の状態を表現し,その状態に対する読み取 りや書き込みを行うことができる 読み取り処理や書き込み処理はアトミックに行われるため,データ競合が発 生することがない エージェントは, Agent モジュールを使って生成し, Agent.get/3 と Agent.update/3 で状態の読み取りと書き込みを行う 26
5.4 並行処理 非同期処理とは タスクの完了を待たずに次のタスクを開始する手法 I/O操作などの待機時間を効率化し,システムの応答性を向上させる 非同期処理のコーディング 関数を非同期関数として実行する async と,非同期関数の結果が利用可能に なるまで関数の実行を待機する await を組み合わせて非同期処理を実現 Task モジュールは,非同期処理を簡潔に実装できる機能を提供 Task.async/1 は非同期関数を実行し,そのプロセス ID を返却する Task.await/2 は引数にわたしたプロセス ID のプロセスが終了するまで 待機し,結果を返却する 27
5.4
並行処理
非同期処理の例 : 複数のweb API(ダミー)からデータを取得して集約 Listing
5.14
defmodule AsyncDataFetch do
def fetch_user_info(uid) do
#
Process.sleep(100) # 0.1
[%{id: uid, name: "John Doe"}]
end
ユーザ情報を取得する模擬関数
秒スリープ
ユーザの投稿を取得する模擬関数
秒スリープ
def fetch_posts(uid) do
#
Process.sleep(300) # 0.3
[%{id: uid, title: "Elixir is fun!"}]
end
ユーザのコメントを取得する模擬関数
秒スリープ
def fetch_comments(uid) do
#
Process.sleep(150) # 0.15
[%{id: uid, content: "Great post!"}]
end
28
5.4 並行処理 すべてのデータを非同期に取得し,結果を集約する関数 # def fetch_all_data(user_id) do tasks = [ Task.async(fn -> fetch_user_info(user_id) end), Task.async(fn -> fetch_posts(user_id) end), Task.async(fn -> fetch_comments(user_id) end) ] すべてのタスクの結果を待つ # results = Enum.map(tasks, &Task.await(&1, 5000)) IO.inspect(results) end end AsyncDataFetch.fetch_all_data(1) 29
5.4
並行処理
実効結果
[
[%{id: 1, name: "John Doe"}],
[%{id: 1, title: "Elixir is fun!"}],
[%{content: "Great post!", id: 1}]
]
30
5.5 まとめ は関数型言語で,関数を第一級オブジェクトとして扱える データはイミュータブルが基本で,変更せず新しいデータを生成する設計 関数の合成によりプログラミングを行う.高階関数の適用には Map , Filter , Reduce などが活用できる コードは副作用のない純粋関数が中心となるので,データの流れが明確で可読性 が高い.また,テストが容易になる 並行処理の実装を支援する機能が充実している Elixir 31