この記事はHaskell Advent Calendar 2021の25日目の記事です。
Haskellのよく言われる問題点の一つとして、文字列型が下記のようによく使われるものだけで5種類もある、という点があります:
String
- Strictな
Text
- Lazyな
Text
- Strictな
ByteString
- Lazyな
ByteString
(上記の頻繁に使われるもの以外にも、もっとあります)
それぞれ確かに使いどころが違うので、アプリケーションで使用する場合は場面に応じて使い分ければいいのですが、文字列を使ったライブラリーを開発する場合はなかなか悩ましいものがあります。内部で依存しているライブラリーが使用しているものがあれば、それをそのまま使うのが簡単で確実ですが、そうでない場合も多いでしょう。そこで本稿では文字列型を抽象化して扱いたい場合の手段として、mono-traversalbeパッケージを検討したいと思います。
Link to
heremono-traversableパッケージの紹介
mono-traversableパッケージは、名前のとおりMonoTraversable
やMonoFoldable
、MonoFunctor
といったおなじみの型クラスの名前にMono
という接頭辞を付けた型クラスによって、多様なコンテナ型を抽象化してくれます。これらの型クラスはすべて、ByteString
やText
のような、「要素として持てる値の型が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
= ByteString.map
omap
instance MonoFunctor Text where
= Text.map
omap
instance MonoFunctor [a] where
= map omap
※mono-traversableパッケージのソースから引用して少し改変しました。
さらに、これまで紹介したMonoTraversable
やMonoFoldable
、MonoFunctor
に加えて、SemiSequence
やIsSequence
という型クラスで分解や構築に関わる操作(例えばcons
やbreak
)などの他、(今回は取り上げませんが)SetContainer
などの型クラスでMap
やSet
、IntMap
などの型まで抽象化してくれます。
そこで次の節では、このmono-traversableパッケージにおける型クラスを中心に、Data.Text
モジュールやData.ByteString
モジュールにおける各関数が、どの型クラスに対するどの関数に対応するのか、まとめた表を作ってみました。
Link to
heremono-traversableパッケージにおける型クラスのメソッドと、各種文字列型向け関数の対応表
- ℹ️調査したmono-traversableパッケージのバージョンは1.0.15.3です
- ℹ️原則として関数の名前しか見ていないので、実際には異なる用途かも知れません
- ℹ️mono-traversableパッケージにある型クラスの他、baseパッケージにある
Monoid
、Semigroup
などのメソッドも調査対象に含めました - ℹ️
String
についてはbaseパッケージにある関数のみを対象にしていますが、Data.List
モジュールのドキュメントと自分の記憶を頼りに埋めているので間違いがあるかも知れません - ℹ️
Text
・ByteString
については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
関数の実装を見ると、Text
やByteString
などについてはRULES
プラグマで最適な実装を設定しているようですが、それ以外の型については一旦リストに変換してから結合する、という効率の悪そうな処理をしています。
Link to
here事例: String
から相互変換できる型を抽象化する
最後に、最近私が作った(まだリリースしてない)ライブラリーにおいて、MonoFoldable
とIsString
を使うことで、Text
とString
両方をサポートした関数を紹介しておきます。ただ、時間とやる気パワーが残り少なくなってしまったので、該当の箇所だけこちらからコピペして、説明は簡単にしておきます:
stringVal :: (IsString a, MT.MonoFoldable a, MT.Element a ~ Char) => CodecEnvVal a
= valByFunction CodecEnvValByFunction
stringVal = MT.otoList
{ encode = Right . fromString
, decode }
CodecEnvVal a
型は、a
型をString
型と相互変換するための情報を含んだ型です。stringVal
の場合、名前のとおり文字列っぽい型とString
との相互変換ができなければなりません。もちろん単純にString
型だけをサポートしてText
用には別途CodecEnvVal Text
を作ってもいいのですが、一つのCodecEnvVal a
だけで扱えた方が楽でしょうし、今回はMonoFoldable
のotoList
とIsString
のfromString
を使って両方をサポートすることにしました。なお、これではByteString
がサポートできませんが、ここで相互変換するString
は、要件上人間が読み書きするファイルにおける文字列を想定しているので、ByteString
はバイナリーデータにだけ使うべきだ、という立場から敢えてサポートしていません。
Link to
hereまとめ
mono-traversableパッケージをうまく使えば、自前で専用の型クラスを作らなくてもString
・Text
・ByteString
などを一挙にサポートする関数が書けるかも知れません!
それでは2022年はmono-traversableでHappy Haskell String Programming!🚝