この記事は「AdventCalendar2021 プログラミング言語 elixir」の14日目です。
昨日は@Papillon6814さんの「Elixir始めたてのころからのコードの書き方変遷」でした。
導入
最近、「UNIXという考え方」という書籍を読みました。元々、気になっていた本で来年、新卒で入社してくるメンバーが「人生で感銘を受けた本です!」と熱く語っているのを見て、「そんなに良い本なんだ🤔」...と思い読み進めました。
各章で扱われている内容は素晴らしく、UNIX
ではどのように考るか、またソフトウェアを開発する際にどのようにUNIX
という考え方を用いれば良いのかが紹介されています。
ある日「UNIXという考え方」を読み進める中で、UNIX
の考え方に馴染みを感じました。
「どこかで学んだような...やったような...」という思考を巡らしていて、ようやく気づきました。
「あ、これelixirでプログラミングする時にやってるやつだ」
この記事の概要📖
- elixirではEnumモジュールという列挙可能型(enumerables)に対する便利なモジュールが提供されている
- パイプライン演算子を組み合わせることで、Enumの力を引き出すことが出来る
- Enum(関数)とパイプライン演算子の組み合わせを作ることでUNIXの4つの定理が形成される
- スモール・イズ・ビューティフル
- 1つのプログラミングには1つのことをやらせる
- 効率より移植性
- ソフトウェアの梃子を有効に活用する
- 結論: elixirを通じて「UNIXという考え方」を体感して身につけることが出来る
elixiらしい書き方🧪
※こちらはelixir
をご存知の方は、サクッと読み飛ばしてください🙇♂️
elixir
ではEnum
モジュールという列挙可能型(Enum
)を第1引数に受け取って、実行可能な関数群が提供されています。列挙可能型の一例として、イメージしやすいのは「リスト」でしょう。
map
、filter
, reduce
など...近年、多くの言語で実装されている関数が提供されています。
JavaScript
でのmap
const lst = [1,2,3,4,5]
lst.map(n => n + 10);
elixir
でのmap
lst = [1,2,3,4,5]
Enum.map(lst, fn n -> n + 10 end)
またelixir
ではパイプライン(|>)
という演算子が提供されています。この演算子はシェルスクリプトのパイプライン(|
)に非常に似ています(というかほとんど同じ)。
パイプライン演算子とEnum
を組み合わせることでパワフルな処理を作り出せます。
lst = [1,2,3,4,5]
Enum.map(lst, fn n -> n * 2 end) # [2,4,6,8,10]
|> Enum.filter(fn n -> n > 5 end) # [6,8,10]
|> Enum.sum() # 24
実践的な例
先ほどのEnum
とパイプライン演算子を用いて実践的なコードを書いてみます。
使用するのはスタジオジブリAPIという無料で公開されている認証なしのAPIです。
その名の通り、スタジオジブリの作品に関する情報がAPIで提供されています。今回はAPIを叩くことが目的ではないので、このスタジオジブリAPIのレスポンスをモックとして使用してコードを書いていきます。
# 実際に`curl`コマンドを叩くことでレスポンスを受け取ることが出来ます
curl https://ghibliapi.herokuapp.com/films
APIからのレスポンス(※長いので折り畳んで記載しました)
[
{
"id": "2baf70d1-42bb-4437-b551-e5fed5a87abe",
"title": "Castle in the Sky",
"original_title": "天空の城ラピュタ",
"original_title_romanised": "Tenkū no shiro Rapyuta",
"image": "https://image.tmdb.org/t/p/w600_and_h900_bestv2/npOnzAbLh6VOIu3naU5QaEcTepo.jpg",
"movie_banner": "https://image.tmdb.org/t/p/w533_and_h300_bestv2/3cyjYtLWCBE1uvWINHFsFnE8LUK.jpg",
"description": "The orphan Sheeta inherited a mysterious crystal that links her to the mythical sky-kingdom of Laputa. With the help of resourceful Pazu and a rollicking band of sky pirates, she makes her way to the ruins of the once-great civilization. Sheeta and Pazu must outwit the evil Muska, who plans to use Laputa's science to make himself ruler of the world.",
"director": "Hayao Miyazaki",
"producer": "Isao Takahata",
"release_date": "1986",
"running_time": "124",
"rt_score": "95",
"people": [
"https://ghibliapi.herokuapp.com/people/598f7048-74ff-41e0-92ef-87dc1ad980a9",
"https://ghibliapi.herokuapp.com/people/fe93adf2-2f3a-4ec4-9f68-5422f1b87c01",
"https://ghibliapi.herokuapp.com/people/3bc0b41e-3569-4d20-ae73-2da329bf0786",
"https://ghibliapi.herokuapp.com/people/40c005ce-3725-4f15-8409-3e1b1b14b583",
"https://ghibliapi.herokuapp.com/people/5c83c12a-62d5-4e92-8672-33ac76ae1fa0",
"https://ghibliapi.herokuapp.com/people/e08880d0-6938-44f3-b179-81947e7873fc",
"https://ghibliapi.herokuapp.com/people/2a1dad70-802a-459d-8cc2-4ebd8821248b"
],
"species": [
"https://ghibliapi.herokuapp.com/species/af3910a6-429f-4c74-9ad5-dfe1c4aa04f2"
],
"locations": [
"https://ghibliapi.herokuapp.com/locations/"
],
"vehicles": [
"https://ghibliapi.herokuapp.com/vehicles/4e09b023-f650-4747-9ab9-eacf14540cfb"
],
"url": "https://ghibliapi.herokuapp.com/films/2baf70d1-42bb-4437-b551-e5fed5a87abe"
}
]
このレスポンスを以下の条件で加工していきます。
- 監督は「宮崎駿(
Hayao Miyazaki
)か鈴木敏夫(Toshio Suzuki
)」である ->director
- 公開年が2000年以前である ->
release_date
- 評価が90点以上である ->
rt_score
- 上記を満たすデータを以下のテキストに加工する
----------------------------------------------------
監督: $director
公開年: $release_date
タイトル: $original_title($title)
あらすじ: $description
評価点(100点中): $rt_score
----------------------------------------------------
コードにすると以下のようになります。
to_text = fn film ->
"""
----------------------------------------------------
監督: #{film["director"]}
公開年: #{film["release_date"]}
タイトル: #{film["original_title"]}(#{film["title"]})
あらすじ: #{film["description"]}
評価点(100点中): #{film["rt_score"]}点
----------------------------------------------------
"""
end
# respにはAPIからのレスポンス(json)をパースした値が入っている想定
resp
|> Enum.filter(fn film -> film["producer"] in ["Hayao Miyazaki", "Toshio Suzuki"] end)
|> Enum.filter(fn film -> (film["release_date"] |> String.to_integer()) < 2000 end)
|> Enum.filter(fn film -> (film["rt_score"] |> String.to_integer()) >= 90 end)
|> Enum.map(fn film -> to_text.(film) end)
elixir
の文法など詳細な説明は省きますが、上から順にEnum
とパイプライン演算子を用いて順に条件に合うデータを抽出しています。
そして、最後に指定されている条件を満たしたデータをテキストに変換するという先ほどの仕様をただ順に組み立てただけです。
コードの内容はさておき、「Enum
とパイプラインを用いて、処理を組み合わせた」という点だけ頭に留めておいて下さい。
「UNIXという考え方」との照らし合わせ
「うん、elixir
の例は分かったよ、それで?🤔」
いよいよUNIX
の考え方がなぜelixir
を通じて身につくのかという話に移っていきます。
以降、書籍「UNIXという考え方」で定理として紹介されている内容を参照しています。
スモール・イズ・ビューティフル
小さいプログラムは分かりやすいし、保守しやすいという定理。
1つの大きなプログラムを作るよりも小さなプログラムを作って、組み合わせていくというのがUNIX
流といったところでしょうか。
先ほどのelixir
のコードを見てみて下さい。
|> Enum.filter(fn film -> film["producer"] in ["Hayao Miyazaki", "Toshio Suzuki"] end)
|> Enum.filter(fn film -> (film["release_date"] |> String.to_integer()) < 2000 end)
:
それぞれがEnum
によって作られた小さなプログラムです。1つ1つが何の処理をしているのかは明確です。
それらをパイプライン演算子を用いて組み合わせています。
これはまさに「スモール・イズ・ビューティフル」を満たしています。
1つのプログラミングには1つのことをやらせる
タイトルの通りですが、逆に言えば「1つのプログラムで複数のことをやらせるな」ということです。
|> Enum.filter(fn film -> film["producer"] in ["Hayao Miyazaki", "Toshio Suzuki"] end)
|> Enum.filter(fn film -> (film["release_date"] |> String.to_integer()) < 2000 end)
:
パイプライン演算子で組んだ上記のコードは、データを処理する条件1つ1つがそれぞれの処理に分割されており、1つの処理だけを請け負っています。
- 監督は「宮崎駿(
Hayao Miyazaki
)か鈴木敏夫(Toshio Suzuki
)」である ->director
- 公開年が2000年以前である ->
release_date
効率より移植性
コードは書いて終わりということはほとんどなく、日々、あらゆる仕様変更や機能追加が求められます。
ある日、「やっぱり2000年以前かどうかの判定はなくしてほしい」とか「プロデューサーが高畑勲の作品に絞ってほしい」という要望が来たとしたらどうでしょうか。
まず「やっぱり2000年以前かどうかの判定はなくしてほしい」という要望への対応は公開年の判定をしている箇所をコメントアウトするだけで完了します。
resp
|> Enum.filter(fn film -> film["producer"] in ["Hayao Miyazaki", "Toshio Suzuki"] end)
# |> Enum.filter(fn film -> (film["release_date"] |> String.to_integer()) < 2000
end)
:
|> Enum.map(fn film -> to_text.(film) end)
次に「プロデューサーが高畑勲の作品に絞ってほしい」という要望ですが、新たにパイプラインを追加するだけで完了します。
resp
|> Enum.filter(fn film -> film["producer"] in ["Hayao Miyazaki", "Toshio Suzuki"] end)
:
|> Enum.filter(fn film -> film["producer"] == "Isao Takahata" end) # 追加
|> Enum.map(fn film -> to_text.(film) end)
プログラムを小さく、1つだけのことをやらせることで結果的に、移植性も高くなります。
リストを何度もループしているので、1度のループの中で全ての条件を判定するコードよりかは効率は悪いでしょうが、移植性が高いのは明らかです。
ソフトウェアの梃子を有効に活用する
梃子(てこ)というのは梃子の原理で有名なあの梃子です。本書では「小さな努力で大きな結果を得る」という表現がされています。
ソフトウェアを大量に書く一番の方法コードを借りてくることです。実装をしようとしていることの多くはすでに、どこかの誰かによって実装されている場合が多いです。
車輪の再発明をするのではなく、コードを借りてくることで大きな結果を生み出すことが出来ます。
Enum
とパイプライン演算子というコードを拝借するで、簡潔なコードを書くことが出来ました。
仮にEnum
とパイプラインがないと大量のコードを書く必要があります。。。
defmodule MyEnum do
def reduce([], _, accum), do: accum
def reduce([h | t], func, accum) do
reduce(t, func, func.(h, accum))
end
def map([], _), do: []
def map(enum, func) do
wrapped = fn n, accum -> accum ++ [func.(n)] end
reduce(enum, wrapped, [])
end
def filter([], _), do: []
def filter(enum, func) do
wrapped = fn n, accum ->
if func.(n) do
accum ++ [n]
else
accum
end
end
reduce(enum, wrapped, [])
end
def sum([]), do: 0
def sum(enum) do
wrapped = fn n, accum -> accum + n end
reduce(enum, wrapped, 0)
end
end
lst = [1,2,3,4,5]
doubled_lst = MyEnum.map(lst, fn n -> n * 2 end)
under_5_lst = MyEnum.filter(doubled_lst, fn n -> n > 5 end)
MyEnum.sum(under_5_lst) # 24
総括
いかがだったでしょうか。
elixir
らしいコーディングを通してUNIX
という考え方を体感しながら身につけることが出来ます。
「小さく作ったものを組み合わせる」のがelixir
のパイプライン演算子の得意とするところです。
今回は説明を簡略化するためにEnum
モジュールだけを扱いましたが、elixir
には他にも様々なモジュールが用意されています。
またこのパイプライン演算子には独自実装した関数を組み込むことも出来るので、移植性もバッチリです。
ぜひ、elixir
を通して「UNIXという考え方」を体感してみてください。
明日は「AdventCalendar2021 プログラミング言語 elixir」の15日目は@Yoosukeさんの「LiveViewを使って簡単にステートフルなタイピングゲームアプリを作ろう!前編」です。
お楽しみに🙌