文字列型を抽象化するのにはmono-traversableパッケージがいいかも

Posted by YAMAMOTO Yuji(@igrep) on December 25, 2021

この記事はHaskell Advent Calendar 202125日目の記事です。

Haskellのよく言われる問題点の一つとして、文字列型が下記のようによく使われるものだけで5種類もある、という点があります:

  • String
  • StrictText
  • LazyText
  • StrictByteString
  • LazyByteString

(上記の頻繁に使われるもの以外にも、もっとあります)

それぞれ確かに使いどころが違うので、アプリケーションで使用する場合は場面に応じて使い分ければいいのですが、文字列を使ったライブラリーを開発する場合はなかなか悩ましいものがあります。内部で依存しているライブラリーが使用しているものがあれば、それをそのまま使うのが簡単で確実ですが、そうでない場合も多いでしょう。そこで本稿では文字列型を抽象化して扱いたい場合の手段として、mono-traversalbeパッケージを検討したいと思います。

Link to
here
mono-traversableパッケージの紹介

mono-traversableパッケージは、名前のとおりMonoTraversableMonoFoldableMonoFunctorといったおなじみの型クラスの名前にMonoという接頭辞を付けた型クラスによって、多様なコンテナ型を抽象化してくれます。これらの型クラスはすべて、ByteStringTextのような、「要素として持てる値の型が1種類だけ」の型も対象にしているのが特徴です。Type Familyを応用し、次のように型毎に要素の型を固定することで、そうした特徴を実現しています:

type family Element mono

type instance Element ByteString = Word8
type instance Element Text = Char
type instance Element [a] = a

-- ...

class MonoFunctor mono where
  -- ...
  omap :: (Element mono -> Element mono) -> mono -> mono

instance MonoFunctor ByteString where
  omap = ByteString.map

instance MonoFunctor Text where
  omap = Text.map

instance MonoFunctor [a] where
  omap = map

mono-traversableパッケージのソースから引用して少し改変しました。

さらに、これまで紹介したMonoTraversableMonoFoldableMonoFunctorに加えて、SemiSequenceIsSequenceという型クラスで分解や構築に関わる操作(例えばconsbreakなどの他、(今回は取り上げませんが)SetContainerなどの型クラスでMapSetIntMapなどの型まで抽象化してくれます。

そこで次の節では、このmono-traversableパッケージにおける型クラスを中心に、Data.TextモジュールやData.ByteStringモジュールにおける各関数が、どの型クラスに対するどの関数に対応するのか、まとめた表を作ってみました。

Link to
here
mono-traversableパッケージにおける型クラスのメソッドと、各種文字列型向け関数の対応表

  • ℹ️調査したmono-traversableパッケージのバージョンは1.0.15.3です
  • ℹ️原則として関数の名前しか見ていないので、実際には異なる用途かも知れません
  • ℹ️mono-traversableパッケージにある型クラスの他、baseパッケージにあるMonoidSemigroupなどのメソッドも調査対象に含めました
  • ℹ️Stringについてはbaseパッケージにある関数のみを対象にしていますが、Data.Listモジュールのドキュメントと自分の記憶を頼りに埋めているので間違いがあるかも知れません
  • ℹ️TextByteStringについてはStrictなバージョンのドキュメントのみ参照しています。Lazyな方になかったらごめんなさい!
  • ℹ️Textual型クラスについては、ByteStringがインスタンスになっていないのでご注意ください
  • ℹ️以下のような関数は除外しました:
    • IOが絡むもの
    • プリミティブな処理で使うもの
Text ByteString String ([Char]) 型クラス / 関数
all all all MonoFoldable / oall
any any any MonoFoldable / oany
append append ++ Semigroup / <>
N/A breakByte N/A N/A
N/A breakEnd N/A N/A
breakOnAll N/A N/A N/A
breakOnEnd N/A N/A N/A
breakOn breakSubstring N/A N/A
break break break IsSequence / break
center N/A N/A N/A
chunksOf N/A N/A N/A
commonPrefixes N/A N/A N/A
compareLength N/A N/A N/A
concatMap N/A concatMap MonoFoldable / ofoldMap
concat concat concat MonoFoldable / ofold
cons cons N/A SemiSequence / cons
copy copy N/A N/A
count count N/A N/A
dropAround N/A N/A N/A
dropEnd N/A N/A IsSequence / dropEnd
dropWhileEnd dropWhileEnd dropWhileEnd N/A
dropWhile dropWhile dropWhile IsSequence / dropWhile
drop drop drop IsSequence / drop
N/A elemIndexEnd N/A N/A
N/A elemIndex elemIndex N/A
N/A elemIndices elemIndices N/A
N/A elem elem MonoFoldable / oelem
empty empty "" Monoid / mempty
filter filter filter IsSequence / filter
N/A findIndexEnd N/A N/A
findIndex findIndex findIndex N/A
N/A findIndices findIndices N/A
N/A findSubstring N/A N/A
N/A findSubstrings N/A N/A
find find find SemiSequence / find
foldl' foldl' foldl' MonoFoldable / ofoldl'
foldl1' foldl1' foldl1' MonoFoldable / ofoldl1Ex'
foldl1 foldl1 foldl1 N/A
foldl foldl foldl N/A
N/A foldr' N/A N/A
N/A foldr1' N/A N/A
foldr1 foldr1 foldr1 MonoFoldable / ofoldr1Ex
foldr foldr foldr MonoFoldable / ofoldr
groupBy groupBy groupBy IsSequence / groupBy
group group group IsSequence / group
head head head MonoFoldable / headEx
index index index IsSequence / indexEx
init init init IsSequence / initEx
inits inits inits N/A
intercalate intercalate intercalate MonoFoldable / ointercalate
intersperse intersperse intersperse SemiSequence / intersperse
isInfixOf isInfixOf isInfixOf IsSequence / isInfixOf
isPrefixOf isPrefixOf isPrefixOf IsSequence / isPrefixOf
isSuffixOf isSuffixOf isSuffixOf IsSequence / isSuffixOf
justifyLeft N/A N/A N/A
justifyRight N/A N/A N/A
last last last MonoFoldable / lastEx
length length length MonoFoldable / olength
lines N/A lines Textual / lines
mapAccumL mapAccumL mapAccumL N/A
mapAccumR mapAccumR mapAccumR N/A
map map map MonoFunctor / omap
maximum maximum maximum MonoFoldable / maximumEx
minimum minimum minimum MonoFoldable / minimumEx
N/A notElem notElem MonoFoldable / onotElem
null null null MonoFoldable / onull
pack pack id IsString / fromString
partition partition partition IsSequence / partition
replace N/A N/A IsSequence / replaceSeq
replicate replicate replicate IsSequence / replicate
reverse reverse reverse SemiSequence / reverse
scanl1 scanl1 scanl1 N/A
scanl scanl scanl N/A
scanr1 scanr1 scanr1 N/A
scanr scanr scanr N/A
singleton singleton singleton MonoPointed / opoint
snoc snoc snoc SemiSequence / snoc
N/A sort sort SemiSequence / sort
N/A spanEnd N/A N/A
span span span SemiSequence / span
splitAt splitAt splitAt IsSequence / splitAt
splitOn N/A splitOn IsSequence / splitSeq
N/A splitWith N/A IsSequence / splitElem
split splitWith N/A IsSequence / splitWhen
stripEnd N/A N/A N/A
stripPrefix stripPrefix stripPrefix IsSequence / stripPrefix
stripStart N/A N/A N/A
stripSuffix stripSuffix N/A IsSequence / stripSuffix
strip N/A N/A N/A
tail tail tail IsSequence / tail
tails tails tails N/A
takeEnd N/A N/A N/A
takeWhileEnd takeWhileEnd N/A N/A
takeWhile takeWhile takeWhile IsSequence / takeWhile
take take take IsSequence / take
toCaseFold N/A N/A Textual / toCaseFold
toLower N/A N/A Textual / toLower
toTitle N/A N/A N/A
toUpper N/A N/A Textual / toUpper
transpose transpose N/A N/A
uncons uncons N/A IsSequence / uncons
unfoldrN unfoldrN N/A N/A
unfoldr unfoldr unfoldr N/A
unlines N/A unlines Textual / unlines
unpack unpack id MonoFoldable / otoList
unsnoc unsnoc N/A SemiSequence / unsnoc
unwords N/A unwords Textual / unwords
words N/A words Textual / words
zipWith zipWith zipWith MonoZip / ozipWith
zip zip zip MonoZip / ozip

以上です。残念ながら万能とはいかないようで、いくつか「N/A」、すなわち対応するものがない関数もありますが、他の関数の組み合わせで実装できるものもあるでしょう。

Link to
here
⚠️パフォーマンスに関わる注意事項

MonoTraversableなどに限らず、型クラスを使って関数を多相化したとき全般に言えることですが、コンパイル時にインスタンスの解決が行えなかった場合、直接対象の型の相当する関数を呼ぶより少し遅くなってしまう場合があります(参考)。

また、それに限らず、各型クラスのメソッドでない関数は、各型の相当する関数でオーバーライドできないため、効率の悪い処理になってしまう恐れがあります。例えば、ointercalate関数の実装を見ると、TextByteStringなどについてはRULESプラグマで最適な実装を設定しているようですが、それ以外の型については一旦リストに変換してから結合する、という効率の悪そうな処理をしています。

Link to
here
事例: Stringから相互変換できる型を抽象化する

最後に、最近私が作った(まだリリースしてない)ライブラリーにおいて、MonoFoldableIsStringを使うことで、TextString両方をサポートした関数を紹介しておきます。ただ、時間とやる気パワーが残り少なくなってしまったので、該当の箇所だけこちらからコピペして、説明は簡単にしておきます:

stringVal :: (IsString a, MT.MonoFoldable a, MT.Element a ~ Char) => CodecEnvVal a
stringVal = valByFunction CodecEnvValByFunction
  { encode = MT.otoList
  , decode = Right . fromString
  }

CodecEnvVal a型は、a型をString型と相互変換するための情報を含んだ型です。stringValの場合、名前のとおり文字列っぽい型とStringとの相互変換ができなければなりません。もちろん単純にString型だけをサポートしてText用には別途CodecEnvVal Textを作ってもいいのですが、一つのCodecEnvVal aだけで扱えた方が楽でしょうし、今回はMonoFoldableotoListIsStringfromStringを使って両方をサポートすることにしました。なお、これではByteStringがサポートできませんが、ここで相互変換するStringは、要件上人間が読み書きするファイルにおける文字列を想定しているので、ByteStringはバイナリーデータにだけ使うべきだ、という立場から敢えてサポートしていません。

Link to
here
まとめ

mono-traversableパッケージをうまく使えば、自前で専用の型クラスを作らなくてもStringTextByteStringなどを一挙にサポートする関数が書けるかも知れません!

それでは2022年はmono-traversableHappy Haskell String Programming!🚝