OCamlでSetをmapする関数
今日も今日とてOCamlでプログラムをガリガリ書いていたのですが、どうしても気になってSetをmapする関数を作って見ました。
とはいえ、さすがにSetに組み込むのは無理なので、外部で似たようなことをやるための汎用関数ですが。
let mapper folder adder empty f s = folder (f >> adder) s empty |
こんな感じです。
以下のように、元Setのfold関数や先Setのadd、emptyを与えて使います。
module Int = struct type t = int let compare = compare end module Float = struct type t = float let compare = compare end module IntSet = Set.Make (Int) module FloatSet = Set.Make (Float) (* IntSetからFloatSetへのmap *) let setmap_itof = mapper IntSet.fold FloatSet.add FloatSet.empty (* FloatSetからIntSetへのmap *) let setmap_ftoi = mapper FloatSet.fold IntSet.add IntSet.empty |
ちなみに、単にfoldの結合を隠蔽しているだけなので、mapでも使えます。
一瞬型が合うか不安になりますが、foldの引数とaddの引数を考えるとうまく対応しています。
module IntMap = Map.Make (Int) module FloatMap = Map.Make (Float) (* IntMapからFloatMapへのmap *) let mapmap_itof = mapper IntMap.fold FloatMap.add FloatMap.empty (* FloatMapからIntMapへのmap *) let mapmap_ftoi = mapper FloatMap.fold IntMap.add IntMap.empty |
実はListからSetへの変換も簡単に作れます。
この場合はfが不要なので、自分自身を返すid関数を渡してやります。
let id x = x let iset_of_list = mapper List.fold_right IntSet.add IntSet.empty id |
ちゃんと動作するか確認してみましょう。
以下の例は、10以下の奇数のリストを集合にした後、この集合の平方根を持つ集合へと変換します。
# let s1 = iset_of_list [1;3;5;7];; val s1 : IntSet.t = # let s2 = setmap_itof (float_of_int >> sqrt) s1;; val s2 : FloatSet.t = # FloatSet.elements s2;; - : FloatSet.elt list = [1.; 1.73205080756887719; 2.23606797749979; 2.64575131106459072] |
うまく動作しているようです。
ただし、この関数だけではタプルのリストからMapへ変換したり、SetからMapに変換したり、あるいはMapのkeyだけを取り出したSetを作ったり、といったことは出来ません。
先ほどMapのmapが作れたのは上手いこと1引数関数に見なせたからで、基本的には’a -> ‘bによる写像でないと動かないわけですね。
さて、よくよく上の例を見ると、実はSetとかListとかいった構造はあまり関係なくて、要するに以下のシグネチャを満たすモジュールであれば上の関数を適用できそうです。
module type S = sig type elt type t val empty : t val add : elt -> t -> t val fold : (elt -> 'a -> 'a) -> t -> 'a -> 'a end |
ということはファンクター使えば上手いことaddとかfoldを考えず関数を作れそう……に思いますが、残念なことに、ファンクターは「ストラクチャーを受け取ってストラクチャーを返す」機構のため、「ストラクチャーを受け取って関数を返す」ということは、言語仕様上出来ません。
(* こんなのはできない *) let mapper = functor (S1 : S) -> functor (S2 : S) -> fun f s -> S1.fold (f >> S2.add) s S2.empty |
ただし、勘違いしていたのですが「ファンクターのファンクター」は作れるみたいです。
というわけで、相互にmapを行うMapperは以下のようにして作ることができます。
module Mapper(S1:S)(S2:S) = struct let map f s = S1.fold (f >> S2.add) s S2.empty let remap f s = S2.fold (f >> S1.add) s S1.empty end |
で、これを使うに当たって問題がひとつ。
ListはSと全然適合しない型になっています。
まぁ、この型を持たせようと思うとListがファンクターになってしまうので仕方ないのですが。
というわけで、Sに適合するIntListを作っておきます。
(* Listの型を指定するためのシグネチャ *) module type Type = sig type t end (* リストのファンクタ *) module AbstructList(S:Type) = struct type elt = S.t type t = elt list let empty = [] let add x l = x::l let fold = List.fold_right end (* int型のリスト *) module IntList = AbstructList(Int) (* IntListからIntSetへのMapper *) module IListIsetMapper = Mapper(IntList)(IntSet) (* IntSetからFloatSetへのMapper *) module ISetFSetMapper = Mapper(IntSet)(FloatSet) |
ようやく準備が出来ました。
先ほどと同じことを試してみます。
# let s1 = IListISetMapper.map id [1;3;5;7];; val s1 : IntSet.t = # let s2 = ISetFSetMapper.map (float_of_int >> sqrt) s1;; val s2 : FloatSet.t = # FloatSet.elements s2;; - : FloatSet.elt list = [1.; 1.73205080756887719; 2.23606797749979; 2.64575131106459072] |
うまくいきました。が、いかんせん関数呼び出しが長いです。
実際には、以下のように各モジュールの関数を短い名前で再定義して使うべきでしょうね。
let map_ltoi = IListISetMapper.map let map_itof = ISetFSetMapper.map |
というわけで、思ったより長くなりましたが、Setをmapする方法として、以下の二種類の方法を紹介しました。
fold、add、emptyを取る関数を作る
2つのモジュールを取るファンクターを作る
2つ目の方も、map関数だけだと微妙ですが色々な相互関数を入れればユーティリティとして使えそうな予感がします。
……ま、こんな物を作る必要がある時点で、OCamlのモジュールシステムがこういった面で不便である、ということな気もしますが。むしろ今回一番の収穫は、ファンクターのファンクターでも省略記法が使えることがわかった、というところだったり。
Fanctor(S1:S)(S2:S)みたいな記法は今後色々使いそうです。