テキストI/OのUnicode対応

 従来のGHCでは,Unicode対応が中途半端な状態でした。Haskell Platform 2010.1.0.0に含まれるGHC 6.12.1では,この点が改善されました。

 これまでも,ソースコードにUTF-8を使用したり,Char型でUnicodeを扱うことはできました。また,OSのシステムコールがワイド文字列用のAPIを用意している場合には,FFIを通してUnicode文字列の入出力を扱えました。

Prelude Foreign.C.Types> :i CWchar
newtype CWchar = Foreign.C.Types.CWchar GHC.Int.Int32
        -- Defined in Foreign.C.Types
~ 略 ~
instance Show CWchar -- Defined in Foreign.C.Types

Prelude Foreign.C.Types> :i CWchar
newtype CWchar = Foreign.C.Types.CWchar GHC.Word.Word16
        -- Defined in Foreign.C.Types
~ 略 ~
instance Show CWchar -- Defined in Foreign.C.Types
Prelude Foreign.C.Types> :m Foreign.C.String
Prelude Foreign.C.String> :browse
~ 略 ~
type CWString = GHC.Ptr.Ptr Foreign.C.Types.CWchar
type CWStringLen = (GHC.Ptr.Ptr Foreign.C.Types.CWchar, Int)
~ 略 ~
newCWString :: String -> IO CWString
newCWStringLen :: String -> IO CWStringLen
~ 略 ~
peekCWString :: CWString -> IO String
peekCWStringLen :: CWStringLen -> IO String
~ 略 ~
withCWString :: String -> (CWString -> IO a) -> IO a
withCWStringLen :: String -> (CWStringLen -> IO a) -> IO a

 しかし,OSのシステムコールがワイド文字列用のAPIを提供していないこともあります。その場合,System.IOモジュールで提供されている関数群はUnicode文字列の入出力に対応していないため,テキスト処理で日本語文字列をうまく扱えませんでした。このため,本連載ではこれまでSystem.IOの関数についての説明をできるだけ避けてきました。

 GHC 6.12.1は,System.IOのこの問題に対処しました。「Haskellプログラム内部の文字コードと外部で使われている文字コードを相互に変換するレイヤー」がハンドルに追加されたのです。これにより,OSがワイド文字列用のAPIを提供していなくても,System.IOモジュールを使った入出力にUnicode文字列を使えるようになりました(参考リンク1参考リンク2)。

 実際に試してみましょう。

 LinuxやMac OS XなどのUnix環境では,ロケール(locale)の設定を見て自動的に適切な文字コードに変換してくれます。ロケールは,言語や時刻など,使用しているOSのローカルな設定を決めるためのものです。

Prelude System.IO> putStrLn "テスト"
テスト

 一方,Windowsでは,UTF(UCS Transformation Format)とHaskellプログラム内部の文字コードとの間で変換を行うことはできますが,技術的な問題からShift_JIS(Microsoftコード・ページ932)のような2バイト文字ではまだ文字コードの変換ができません(参考リンク1参考リンク2)。Shift_JISのように変換不可能な文字コードを使うようにWindows環境が設定されている場合,代わりにLatin-1(ISO 8859-1)がロケールで用いる文字コードとして設定されていると見なされます(参考リンク)。

{-# NOINLINE localeEncoding #-}
localeEncoding :: TextEncoding
localeEncoding = unsafePerformIO $ fmap codePageEncoding getCurrentCodePage

codePageEncoding :: Word32 -> TextEncoding
~ 略 ~
codePageEncoding cp = maybe latin1 buildEncoding (lookup cp codePageMap)

 つまり,Latin-1とHaskellプログラム内部の文字コードとの間での自動変換が行われることになります。しかし,Latin-1は日本語文字列を扱えません。このため,Windowsでは文字コードとしてUTF-8などを明示的に設定する必要があります。

 文字コードは,hSetEncoding関数にutf8などのTextEncoding型の変数を渡すことで設定します。

Prelude System.IO> :t hSetEncoding
hSetEncoding :: Handle -> TextEncoding -> IO ()
Prelude System.IO> :browse
~ 略 ~
latin1 :: TextEncoding
localeEncoding :: TextEncoding
~ 略 ~
utf16 :: TextEncoding
utf16be :: TextEncoding
utf16le :: TextEncoding
utf32 :: TextEncoding
utf32be :: TextEncoding
utf32le :: TextEncoding
utf8 :: TextEncoding
~ 略 ~

 逆に,ハンドルから現在使用しているTextEncodingを取得するhGetEncodingという関数もあります。

Prelude System.IO> :t hGetEncoding
hGetEncoding :: Handle -> IO (Maybe TextEncoding)

 hSetEncodingはHandle型を引数として取るため,使用するハンドルごとに文字コードを設定しなければならない点に注意してください。例えばコマンドラインのプログラムに対して文字コードを設定したければ,標準入力(standard input)であるstdin,標準出力(standard output)であるstdout,標準エラー(standard error)出力であるstderrのすべてに文字コードを設定する必要があります(参考リンク1参考リンク2)。

Prelude System.IO> :t stdin
stdin :: Handle
Prelude System.IO> :t stdout
stdout :: Handle
Prelude System.IO> :t stderr
stderr :: Handle

 hSetEncodingを使うことで,Windowsでも日本語を含むUnicode文字列を入出力で使うことができます。

module Unicode where
import System.IO

main = do
    h <- openFile "test.txt" WriteMode
    hSetEncoding h utf8
    hPutStrLn h "テスト"
    hClose h

test.txt(UTF-8)の内容

テスト

 ただし,ここで設定する文字コードは,コマンドプロンプトの表示に使われる文字コードと同じであるとは限りません。このため文字化けが生じる可能性があります。例えばWindowsでUnicode文字列を入出力するようにした場合,コマンドプロンプトでは以下のような表示になります。

Prelude System.IO> hSetEncoding stdout utf8
Prelude System.IO> hPutStrLn stdout "テスト"
繝・せ繝
Prelude System.IO> putStrLn "テスト"
繝・せ繝

 putStrLnは「hPutStrLn stdout」と同じ意味なので,stdoutに設定した文字コードを自動的に利用します。したがって,一つのI/Oアクションで使用する複数のputStrLnに対し,別々に文字コードを設定する必要はありません。

-- | Write a string to the standard output device
-- (same as 'hPutStr' 'stdout').
putStr          :: String -> IO ()
putStr s        =  hPutStr stdout s
-- | The same as 'putStr', but adds a newline character.
putStrLn        :: String -> IO ()
putStrLn s      =  do putStr s
                      putChar '\n'

 また,putStrLnやhPutStrLnではなくprintを日本語文字列の出力に使った場合,文字列が数値を使った表現にエスケープされてしまう点に注意してください。

Prelude System.IO> print "テスト"
"\12486\12473\12488"

 エスケープの原因は,printの内部で使われているshow関数にあります。Char型のShowクラスに対するインスタンスでは,文字列をどんな環境でも表示可能にするため,UnicodeでDELよりも後にくる文字をエスケープするよう定義されています(参考リンク1参考リンク2参考リンク3)。この結果,print関数で日本語文字列を出力させようとするとエスケープされてしまうのです。

instance  Show Char  where
    showsPrec _ '\'' = showString "'\\''"
    showsPrec _ c    = showChar '\'' . showLitChar c . showChar '\''
    showList cs = showChar '"' . showl cs
                 where showl ""       s = showChar '"' s
                       showl ('"':xs) s = showString "\\\"" (showl xs s)
                       showl (x:xs)   s = showLitChar x (showl xs s)
                -- Making 's' an explicit parameter makes it clear to GHC
                -- that showl has arity 2, which avoids it allocating an extra lambda
                -- The sticking point is the recursive call to (showl xs), which
                -- it can't figure out would be ok with arity 2.

-- | Convert a character to a string using only printable characters,
-- using Haskell source-language escape conventions.  For example:
--
-- > showLitChar '\n' s  =  "\\n" ++ s
--
showLitChar                :: Char -> ShowS
showLitChar c s | c > '\DEL' =  showChar '\\' (protectEsc isDec (shows (ord c)) s)
showLitChar '\DEL'         s =  showString "\\DEL" s
showLitChar '\\'           s =  showString "\\\\" s
showLitChar c s | c >= ' '   =  showChar c s
showLitChar '\a'           s =  showString "\\a" s
showLitChar '\b'           s =  showString "\\b" s
showLitChar '\f'           s =  showString "\\f" s
showLitChar '\n'           s =  showString "\\n" s
showLitChar '\r'           s =  showString "\\r" s
showLitChar '\t'           s =  showString "\\t" s
showLitChar '\v'           s =  showString "\\v" s
showLitChar '\SO'          s =  protectEsc (== 'H') (showString "\\SO") s
showLitChar c              s =  showString ('\\' : asciiTab!!ord c) s
        -- I've done manual eta-expansion here, becuase otherwise it's
        -- impossible to stop (asciiTab!!ord) getting floated out as an MFE

 エスケープされた文字列は,Readクラスのreadメソッドを使うことで元に戻せます。

Prelude> :i Read
class Read a where
  readsPrec :: Int -> ReadS a
  readList :: ReadS [a]
  GHC.Read.readPrec :: Text.ParserCombinators.ReadPrec.ReadPrec a
  GHC.Read.readListPrec ::
    Text.ParserCombinators.ReadPrec.ReadPrec [a]
        -- Defined in GHC.Read
~ 略 ~
instance (Read a) => Read [a] -- Defined in GHC.Read
~ 略 ~
instance Read Char -- Defined in GHC.Read
~ 略 ~

module Unicode where
import System.IO

main = do
    h <- openFile "test.txt" WriteMode
    hSetEncoding h utf8
    hPutStrLn h (read (show "テスト")::String)
    hClose h

test.txt(UTF-8)の内容

テスト


著者紹介 shelarcy

 前回までプログラム効率化の話題を続けて取り上げていましたが,Haskell Platform 2010.1.0.0のリリースに合わせて,プログラム効率化の話題を一時中断し,今回はHaskell Platformを紹介してみました。いかがでしたか?

 記事を書くに当たって,Haskell Platformのリリースまでの動向,およびGHC 6.12ブランチの変更を見守っていました。GHC 6.12ブランチもチェックしていたのは,Haskell Platformのリリースまでに6.12.1のバグを修正した6.12.2がリリースされるのではないかという期待があったためです(参考リンク1参考リンク2参考リンク3)。結局,GHC 6.12.2はリリースされず,Haskell PlatformにはGHC 6.12.1が含まれることになりました。GHC 6.12.2がリリースされ次第,これを含むHaskell Platformのマイナーリリースが出るはずなので,気長に待ちたいと思います。