やわらかテック

業務を通して得られた知見、知見。個人的に試してみたこと、興味のあることについてアウトプットしています。

数学全然わからないけどElixirでFunctorを作ってみる

きっかけ😲

最近、「入門Haskellプログラミング」という書籍を読み進めています。

毎日1章ずつと非常にスローペースではありますが、第27章のFunctorまで辿り着きました。このFunctorというものが非常に面白かったので自分のメモがてらまとめておきます。

ただ概念やサンプルを写経するだけでは明日、目覚めた時に忘れてしまっている可能性もあるので、自分でFunctorというものを実装してみようと思います。実装にはElixirを使います。過去にも同じようなことをやっていますので、ぜひ覗いてみてください。

www.okb-shelf.work

HaskellにおけるFunctor👓

HaskellではFunctorはクラスという機能を用いて作られています。これは他言語でいうところのinterfaceという概念に近いでしょうか。Functorをメンバーに持つクラス(型)はFunctorに定義されているfmapという関数を独自に定義することでFunctorインスタンスとして扱うことが出来ます。

(正確には<$>という二項演算子もありますが、今回は簡単のために省略します)

hackage.haskell.org

fmapという関数は以下のように定義されています。

fmap :: (a -> b) -> f a -> f b

またHaskellではList, Map, Maybe, IOの4つのクラス(型)がFunctorのメンバーとして定義されています。すなわち、この4つのクラス(型)でfmap関数を使用することが出来ます。この4つのクラス(型)に共通して言えることがコンテナもしくはコンテキストに包まれているということです。

ElixirではMaybeIOという型は存在しないので分かりやすさのために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においてfmapmapと同等だということが分かったので、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ではMapenumerablesに定義されているので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におけるFunctorfmapがよく知られるmap関数であったように、実際に抽象化された概念をコードに書き起こしてみると、身近に使っているものであったと認識することが出来ました。

非常に面白いですね。次はApplicativeという章に進むのが楽しみです。

参考文献

ブログという体裁上、かなり説明を簡略化したり、省いている箇所が多くあります。正しく深く理解するためには書籍を購入して、自分で読んでみることをお勧めします。ボリュームはありますが、数学が全然分からない自分にも易しい1冊です。

hackage.haskell.org