Posted at

ReasonMLのFunctor(Module Function)を使って文字列のサブタイプを表現してみる

More than 1 year has passed since last update.


概要

ReasonML/OCamlには、Functor(Module Function)と呼ばれる機能があります。

その名の通り、moduleを受け取ってmoduleを返すことができます。

うまく使うと、複数のmoduleの共通機能をまとめることができそうです。


Set.Make Functor

標準ライブラリのSet.Makeが好例なので、こちらを見て雰囲気をつかむと良いと思います。

https://reasonml.github.io/api/Set.html

ただし、2018/03/11時点ではOCamlの構文でしたので、Reason3でコンパイルする場合は修正が必要です。

以下は自分の記事ですが、Reason3に修正したソースコードになります。

https://qiita.com/soebosi/items/57f96ff029f2910c57d5#%E6%A7%8B%E6%96%87


記事の目標

Functorを使って、正規表現を利用した文字列のサブタイプを作ってみます。


Functor

正規表現パターンを持つモジュールを受け取って、String.subをラップした関数をもつモジュールを返すFunctorを作成してみます。

String.subは、指定した範囲の文字列を返す関数です。

指定した範囲によっては、正規表現にマッチしなくなるので、String.subを呼んだ後にマッチしているか判定し、マッチしない場合はNoneを返すようにします。


SubtypeOfString.re

exception InvalidString;

/* "Functorが受け取るモジュールの型を宣言します" */
module type SubtypeOfString = {let pattern: string;};

/* "宣言したSubtypeOfStringを受け取って、make, unwrap, sub関数をもつモジュールを返します" */
module Make = (ST: SubtypeOfString) => {

/* "生成するモジュールの型として、option(string)をもつレコードを採用しました。" */
type t = {
str: option(string)
};

/* "make: 文字列からサブタイプ文字列を生成します。" */
let make = (s: string) : t => {
let valid = Js.Re.(fromString(ST.pattern) |> test(s));
{str: valid ? Some(s) : None};
};

/* "unwrap: サブタイプ文字列から文字列に変換します。サブタイプ文字列として不正だった場合例外を投げるようにしました。" */
let unwrap = ({str}: t) =>
switch str {
| Some(s) => s
| None => raise(InvalidString)
};

/* "sub: String.subのラッパーです" */
let sub = (start, len, {str}: t) : t =>
switch str {
| Some(s) => String.sub(s, start, len) |> make
| None => {str: None}
};
};



Functorを使う側

URL文字列を正規表現でマッチするかを確認するモジュールをさきほどのFunctorで生成します。

真面目にURLの正規表現を考えるのは辛いので、ここではhttp, httpsで始まる文字列をURL文字列として扱います。


index.re

module UrlString =

SubtypeOfString.Make(
{
let pattern = "https?:\\/\\/";
}
);

let print_url = (url: UrlString.t) =>
switch (UrlString.unwrap(url)) {
| u => Printf.printf("url: %s\n", u)
| exception SubtypeOfString.InvalidString =>
Printf.printf("Incorrect URL!\n")
};

let correctUrl1 = UrlString.(make("http://example.com") |> sub(0, 10));
let correctUrl2 = UrlString.(make("https://example.com") |> sub(0, 10));
let incorrectUrl1 = UrlString.(make("example.com") |> sub(2, 5));
let incorrectUrl2 = UrlString.(make("http://example.com") |> sub(2, 5));

print_url(correctUrl1); /* => url: http://exa */
print_url(correctUrl2); /* => url: https://ex */
print_url(incorrectUrl1); /* => Incorrect URL! */
print_url(incorrectUrl2); /* => Incorrect URL! */



困ったこと1

SubtypeOfString.Makeで作成された別のモジュールの型を渡すと、ちゃんとコンパイルエラーになってくれます。

module FileString =

SubtypeOfString.Make(
{
let pattern = "file:\\/\\/\\/";
}
);

/*
"こちらはコンパイルエラー"
FileString.make("file:///hoge") |> print_url
*/

ですが困ったことがあります。

UrlStringとしてはinvalidな場合でもUrlString.tと宣言されたstr: some(string)をもつレコードは、UrlString.tとして渡すことができてしまいます。

let incorrectUrl3: UrlString.t = {

str: Some("hoge")
};
print_url(incorrectUrl3); /* url: hoge */

防ぐことができないのか、そもそもFunctorで文字列のサブタイプを表現しようとすることが無理があるのかは、今後調べていきたいと思います。


困ったこと2

今回は、SubtypeOfStringStringの関数としてsubを実装しました。

ですが、それ以外のStringの関数もほしいです。

愚直にFunctor側に実装すればよいのですが、共通点があるため自動で生成したいものです。

共通点としては、以下になります。


  • 渡ってきたST.tからstringを取り出す


  • String.****stringを渡す

  • 結果の文字列をST.makeに渡して、returnする

これらを実現するには、マクロのようなことが必要だと思っています。

OCamlには、ppxがあるのでこちらでどのようなことができるのか、今後勉強したいと思います。


まとめ


  • Functorで文字列のサブタイプを表現できた

  • さらに使いこなすには、マクロなどを駆使する必要があるかもしれない

以上です!