USC4 ←→UTF8 変換を調べてみました。

Haskell の内部ではUSC4を使用しています。
UTF8 に変換するには以下のテーブルにより行います。

<http://www.ietf.org/rfc/rfc2279.txt>

 UCS-4 range (hex.)    UTF-8 octet sequence (binary)
 0000 0000-0000 007F   0xxxxxxx
 0000 0080-0000 07FF   110xxxxx 10xxxxxx
 0000 0800-0000 FFFF   1110xxxx 10xxxxxx 10xxxxxx

 0001 0000-001F FFFF   11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
 0020 0000-03FF FFFF   111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
 0400 0000-7FFF FFFF   1111110x 10xxxxxx ... 10xxxxxx

xxx は、UCS を2進数にした一部。
0000 0080 以上 0000 07FF 以下の場合の110xxxxx 10xxxxxx は 110B + (上位 5BIt) と 10B + (下位 6BIt)の2文字と言う意味。

import Data.Word        (Word8,Word32)
import Data.Bits        ((.|.),(.&.),shiftL,shiftR)
import Data.Char        (chr,ord)
import Numeric
import Debug.Trace


main = do
       print (map (\x->showHex (ord x) "") "あいう0123")
       -- UCS 4
       --  'あ'   'い'   'う'    '0'  '1' '2'   '3'
       -- ["3042","3044","3046","30","31","32","33"]

       print (map (\x->showHex x "") $ encode' "あいう0123") 
       -- UTF8
       -- <---  'あ' ---><---  'い' ---><---  'う' ---> '0'  '1'  '2'   '3'
       -- ["e3","81","82","e3","81","84","e3","81","86","30","31","32","33"]

       (putStrLn.decode'.encode') "あ"
       -- decode'を実行する様子
       -- 最初のバイトは 1110xxxx なので 0xF をマスク
       -- c:e3 -->(c .&. f):3

       -- acc:3  r:81 --> c1=(shiftL acc 6:c0) .|. (r .&. 0x3f):1)
       -- 3を左へ6ビットシフトしてc0    
       -- 次のバイトは10xxxxxx なので 0x3f をマスク --> 1
       -- c0 .|. 1 --> c1 (c0 と 1をORしてc1)

       -- acc:c1 r:82 --> 3042=(shiftL acc 6:3040) .|. (r .&. 0x3f):2)
       -- c1を左へ6ビットシフトして3040
       -- 次のバイトは10xxxxxx なので 0x3f をマスク --> 2
       -- 3040 .|. 2 --> 3042
       -- 3042

       -- => あ

-- | Encode a Haskell String to a list of Word8 values, in UTF8 format.
encode' :: String -> [Word8]
encode' = concatMap (map fromIntegral . go . ord)
 where
  go oc
   | oc <= 0x7f       = [oc]                            -- 0x7f  以下(ASCII)は同じ
                                                        -- ラテン補助、拡張、・・・ギリシャ文字、アラビア文字
   | oc <= 0x7ff      = [ 0xc0 + (oc `shiftR` 6)        -- 右へ6ビットシフトした残りと 110B の OR 
                        , 0x80 + oc .&. 0x3f            -- 甲斐6ビットをマスクし 10B と OR
                        ]

   -- ひらがな、漢字、ハングルなど。Windows IME 文字パッドはここまで
   | oc <= 0xffff     = [ 0xe0 + (oc `shiftR` 12)              -- 右へ6ビットシフトして1110B と OR
                        , 0x80 + ((oc `shiftR` 6) .&. 0x3f)    -- 右へ6ビットシフトして下位6ビットをマスク
                                                               --  10B と OR
                        , 0x80 + oc .&. 0x3f                   -- 下位6ビットをマスクし 10B と OR
                        ]
   -- それ以外は4バイト表現。5バイト、6バイト表現の文字には対応していない。
   | otherwise        = [ 0xf0 + (oc `shiftR` 18)
                        , 0x80 + ((oc `shiftR` 12) .&. 0x3f)
                        , 0x80 + ((oc `shiftR` 6) .&. 0x3f)
                        , 0x80 + oc .&. 0x3f
                        ]

--
-- | Decode a UTF8 string packed into a list of Word8 values, directly to String
--
decode' :: [Word8] -> String
decode' [    ] = ""
decode' (c:cs)
  | c < 0x80  = chr (fromEnum c) : decode' cs      -- 0x7f  以下(ASCII)は同じ
  | c < 0xc0  = replacement_character : decode' cs -- 0x80-0xbf はエラー
  | c < 0xe0  = multi1
  | c < 0xf0  = multi_byte 2 0xf  0x800            -- ひらがな、漢字、ハングルなど。
                                                   -- Windows IME 文字パッドはここまで
  | c < 0xf8  = multi_byte 3 0x7  0x10000          
  | c < 0xfc  = multi_byte 4 0x3  0x200000
  | c < 0xfe  = multi_byte 5 0x1  0x4000000
  | otherwise = replacement_character : decode' cs -- エラー
  where
    multi1 = case cs of
      c1 : ds | c1 .&. 0xc0 == 0x80 ->
        let d = ((fromEnum c .&. 0x1f) `shiftL` 6) .|.  fromEnum (c1 .&. 0x3f)
        in if d >= 0x000080 then toEnum d : decode' ds
                            else replacement_character : decode' ds
      _ -> replacement_character : decode' cs

    -- multi_byte 2 0xf  0x800 -- (ひらがな、漢字のとき)
    multi_byte :: Int -> Word8 -> Int -> [Char]
    multi_byte i mask overlong = trace("c:" ++ showHex c "" ++ " -->(c .&. "++ showHex mask ""++"):" ++ 
                                       showHex (c .&. mask) "")
                                      (aux i cs (fromEnum (c .&. mask)))
      where
        aux 0 rs acc -- ロジックはエラー検出のためのもの。次の文字へ
          | overlong <= acc && acc <= 0x10ffff &&
            (acc < 0xd800 || 0xdfff < acc)     &&
            (acc < 0xfffe || 0xffff < acc)      = trace(showHex acc "")(chr acc : decode' rs)
          | otherwise = replacement_character : decode' rs -- エラー

        aux n (r:rs) acc       -- 10xxxxxx の部分
          | r .&. 0xc0 == 0x80 = trace("acc:" ++ showHex acc "" ++
                                       " r:"  ++ showHex r "" ++
                                       " -->"  ++ showHex (shiftL acc 6 .|. fromEnum (r .&. 0x3f)) "" ++
                                       "=(shiftL acc 6:" ++ showHex (shiftL acc 6) "" ++
                                       ") .|. (r .&. 0x3f):" ++ showHex (r .&. 0x3f) "" ++ ")"
                                       )
                                      (aux (n-1) rs
                                       (shiftL acc 6 .|. fromEnum (r .&. 0x3f)))

        aux _ rs     _ = replacement_character : decode' rs  -- エラー

-- エラー時に置き換える文字
replacement_character :: Char
replacement_character = '\xfffd'