概要
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を返すようにします。
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文字列として扱います。
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
今回は、SubtypeOfString
にString
の関数としてsub
を実装しました。
ですが、それ以外のString
の関数もほしいです。
愚直にFunctor側に実装すればよいのですが、共通点があるため自動で生成したいものです。
共通点としては、以下になります。
- 渡ってきた
ST.t
からstring
を取り出す -
String.****
にstring
を渡す - 結果の文字列を
ST.make
に渡して、returnする
これらを実現するには、マクロのようなことが必要だと思っています。
OCamlには、ppxがあるのでこちらでどのようなことができるのか、今後勉強したいと思います。
まとめ
- Functorで文字列のサブタイプを表現できた
- さらに使いこなすには、マクロなどを駆使する必要があるかもしれない
以上です!