やわらかテック

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

ElixirのEnumモジュールがどのように作られているのか調べてみた

こちらは僕が主催している清流elixir - connpassで扱った内容の備忘録になります。
現在は1ヶ月に一度を目処にオンラインで活動をしています。

今回は愛用してやまないElixirのパワフルなEnumモジュールについて、果たしてどのように作られているのか、すなわちどのようなコードが書かれているのかを掘り下げていきました。

動機としては良いプログラマーになるためには良いコードをたくさん読むのが手っ取り早いからです。とはいえ、いきなり言語のコードを読むのは大丈夫かなと思ったのですが、皆様のご助力もあり、何とかなりました。

参加者の皆さん、ありがとうございました🙇‍♂️

Enumモジュールについて🧪

Elixirでは関数群をモジュールという単位で管理することができます。オブジェクト指向言語で言うところのクラスに近いものでしょうが、継承の概念はありません。ただ関数をまとめるだけになります。

defmodule Sample do
  def foo(:a), do: 1
  def foo(:b), do: 2
  def foo(:c), do: 3
end

合わせてElixirには標準に実装されているモジュールがいくつかあります。その中でもパワフルなのは何と言ってもEnumモジュールです。Enumモジュールに定義されている関数は以下の3つのデータを第一引数に受け取り、処理を実行します。

  • Lists ([1, 2, 3])
  • Maps (%{foo: 1, bar: 2})
  • Ranges (1..3)

In Elixir, an enumerable is any data type that implements the Enumerable protocol.
Lists ([1, 2, 3]), Maps (%{foo: 1, bar: 2}) and Ranges (1..3) are common data types used as enumerables:

hexdocs.pm

関数1つ1つを実行するだけではその真の力を発揮することは出来ません。このEnumモジュールの関数とパイプライン演算子(|>)を組み合わせることで、Enumモジュールは本当の力を発揮します。

1..100
|> Enum.to_list()
|> Enum.filter(fn n -> n < 50 end)
|> Enum.map(fn n -> n + 1 end)
|> Enum.sum()
|> IO.puts() # 1274

このように実行結果の戻り値がEnumモジュールに受け渡し可能な上記3つのデータ構造であれば、無限にパイプラインをつなげることが可能です。

頻繁に使用する関数🍺

勉強会の当日に参加者の皆さんに「Enumでよく使う関数って何ですか?」と尋ねたところ、map, filter, reduceがやはり人気でした。Enum三種の神器ともいえる関数です。(他にもgroup_byatなど...)

mapは各要素に対して関数を適応させる関数です。map関数が使用率が最も高いと判断したので、Enumモジュールに定義されているmap関数のコードを見てみることにします。

f:id:takamizawa46:20210902085737p:plain

enum.exを掘り下げていく⛏

Enumモジュールが定義されているのはこちらのファイルです。

github.com

まずはmap関数を探してみます。コード内でdef mapと検索をかけてやれば簡単に見つけることが出来ました。

@spec map(t, (element -> any)) :: list
def map(enumerable, fun)

def map(enumerable, fun) when is_list(enumerable) do
  :lists.map(fun, enumerable)
end

def map(enumerable, fun) do
  reduce(enumerable, [], R.map(fun)) |> :lists.reverse()
end

これがmap関数の実装です。1, 2行目はdialyxirという型アノテーションライブラリのための定義なので、今回は扱いません。その下にある2つの関数に注目していきます。

パターンマッチを使って2つのケースが想定されています。1つ目は第1引数のenumerableがリストの時です。その場合は単純にErlanglistsモジュールに定義されているmap関数を呼び出しています。

:lists.map(fun, enumerable)

erlang.org

それ以外のケース、すなわち、List, Mapの場合にはファイル内に定義されているreduce/3関数が呼び出されています。

reduce(enumerable, [], R.map(fun)) |> :lists.reverse()

先程と同じようにdef reduceを検索してみると、以下のコードが見つかりました。なお、reduce関数は引数が2つのものと、3つのものがあり先程呼び出されていたのは引数が3つのものだったので、引数が2つのreduce関数については今回は取り扱いません。

def reduce(enumerable, acc, fun) when is_list(enumerable) do
  :lists.foldl(fun, acc, enumerable)
end

def reduce(first..last//step, acc, fun) do
  reduce_range(first, last, step, acc, fun)
end

def reduce(%_{} = enumerable, acc, fun) do
  reduce_enumerable(enumerable, acc, fun)
end

def reduce(%{} = enumerable, acc, fun) do
  :maps.fold(fn k, v, acc -> fun.({k, v}, acc) end, acc, enumerable)
end

def reduce(enumerable, acc, fun) do
  reduce_enumerable(enumerable, acc, fun)
end

mapとは異なり、かなり多くのケースが想定されています。上から順にList, Range, Structs(構造体), Map, それ以外とマッチさせています。余談ですが、Elixir 1.12verからRangeのステップが指定出来るようになったそうです。

1..10//2 |> Enum.to_list() |> IO.inspect()
# [1, 3, 5, 7, 9]

elixir-lang.org

また、%_{}とすることでStructs(構造体)をマッチさせることが出来ます。こちらも初めて知りました。

defmodule Sample do
  def url(%_{}), do: IO.puts("match!")
end

%URI{ authority: "okb" } |> Sample.url() # match!

重要なのはreduce_enumerableという関数です。同じようにこのファイルに定義されています。

defp reduce_enumerable(enumerable, acc, fun) do
  Enumerable.reduce(enumerable, {:cont, acc}, fn x, acc -> {:cont, fun.(x, acc)} end) |> elem(1)
end

何やら見慣れないモジュールを呼び出しています。Enumerableというのは何でしょうか。ヒントはEnumモジュールのドキュメントにあります。このような一文が書かれていました。

In Elixir, an enumerable is any data type that implements the Enumerable protocol

hexdocs.pm

なのでEnumerableというのはモジュールではなく、protocol(プロトコル)であり、Enumerableプロトコルに定義されているreduce関数を呼び出しているということになります。

プロトコルについて🤔

プロトコルは他言語でいうところのインターフェースに近い概念です。プロトコルに指定されている関数を定義することで、特定の型に対しての振る舞いを決定することが出来ます。

defprotocol AsAtom do
  def to_atom(data)
end

これでAsAtomというプロトコルを作成しました。合わせて、AsAtomto_atom(data)という関数を定義する必要があるということを表しています。あとはAsAtomの定義をもとに、それぞれの型に対して振る舞いを実装していきます。例えば、Atomのデータ構造に対してAsAtomプロトコルの振る舞いを定義するにはdefimpl AsAtom, for: Atom doと記述します。

defimpl AsAtom, for: Atom do
  def to_atom(atom), do: atom
end

defimpl AsAtom, for: BitString do
  defdelegate to_atom(string), to: String
end

defimpl AsAtom, for: List do
  defdelegate to_atom(list), to: List
end

defimpl AsAtom, for: Map do
  def to_atom(map), do: List.first(Map.keys(map))
end

上記のコードはElixir Schoolから抜粋しました。

elixirschool.com

Elixir Schoolのコードをもとに、Intergerに対してオリジナルの定義をしてみました。Interger型の値を受け取った場合は必ず0を返すようにします。

defimpl AsAtom, for: Integer do
  def to_atom(map), do: 0
end

実行してみます。

import AsAtom
to_atom(111) |> IO.puts() # 0

実装が反映されていることが確認できました。プロトコルはいわゆるポリモーフィズムの1つであり、3つあるポリモーフィズムのうちのアドホック多相に該当するとのことです。ポリモーフィズムにも3つの種類があるというのは全く知りませんでした...。

先生詳しすぎます...👨‍🏫

twitter.com

Enumerableプロトコル🧪

プロトコルについて理解が深まったところで、最後にEnumerableプロトコルについて追っていきます。Enumerableenum.exのファイル内に実は定義されています。なんとファイルの一番上に定義されていました。

# 簡単のため@moduledocなどは削除してあります
defprotocol Enumerable do
  def reduce(enumerable, acc, fun)
  def count(enumerable)
  def member?(enumerable, element)
  def slice(enumerable)
end

Enumerableプロトコルは上記4つの関数を定義してあげる必要があります。何とこの4つを定義することでEnumモジュールに定義されている関数のほぼ全てが使用可能になるというのです。Enumモジュールに定義されている関数の多くはこのEnumerable.reduce/count/member?/sliceによって実装されているのです。非常によく設計されています。恐るべしEnumモジュール...。

Listへの定義

一例としてListに対してEnumerableプロトコルがどのように定義されているか紹介しておきます。enum.exファイルのほぼ一番下に定義されているのを確認しました。

defimpl Enumerable, for: List do
  def count([]), do: {:ok, 0}
  def count(_list), do: {:error, __MODULE__}

  def member?([], _value), do: {:ok, false}
  def member?(_list, _value), do: {:error, __MODULE__}

  def slice([]), do: {:ok, 0, fn _, _ -> [] end}
  def slice(_list), do: {:error, __MODULE__}

  def reduce(_list, {:halt, acc}, _fun), do: {:halted, acc}
  def reduce(list, {:suspend, acc}, fun), do: {:suspended, acc, &reduce(list, &1, fun)}
  def reduce([], {:cont, acc}, _fun), do: {:done, acc}
  def reduce([head | tail], {:cont, acc}, fun), do: reduce(tail, fun.(head, acc), fun)

  @doc false
  def slice(_list, _start, 0, _size), do: []
  def slice(list, start, count, size) when start + count == size, do: list |> drop(start)
  def slice(list, start, count, _size), do: list |> drop(start) |> take(count)

  defp drop(list, 0), do: list
  defp drop([_ | tail], count), do: drop(tail, count - 1)

  defp take(_list, 0), do: []
  defp take([head | tail], count), do: [head | take(tail, count - 1)]
end

github.com

先程、紹介した4つの関数が定義されています。何やらそれぞれの関数がタプルを返していますが、これはStream型のデータに対応するための記述だそうです。Streamは遅延のEnumとして定義されているとドキュメントに記載がありました。

Streams are composable, lazy enumerables

hexdocs.pm

今回はタプルの返す値の意味については割愛しますが、Enumerableプロトコルに定義された4つの関数全てが定義されているのが確認出来ます。そしてこの4つの関数によってEnumモジュールに定義されている関数のほぼ全てを使うことが出来るのです。

まとめ📖

Enumモジュールを掘り下げていくことでどのように作られているのかが分かりました。

  • Enumはモジュールであり関数をまとめたもの
  • Enumモジュールはプロトコルを使って実装されている
  • プロトコルとは振る舞いを定義したインターフェースのようなもので、型に対して定義する
  • Enumerableプロトコルreduce/count/member?/sliceの4つの関数を定義する必要がある
  • 4つの関数を定義することで、Enumモジュールに定義されている関数をほぼ全て使うことが出来る

普段、自分が使っている言語の仕組みが分かると非常に面白いですね。
中々、一人では挫折してしまうような内容ですが、皆さんの知識を総動員して掘り下げていった結果、Enumモジュールを完全に理解した状態になれました。

好評でしたら、次は別のモジュールなどを掘り下げていこうと思います。

elixir-sr.connpass.com

【コピペでOK】はてなブログでソースコードをエディタ風に表示するCSS

デフォルトのデザインが何ともいえない...🤔

こちらがはてなブログソースコード<pre>タグで表示した時に表示されるデフォルトのソースコードです。
自分はMarkdown記法を使用していまして、バッククオート3つで囲うことでソースコードを記述しています。

f:id:takamizawa46:20210826204140p:plain

良くも悪くも何とも言えないですね...。
今までデフォルトのデザインのままブログを書いてきましたが、最近になってデザインが気になり始めました。
普段自分が使用しているエディタ(VScode)のようにソースコードを表示できないものかと色々、試していました。

...

試行錯誤の結果、ようやく自分が納得できる形になったので、紹介してみようと思います。

完成形🦩

というわけで...
こちらがデザインをカスタマイズして表示されるようになったソースコードです。

f:id:takamizawa46:20210826204632p:plain

どうでしょうか。以前のものと比べるとかなり見やすくなったと思います。

基本的には他の方が公開されていたコードを組み合わせて、多少、手を加えただけです。
ソースコード内で使用している言語名と行数を表示するようにして、ダークテーマなエディタ風の良い感じに仕上がりました。カスタマイズしている内容は大きく5つになります。

  • コードは文字数が多くなりがちなので<pre>タグ内のフォントサイズを従来指定サイズより10%小さく変更
  • 左部分にコードの行数を表示
  • 上部に言語名を表示
  • <pre>タグの上部が狭いと感じたので30pxほどpaddingを追加
  • Monokaiというテーマ基調のダークテーマに変更

カスタマイズにあたり、以下の2つのサイトを参考にしました。
素敵なコードを公開して頂きましてありがとうございます🙇‍♂️

souiunogaii.hatenablog.com

wakalog.hatenadiary.jp

コピペするコード

まずは以下のコードを「はてなブログの管理画面 -> デザイン -> カスタマイズ:デザインCSS」に貼り付けて下さい。
すでに既存のコードがある場合は既存コードを誤って削除しないように注意して下さい。

/* code boxの見た目 */
/* ソースコードのフォントはちょっと小さめにする */
pre {
  font-size: 90%;
}

/*code-lineクラスの数でカウント*/
.code-line {
  counter-increment: linenumber;
}

/*行番号を擬似要素として表示*/
.code-line::before {
  content: counter(linenumber);
  display:inline-block;
  color: #ccc;
  text-align: right;
  width: 35px;
  padding: 0 15px 0 0;
}

/*コードブロックに言語名を表示*/
pre.code:before {
  width: 99%;
  content: attr(data-lang);
  display: inline-block;
  background: #454545;
  color: #fff;
  padding: 0.5px 0px;
  padding-left: 1%;
  border-radius: 4px 4px 0 0;
  position: absolute;
  margin-left: -10px;
  margin-top: -30px;
}

pre.code {
  padding-top: 30px !important;
}

/*Monokai*/
.entry-content pre.code {
  background-color: #272822;      /*背景色*/
  color: #F8F8F2;                 /*テキスト*/
}

.synComment { color: #75715E }      /*コメント*/
.synSpecial { color: #E6DB74 }      /*特殊文字*/
.synType { color: #66D9EF }         /*型*/
.synPreProc { color: #F92672 }      /*プリプロセッサ*/
.synStatement { color: #F92672 }    /*ステートメント*/
.synIdentifier { color: #F8F8F2 }   /*識別子*/
.synConstant { color: #AE81FF }     /*定数*/

次に、以下のコードを「はてなブログの管理画面 -> デザイン -> カスタマイズ:フッタ」へ貼り付けます。

<script>
  var codeBlocks = document.getElementsByClassName('code');
  [].forEach.call(codeBlocks, function(e) {
    if (!/lang/.test(e.className)) {
        return;
    }
    var sourceCode = e.innerHTML.slice(-1) === '\n' ? e.innerHTML.slice(0, -1) : e.innerHTML;
    var lines = sourceCode.split(/\n/);
    var codeBlock = "";
    lines.forEach(function(line){
      line += line === '' ? '\n' : '';
      codeBlock += '<div class="code-line">' + line + '</div>'
    })
    e.innerHTML = codeBlock;
  });
</script>

変更は以上になります。

「変更を保存する」をクリックして上記2点の変更を反映させた後、ソースコードが記載されている自身の記事を確認してみて下さい。 問題なくデザインが変更されていれば作業は終了です。

参考までに...Elxiirですとこんな感じに表示されます🙌

# Enumとパイプライン演算子の組み合わせ
pipeline = Enum.map(1..100, fn n -> n end) # [1..100]の要素を持つ配列を作成
            |> Enum.map(fn n -> n * 2 end) # 各要素を2倍に
            |> Enum.filter(fn n -> n < 50 end) # 50以下の要素を排除
            |> Enum.reduce(0, fn n, acc -> acc + n end) # 残った要素を合計
            
IO.puts(pipeline)
# 600

ハッキリと「この機能必要ですかね」と言うことが一番重要だった話

お客様が増えると要望は増え続けていく⏫

現在、国内でHRテックのアプリケーションの開発を担当しています。
ありがたいことに、今となってはかなり多くのお客様に使って頂いておりまして、成長期を抜けて成熟期に突入しました。

www.nri.com

そんな運営中のアプリケーションに対して、お客様が増えれば増えるほど、今まで考えていなかったような要望やニーズが営業さんを通して聞こえるようになります。

要望は本当に様々です。既存機能の仕様変更を求める声であったり、一括更新機能のような既存機能を強化するもの。もしくは全く別の新しい機能が欲しいという場合もあります。

さて、このような多種多様な声を取り入れたその先には混乱と混沌が待っていました。

www.okb-shelf.work

あれもこれも欲しい😓

一人でも多くのお客様の要望を叶えるため、頂いた声は可能な限り叶えるようにしました。
小さな仕様変更から、アプリケーションのコンセプトを変えてしまうような大きな仕様変更。「これ他のお客様は使うのかな?」と不安になるような個別カスタマイズに近い仕様変更、もしくは機能追加を行いました。

まるで駄々をこねる子供のように「あれも!これも!〇〇機能が欲しい!」という状態に陥りました。

...


結果として、要望の数が多すぎて開発の優先順位の整理が出来なくなり、複雑さがマシマシで仕様を把握しきれている人間がいないというレベルになってしまいました。問い合わせがあった際に、あるべき姿が人によって認識が異なるという状態です。

私が所属する開発チームでは「優先順位がないと何からやるべきが判断できない」「複雑すぎる」という声を定期的に上げています。しかし、それでもお客様は増え続けています。従って、機能追加や仕様変更は今日も行われていきます。

ハッキリと意見を言う🧐

上記の流れが繰り返し繰り返し、行われてきた結果、生み出されたのは肥大化したソースコードです。複雑な設定と判定条件まみれになりました。 とても、これから開発をスマートに行なっていくことは出来ません。

では、どうしたら良かったのでしょうか。
心の中では思っていたこと「この機能必要ですかね?」という一言をハッキリと言えていれば良かったのです。

  • 誰が使うのか
  • 何のために使うのか
  • どれぐらいのお客様が欲しいと思っているのか
  • 導入前のユーザーストーリーは導入後にどのように変わるのか ...

1つでも確認出来ていないのであれば、それは必要な機能とは言えません。
そんな時にはハッキリと言わねばなりません。誰かがこれをやらねばならぬ。宇宙戦艦ヤマトの歌詞にもそう書かれていますね。

誰かがこれを やらねばならぬ
期待の人が 俺たちならば

ささきいさお 宇宙戦艦ヤマト 歌詞 - 歌ネット

お客様はアプリケーションについては素人であり、他のお客様がどう使っているかを考慮しないので、上がってきた要望に対してしっかりと深堀し、全体にとってのあるべき姿を模索します。

この工程をサボることで将来、自分達に負債が返ってくるということを身を持って学びました。

プロダクトは誰のもの?🤔

この問いかけをよく外部メンターの方に聞かれます。答えは「お客様のもの」です。使っていただけなければ、どれだけ綺麗でモダンな言語で記述されたコードも価値を成しません。

先程の話で既存仕様を変更したことで、他のお客様の不満を生み出してしまいました。新規機能追加も同様です。今まで満足に使って頂いていたのに新たな機能が増えたことで、必要ないお客様にとっては手間となってしまうのです。

増やしていくだけが価値ではありません。既存機能への修正やコードのリファクタリング。使われていない機能の削除。インフラの強化や安定化、堅牢性を高める...などなど、やることはたくさんあるのです。

「この機能必要ですかね?」という一言を勇気をもって言っていきます🔥

【コスパ最強】Ankerのマグネットケーブルホルダーで机の配線を最終形態にしました

配線が見えなくなったものの...🙈

僕は配線が見えるのが嫌で、極力、配線が見えないようにしてします。
今年の春頃に充電コード(lightning, type-c)やモニターの出力ケーブル(HDMI)を可能な限り見えない状態にしました。

ケーブル類はテーブル下のケーブルトレーにまとめ、最低限必要だと判断した3つのケーブルにはマグネットで固定するケーブルホルダーを写真のように壁に貼り付けて使っていました。「なんで壁側に貼ってるの?」かと言うと、天板の側面にLEDテープを貼っており、その都合上で壁紙にケーブルホルダーを貼り付けていました。

当時はこれで「快適だなぁ」と思っていたのですが、3ヶ月ほど使っていて、新たな問題が発生しました。


配線を整理した過去の記事はこちら

www.okb-shelf.work

3つの問題🤔

この商品が元々、使っていたケーブルホルダーです。自分の場合、ケーブルの位置や必要な種類が変化したので役不足となってしまいましたが、下記3つの問題に該当しない方にはオススメしたい商品です。この手の商品の中では一番作りがしっかりしていました。やや太いケーブルにも取り付けることが出来ます。100均の商品とは頑丈さ、磁石の強力さが段違いです。

1.徐々に粘着テープが剥がれはじめた

ケーブルホルダーには3M規格の粘着テープが付属しており、そちらを使用して壁に貼り付けていました。しかしながら壁紙の材質と相性が悪かったのか、はたまたケーブルに柔軟性がなく、粘着テープにあらゆる方向に力がかかり、ねじるような状態を生み出してしまったか...で徐々に粘着テープが壁から剥がれてきました。

ホームセンターで購入した強力両面テープを上から貼り直してみましたが、同じ現象が何度も発生して安定することはありませんでした。

2.貼り付け直すのが面倒

天板の側面にLEDテープを貼り付けている都合上、粘着テープを壁側に貼っていました。しかし、リモートワークでずっと同じ部屋にいるとたまに模様替えがしたくなります。いざテーブルを移動させようと思ったら壁側に貼ってある粘着テープを貼り直す必要があります。

せっかく貼り付けた粘着テープを剥がし、再度、移動先に貼り付け直すのが非常に大変でした。当然、粘着テープも無限に粘着力を保持しているわけではありませんので、貼り付け直す度に粘着力は弱くなっていきます。

1つ目の問題とも合わさって、粘着テープを何度も貼り直すのは、当然ですが極力避けるべきだと思いました。

3.接着の横ラインが不揃いで気になる

僕が使っていた粘着テープは1つ1つを個別で貼り付けるタイプのものでした。購入した当時は左側にケーブルが2本、右側に1本という構成にしたかったのですが、色々とテーブルの最適化を進めていった結果、3本のケーブルが左側に集結することになりました。
3つ合わせて粘着テープの最適な位置、横と高さを模索する上で何度も、位置決めを考えました。いざ、粘着テープを付けてみると、剥がれてきたり、横の位置がずれていたりと視界に入ると非常に気になりました。「気にしすぎなんじゃないか」とも思いましたが、貼り直す際の手間を考えると我慢出来なくなってしまいました。

Amazonタイムセールで購入🙌

というわけで...

前から気になっていたこちらの商品がAmazonのタイムセールで300円ほど安くなっていたので、購入してみました。

Anker Magnetic Cable Holder マグネット式 ケーブルホルダー

厚紙製の箱に最低限の付属品というAnkerらしい商品です。

さっそく、使用しているケーブルに取り付けていきます。

これがiPhoneに付属しているAppleの純正のlightningケーブルです。

この手のケーブルは細めのタイプのものが、多く、今回紹介したAnkerのケーブルホルダーだと、径が大きくずれ落ちてしまいます。なので根本の太くなっている箇所に取り付けました。

また、type-cケーブルによく見られる、やや太い形状のケーブルだとAnkerのケーブルホルダーを取り付けるのは難しいです。無理矢理、取り付けても良いのですが、大きく口をあけている状態の癖が付いてしまうでしょう。

上側のケーブルがAnkerのケーブルホルダーが取り付けられない太さだった

こちらには元々、使っていたマグネット式のケーブルホルダーを以前のまま取り付けました。Ankerのケーブルホルダーは径の大きさが、それほど大きくないので太めのケーブルに使用する時は注意が必要です。

最後にmicroUSBのコードにもケーブルホルダーを取り付けてケーブルへの処置は完了です。あとは手頃な位置にボードを貼り付けます。左側の元々、粘着テープを貼り付けていた箇所が空いたので、そちらに貼り付けました。

完成🎉

こんな感じになりました。非常にスッキリしていて最高です。

先ほど説明した3つの問題を完全にクリアすることが出来ました。粘着力を心配する必要はありませんし、机を移動する際に、貼り直す必要もありません。マグネットを固定する位置も揃っていて、言うことなしという状態になりました。

引いた位置から机全体を写してみました。

配線の存在を感じさせないデスク環境に仕上げることが出来て大満足です🤗

数学全然わからないけど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