これは #Elixir Advent Calendar 2021 の17日目です。昨日は @piacerex さんの 「JupyterNotebook + NumPyでサクッと画像加工するノリ」をElixirでやってみた(lennaさんのバージョンアップもあるよ) でした。
はじめに
Elixir は関数型言語です。
これは、プログラミング言語を分類する代表的な(古典的な)方法で考えると「Elixir は関数型と呼ばれる分類に該当します」ということです。他には、手続き型、オブジェクト指向形、論理型、などがあります。
しかし、「Elixir は関数型言語です」という言い方をあえて避けるアルケミストもいます。それは「関数型」と言ってしまうと Elixir の良さが伝わりにくくなるからです。さらに、むしろ近づきがたい印象を与えてしまうかもという懸念もあります。関数型言語の説明で「数学で言う関数の概念に基づく」ってな説明もされますが、これから展開されるイメージの先に Elixir の快適さが来ないんです。実際、Elixir を愉快に使ってるときは、別の気持ちよさを感じてることが多いように思います。パイプ演算子、パターンマッチ、プロセスとメッセージモデル、とかですね。
ただ、こういう枠組みを気持ちよく使えるのも下に関数型言語パラダイムが控えてるからです。ですので「説明するときに関数型とは言いたくないけど言わねばならぬぅぅぅ」というジレンマがでます。
先日、携帯網の 5G の OSS 上で活動してるコミュニティ OMNI-JP (Open Mobile Network Infra Meetup) で びよんどプロジェクト という研究開発プロジェクトについて発表の機会がありました。ここで Elixir を聴衆にどう伝えるかというのに腐心しました。
そこで思いついた用語が「世俗派関数型言語」です。これは「関数型言語原理主義」に陥ってしまうとプログラミングの本質を見失いかねないという自戒もこめて作った用語です。気に入ったら使ってみてください。(これから私も使いますとは言ってない)
Elixir って本当に関数型言語なのか
さて、本題です。(前フリ、長いな〜)
関数型言語であることを意識せずに楽しくプログラムにいそしめる世俗派関数型言語の Elixir ですが、果たして関数型言語としての振る舞いをするのでしょうか。数学で言う関数との関連を調べてみましょう。(結局、そこかい)
関数の重要な性質に合成や結合律があります。
例えば $f, g$ と関数があったとして $g(f(x))$ と引数を順番に関数に適用して計算するのと、先に $g \circ f$ という関数を合成しておいて $(g \circ f)(x)$ と引数を適用するのとで結果が同じという性質です。
これは関数だけでなく数学全般によく出てくる基本的な概念で、より広く圏論でも扱います。圏論と言えば Haskell と思いますが純粋関数型言語を標榜する Haskell で出来ても面白くもなんともないですから、ここは Elixir で試してみましょう。
なお、このあたりは理屈の上では「その言語処理系において関数自体を第一級オブジェクトとして扱えるかどうか」という話に繋がります1。
準備
中身に入る前に多少準備が必要です。Elixir に慣れてる方はこの章を飛ばしてください。
無名関数
無名関数を使うのでちょっとおさらいします。
まず iex の中でさらっとやってみましょう。まず与えられた引数に 1 を足す関数 f/1
を作ることを考えます。
iex(1)> def (x), do: x + 1
** (ArgumentError) cannot invoke def/2 outside module
(elixir 1.12.3) lib/kernel.ex:5963: Kernel.assert_module_scope/3
(elixir 1.12.3) lib/kernel.ex:4699: Kernel.define/4
(elixir 1.12.3) expanding macro: Kernel.def/2
iex:11: (file)
iex 内だと defmodule
してからでないと def
出来ないですね。ということで無名関数で書くことになります。
iex(2)> fn(x) -> x+1 end
#Function<44.65746770/1 in :erl_eval.expr/5>
これで関数は出来ました。これの引数に 0 を与えてみます。
iex(3)> (fn(x) -> x+1 end).(0)
1
無名関数に名前をつける
では、無名関数に名前をつけましょう。
iex(19)> f = fn(x) -> x+1 end
#Function<44.65746770/1 in :erl_eval.expr/5>
これはこうも書けます。
iex(21)> f = & &1+1
#Function<44.65746770/1 in :erl_eval.expr/5>
使うときはこうですね。
iex(28)> f.(0)
1
2つの関数を適用してみる
では f/1
ともうひとつ、値を2倍する関数 g/1
の定義をしてみます。
iex(23)> f = & &1+1
#Function<44.65746770/1 in :erl_eval.expr/5>
iex(24)> g = & &1*2
#Function<44.65746770/1 in :erl_eval.expr/5>
じゃあ、これに引数を与えてみましょう。
iex(25)> (g.(f.(0)))
2
ようやくできました。これは $g(f(0))$ の計算です。
合成関数を作る
さて $g \circ f$ という合成関数を作ります。これ Elixir で関数を直接合成するような記述方法を見つけられませんでした。要は引数を与えずに複数の関数を組み合わせるような文法がなさそうに思えます2。なので「なんちゃって」をやります。
パイプを使う
関数を次々に適用する方法は引数の入れ子にする他に Elixir にはパイプ演算子があります。これでやってみることにします。
まずは、先程の(名前のない)無名関数への引数の適用はパイプで書くとこうなります。
iex(27)> 0 |> (fn(x) -> x+1 end).()
1
これは省略したカタチでこうも書けます。
iex(28)> 0 |> (& &1+1).()
1
でもかなり記号的ですね。名前をつけた関数にパイプで値をくわせるのは、先程の f
を使うとこうなります。
iex(29)> 0 |> f.()
1
これに関数 g
も適用してみます。
iex(30)> 0 |> f.() |> g.()
2
ようやくできました。これは g(f(0)) をパイプで書いた計算です。
パイプ演算子で合成関数風表現を作る
さてこれ、パイプ演算子が左結合なので左から演算されてます。なので先に f(0)
が計算されています。これ、さきに f
と g
とを組み合わせることは出来ないでしょうか。パイプ演算子が複数あってデフォルトが左優先ならが括弧をつけると演算子の優先順位が変えられるかもです。
と思って試しにこう書いてみたら行けたんです。
iex(37)> 0 |> (f.() |> g.())
2
よく見ると 0 を (f.() |> g.())
に与えているような記述になっています。こんどこそ合成関数風に書くことができました。
いろいろな値で試してみる
念のため、合成関数風なのとそうでないのとに値を突っ込んでみます。
iex(42)> 0 |> f.() |> g.()
2
iex(43)> 0 |> (f.() |> g.())
2
iex(44)> 2 |> f.() |> g.()
6
iex(45)> 2 |> (f.() |> g.())
6
iex(46)> -3 |> f.() |> g.()
-4
iex(47)> -3 |> (f.() |> g.())
-4
なんとなぁく大丈夫そうです。
3関数でやってみる
同様のことは関数がもっと増えても出来ます。
iex(55)> h = & -&1
&:erlang.-/1
と関数 h/1
を定義しておいて f, g, h をいくつかのパターンで合成関数を作った風に使うこともできます。
iex(58)> 0 |> f.() |> g.() |> h.()
-2
iex(59)> 0 |> (f.() |> g.()) |> h.()
-2
iex(60)> 0 |> f.() |> (g.() |> h.())
-2
iex(61)> 0 |> (f.() |> (g.() |> h.()))
-2
からくり
とまあ、やっては見ましたが $g(f(x))$ と $(g \circ f)(x)$ との違いを Elixir がちゃんと処理したかというとはなはだ怪しいです。特にこの方法では関数を合成したつもりの (g.() |> h.())
のところだけ取り出して取り扱うことが出来ないです。
結局のところは、与えた引数が順番に f, g, h と関数適用されて結果が出てきてるだけです。合成関数風にどう書いたところで(パイプ演算子の右側を括弧でまとめたところで)、うしろでまとまったひとかたまりの関数が出現しているわけではありません。
Elixir の元々の文法には関数の合成をする関数は定義されていないようです。ただし、関数自体を第一級オブジェクトとして扱える言語ではあるので、そのような関数を作ることは本質的に可能なはずです。探してみたら同じこと考えてる方々がいらっしゃって実際にできるようです(参考文献を参照)。また時間のあるときに試してみたいです。
まとめ
関数の合成をテーマに Elixir の関数型言語性?を見てみました。無名関数とパイプとを使って Elixir でそれっぽいことはできました。いわゆる純粋関数型言語とは異なり Elixir の言語自体に関数合成の構文や関数を持っていないです。ただし、関数を第一級オブジェクトとして扱うので自分で作ることは可能そうです。
さて、明日の #Elixir Advent Calendar 2021 の記事は ohr486 さんの iex inside です。お楽しみに!
参考文献
Elixir の文法関係。
計算機言語的な話。
OMNI-JP (Open Mobile Network Infra Meetup, Japan) は、モバイルインフラの構成技術要素(RAN,Core,Platform,device 等)の枠を超え、技術者(開発者・ユーザ)が主体的に情報発信したり様々なモバイルインフラのOSSコミュニティの最新情報を展開する等、技術者同士の交流を促進させることを目的としたコミュニティです。
びよんどプロジェクトは、デバイス・エッジ・クラウド・MEC と言ったネットワーク上の様々な計算リソース上で、Elixir をベースにした統一的なシステム構築環境を研究開発するプロジェクトです。
関数型言語で関数合成する話やその周辺の話題がこちら。
- Composing Elixir Functions
- Learn With Me: Elixir - Currying and Partial Application (#32)
- [翻訳] Elixirでの関数カリー化
- 【Elixir】関数で関数を返す(カリー化、部分適用)
- Haskell - Function Composition
- Haskellの関数合成、または (.) 関数
無名関数に名前をつけることに関する話は以下にも書いてます。御覧ください。