2021/12/08の回です。
昨日は Koyoさん の Elixir + SendGrid でメール送信してみる でした。
はじめに
私はfukuoka.exを通じて、Elixirに関わってちょうど1年になります。
普段はO社を憎みながらJavaでお仕事をするエンジニアなんですが、知人の紹介でElixirを触り始めました。
当然JavaとElixirのパラダイムは大きく違うので、最初は戸惑うことがありました。
今回は振り返りとして、それぞれのプログラミングスタイルの違いを書いていこうと思います。
ループ処理の取り扱い
Elixirではfor文などを使う必要がありません。
「何を言っているんだ?」と思いますが、階乗を計算するソースコードを見てみましょう。
まずはJavaのコードから。はい、普通にfor文でループしていますね。
static int factorial(int num) {
int result = 1;
for (int i = num; i > 0; i--){
result = result*i;
}
return result;
}
次にElixirのコードを見てみます。一見すると、ループしているようには見えません。
defmodule Factorial do
def of(0), do: 1 # 初項
def of(n), do: n * of(n - 1) # n>0の項
end
階乗の処理は、数学的には以下で定義できます。
factorial.exの関数はそれぞれ初項、n>0の項に対応します。
a_0 = 1 \\
a_n = n \times a_{n-1} \quad (n>0)
プログラミングコンテストをやっていると「これって再帰処理じゃね?」と気づくかもしれません。
はい、その通り。Elixirでは「再帰処理とパターンマッチでループ処理を表現」します。
Elixirでは、例えば Factorial.of(3)
は以下の流れで実行されます。
この of(0)NG, of(n)OK
または of(0)OK, of(n)NG
というのがポイントです。
関数が実行される際、その引数のパターンに合致するものを探して実行する仕組みをパターンマッチと言います。
分岐処理の取り扱い
Elixirではif文などを使う必要がありません。
またまた「何いってだこいつ?」と思うかもしれませんが、これもソースコードで確認していきましょう。
まずはJavaのコードから。はい、普通にif文で分岐していますね。
※わざと酷い書き方にしています。
static boolean strJudge(String str) {
if (str.isEmpty()) {
return false;
}
if ("hogehoge".equals(str)) {
return false;
} else if ("foo".equals(str)) {
return false;
} else {
return true;
}
}
次にElixirのコードを見ていきます。以下のようにガード節とパターンマッチを使っています。
defmodule Guard do
def what_is(x) when not is_binary(x) do false end # 文字列か否かの判定
def what_is(x) when x == "" do false end
def what_is(x) when x == "hogehoge" do false end
def what_is(x) when x == "foo" do false end
def what_is(x) do true end # xを使用していないのでwarning
end
このように、Elixirではif文を使うことなくガード節で記述することが可能です。
Javaのコードでも、リファクタリングする際にガード節にすると可読性が向上することがあります。
そのため、最初からガード節での記述を求めているElixirはエレガントな言語だと言えます。
応用:複雑な分岐処理について
例えば以下のように、Javaでnestedなif文があるとします。
読みずらいですね。
static Result getResult(int a, int b, int c) {
int csd = 0;
int bd = 0;
int p = 0;
int count = 0;
if (a == 1) {
csd = 5;
bd = 5;
p = 6;
if (b == 1 && c != 1) {
count = 1;
}else {
count = 20;
}
} else {
csd = 10;
bd = 10;
p = 10;
count = 1;
}
return new Result(csd, bd, p, count); // Result(int a, int b, int c, int d)
}
上記のJavaコードは、Elixirだとこのようにシンプルに書き直すことが出来ます。
nestedなif文は、複数の変数に対して判定する/しないといった場合分けが存在しますが、Elixirではパターンマッチで判定しない変数を_
で値を無視することが出来ます。
非常に強力なパターンマッチによって、このようにシンプルな記述が実現できています。
defmodule CaseSwitch do
def judge(a, b, c) do
{csd, bd, p, count} = case {a, b, c} do
{1, 1, 1} -> {sd, sd, sdi, 20}
{1, 1, _} -> {sd, sd, sdi, 1}
{1, _, _} -> {sd, sd, sdi, 20}
{_, _, _} -> {dd, dd, dd, 1}
end
end
end
パターンマッチとパイプライン演算子
Elixirはデータドリブンな言語です。
もう何を言っているかわからねーと(ry ですが、これもソースコードを見ていきましょう。
例えばJava8以前では、このような書き方をしていました。
Listをfor文でループして、必要な情報はif文の条件でfilterして…という具合に。
※もちろんJava8以降はStreamとラムダ式によってこのようなクソコードは書かなくてもよくなりましたが。
static List<User> getUserList() {
List<User> userList = Arrays.asList(
new User("foo", 20, "Tokyo"), // User(String name, int age, String address)
new User("bar", 15, "Fukuoka"),
new User("hoge", 18, "Tokushima")
);
final List<User> filteredList = new ArrayList<>();
for (User user : userList) {
if (user.getAddress().startsWith("T")) {
filteredList.add(user);
}
}
return filteredList;
}
これをElixirで書くと、以下のようになります。
Elixirに馴染みが無いと分かりずらいかもしれませんが、最後のEnum
の部分がリストにfilterをしている部分です。
defmodule UserList do
def get_user_list() do
user_list = [
%{ name: "foo", age: 20, address: "Tokyo" },
%{ name: "bar", age: 15, address: "Fukuoka" },
%{ name: "hoge", age: 18, address: "Tokushima" }
]
Enum.filter(user_list, (fn %{name: _name, age: _age, address: address} ->
String.first(address) == "T" end))
end
end
user_list
は生成されたタイミングで
[%{.., .., ..}, %{.., .., ..}, %{.., .., ..}]
という形式になっています。また、Enum.filter
でuser_list
を解析しているときは、一番外側の[]
が外れて%{.., .., ..}
という形式で対応付けています。
Elixirでは、「与えられたデータの形式」と「処理するデータの形式」が対応することを保証しなければなりません。
もし対応しないような記述をしてしまうと、たちどころにコンパイルエラーとなります。
したがって、コンパイルのタイミングですでにパターンマッチは保証されているということになります。
また、更にuser_list.ex
のソースをこのように書き換えることが出来ます。
defmodule UserList do
def get_user_list() do
(...中略...)
Enum.filter(user_list, (fn %{name: _name, age: _age, address: address} ->
String.first(address) == "T" end))
|> Enum.filter(fn %{name: _name, age: age, address: _address} ->
age < 20 end)
end
end
追記したのは|> Enum.filter(fn %{name: _name, age: age, address: _address} -> age < 20 end)
の部分なのですが、これはその手前のfilterの処理結果を引き継いで(|>)、更にfilterの処理を実行しています。
パイプライン演算子|>
が「直前の処理結果を引き継ぐ」ということをしてくれます。
これはさながらLinuxのパイプと同じ役割です。Linuxコマンドに触れる機会が多い人にとっては、直感的でうれしい仕様です。
このように、
- 言語仕様でパターンマッチを義務付けている
- パイプライン演算子によって直前のデータ処理結果を引き継ぐ
ことから、Elixirはデータドリブンな言語だと言えます。
まとめ
いかがだったでしょうか。OOPとElixirでは、そのパラダイムの違いから、大きくプログラミングスタイルが異なることがわかりました。
Elixirでは、
- ループ処理は、再帰処理とパターンマッチで実現
- 分岐処理は、ガード節とパターンマッチで実現
- パターンマッチとパイプライン演算子によって、データドリブンな記述が実現
しています。さらにエレガントで簡潔、直感的な記述のため、非常に読みやすい。
まだまだElixirの魅力はありますが、もし興味を持たれた方は以下のコミュニティへJOINをお願いしますmm
では、2021/12/08の回は以上です。
ありがとうございました。