きっかけ😲
最近、「入門Haskellプログラミング」という書籍を読み進めています。
毎日1章ずつと非常にスローペースではありますが、第27章のFunctor
まで辿り着きました。このFunctor
というものが非常に面白かったので自分のメモがてらまとめておきます。
ただ概念やサンプルを写経するだけでは明日、目覚めた時に忘れてしまっている可能性もあるので、自分でFunctor
というものを実装してみようと思います。実装にはElixir
を使います。過去にも同じようなことをやっていますので、ぜひ覗いてみてください。
HaskellにおけるFunctor👓
Haskell
ではFunctor
はクラスという機能を用いて作られています。これは他言語でいうところのinterface
という概念に近いでしょうか。Functor
をメンバーに持つクラス(型)はFunctor
に定義されているfmap
という関数を独自に定義することでFunctor
のインスタンスとして扱うことが出来ます。
(正確には<$>
という二項演算子もありますが、今回は簡単のために省略します)
fmap
という関数は以下のように定義されています。
fmap :: (a -> b) -> f a -> f b
またHaskell
ではList
, Map
, Maybe
, IO
の4つのクラス(型)がFunctor
のメンバーとして定義されています。すなわち、この4つのクラス(型)でfmap
関数を使用することが出来ます。この4つのクラス(型)に共通して言えることがコンテナもしくはコンテキストに包まれているということです。
Elixir
ではMaybe
とIO
という型は存在しないので分かりやすさのためにList
を使用して説明を進めます。
List
という型は空配列もしくは何らかの型の値をN(N > 1)個、格納します。つまり、List
は何らかの型の値を包み込むコンテナだと考えることが出来ます。Map
についても同様です。
さてfmap
とはこのようなコンテナ、コンテキストの内部の値に対して、処理を適応させるために非常に便利なものです。List
の場合は各要素、全てに処理を適応させるためのfmap
を定義する必要があります。
各要素に適応...どこかで聞き覚えのあるフレーズです。そうです。まさにmap
関数がList
におけるfmap
の定義になります。
-- Functor [] fmap :: (a -> b) -> [a] -> [b] -- Data.List map map :: (a -> b) -> [a] -> [b]
つまり、Functor
は結構身近なものだったりします。Functor
を用いることでコンテナ、コンテキストの内部の値を変更することが可能となります。仮にFunctor
がない場合、List
の各要素に処理を適応させる関数を一つ一つ定義していく必要があります。非常に面倒です。
defmodule MyList do def add_n([], _), do: [] def add_n([h | t], n), do: [h + n | add_n(t, n)] def multiple_n([], _), do: [] def multiple_n([h | t], n), do: [h * n | multiple_n(t, n)] end data = Enum.to_list(1..10) MyList.add_n(data, 1) |> IO.inspect # [2, 3, 4, 5, 6, 7, 8, 9, 10, 11] MyList.multiple_n(data, 10) |> IO.inspect # [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
Functor
がある場合
data = Enum.to_list(1..10) Enum.map(data, fn n -> n + 1 end) |> IO.inspect Enum.map(data, fn n -> n * 10 end) |> IO.inspect
Elixirでの実装🧪
List
においてfmap
がmap
と同等だということが分かったので、List
に対してfmap
を定義してみます。Elixir
ではmap
関数を使用するためにEnum
モジュールに定義されたmap
関数を使用します。
data = Enum.to_list(1..5) # [1, 2, 3, 4, 5] fmap = fn func, lst -> Enum.map(lst, func) end result = fmap.(fn n -> n + 1 end, data) IO.inspect(result) # [2, 3, 4, 5, 6]
Enum
を使うのはちょっとズルい気もしたので、map
を自前で用意したものも定義してみました。
defmodule Functor do def map([], _), do: [] def map([h | t], func), do: [func.(h) | map(t, func)] end data = Enum.to_list(1..5) fmap = fn func, lst -> Functor.map(lst, func) end result = fmap.(fn n -> n + 1 end, data) IO.inspect(result) # [2, 3, 4, 5, 6]
List
の全ての要素に対して、第一引数で渡した関数が適応されていることが確認出来ます。次にMap
でも実装してみましょう。やることは全く同じです。Elixir
ではMap
もenumerables
に定義されているのでEnum.map
を使用することが出来ます。
# %{"1" => "Mr.1", "2" => "Mr.2", "3" => "Mr.3", "4" => "Mr.4", "5" => "Mr.5"} data = Enum.reduce(1..5, %{}, fn n, accum -> Map.put(accum, "#{n}", "Mr.#{n}") end) fmap = fn func, map -> Enum.map(map, func) end result = fmap.(fn { _, name } -> "#{name}! hello!" end, data) IO.inspect(result) # ["Mr.1! hello!", "Mr.2! hello!", "Mr.3! hello!", "Mr.4! hello!", "Mr.5! hello!"]
最後に構造体でも作ってみます。今回はProduct
という構造体を定義しました。fmap
は定義した構造体が持つ、2つのフィールドを別々に引数で受け取り新たなProduct
を作成して返すという定義にしました。
defmodule Product do defstruct name: "", price: 0 def fmap(func, %Product{ name: name, price: price }) do { n_name, n_price } = func.(name, price) %Product{ name: n_name, price: n_price } end end stones = %Product{ name: "大人気! その辺に落ちていた石の詰め合わせ", price: 2000 } summer_sale = Product.fmap(fn name, price -> { "[SummerSale]: #{name}", price - 200 } end, stones) # %Product{ name: "[SummerSale]: 大人気!その辺に落ちていた石の詰め合わせ", price: 1800 } price_hike = Product.fmap(fn name, price -> { "[市場価格高騰]: #{name}", price + 2000 } end, stones) # %Product{ name: "[市場価格高騰]: 大人気!その辺に落ちていた石の詰め合わせ", price: 4000 }
構造体というコンテナの中身に対しても関数の処理を適応させることが出来ました。
総括
Functor
というのが圏論という数学の概念から取り入れたもので難しいものだと考えがちですが、それは数学での定義なので、プログラミング言語での定義とはまた別です。
List
におけるFunctor
のfmap
がよく知られるmap
関数であったように、実際に抽象化された概念をコードに書き起こしてみると、身近に使っているものであったと認識することが出来ました。
非常に面白いですね。次はApplicative
という章に進むのが楽しみです。
参考文献
ブログという体裁上、かなり説明を簡略化したり、省いている箇所が多くあります。正しく深く理解するためには書籍を購入して、自分で読んでみることをお勧めします。ボリュームはありますが、数学が全然分からない自分にも易しい1冊です。