テキスト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のマイナーリリースが出るはずなので,気長に待ちたいと思います。 |