Enumでシーザー暗号、もしくは既知の型のインスタンス一部変更

Enumクラスのメソッドsucc, pred使うとシーザー暗号簡単に書けそうですよね。
succはsuccessorの略で、次に続く値を返してくれます。
数値の場合はインクリメントした値を返します。
predはpredesessorですね。数値ではデクリメントした値を返します。

caesarCipher' = map succ

main = putStrLn $ caesarCipher' "abcde" -- => bcdef

しかしsucc 'z'しても'a'にはなりません。

main = putStrLn $ caesarCipher' "zzzz" -- => {{{{{

では(succ 'z' == 'a')となってくれるようなCircularCharを定義すれば良さそうですね。 そうすればsuccで簡単にシーザー暗号書けるはずです。

newtype CircularChar = CC Char
    deriving (Show) 

instance Enum CircularChar where
    succ (CC 'z') = CC 'a'
    succ (CC 'Z') = CC 'A'
    succ (CC c) = CC . succ $ c
    pred (CC 'a') = CC 'z'
    pred (CC 'A') = CC 'Z'
    pred (CC c) = CC . pred $ c
    toEnum n = CC . toEnum $ n
    fromEnum (CC c) = fromEnum c


Enumクラスのインスタンスにする時の最小限の定義はtoEnum, fromEnumの二つですが、ここではsucc, predをオーバーライドしています。
また、ここではsuccで循環するように、
a->b->c->...->z->a
A->B->C->...->Z->A
と定義しています。まあ本当のシーザー暗号どんなもんか知りませんけど。


ちょっと関係ないですが、Enumクラスのインスタンスにすると、[1..3]といった記述が出来るようになります。

ghci> [1..3] -- => [1,2,3] 
ghci> [CC 'a'..CC 'c'] -- => [CC 'a',CC 'b',CC 'c']

まあ見た目そんなにいいわけではないですし、enumFromTo使った方が良いかもしれないですが。
でもまあ、ある型クラスのインスタンスにすることで組み込みのシンタックスが使えるようになるというのはなかなか面白いものですね。


CircularCharが定義出来たので、シーザー暗号を定義します。

caesarCipher :: Int -> [CircularChar] -> [CircularChar] 
caesarCipher 0 cs = cs
caesarCipher n cs | n > 0 = caesarCipher (n-1) (map succ cs)
                  | n < 0 = caesarCipher (n+1) (map pred cs)
main = print $ caesarCipher 3 (map CC "abcdwxyz")
    -- => [CC 'd',CC 'e',CC 'f',CC 'g',CC 'z',CC 'a',CC 'b',CC 'c']

できました。n回succ(pred)するだけの単純な定義で済みました。

ちょっと改良

表示の際に"CC"とかいらないので、Showのインスタンス定義を変更します。

instance Show CircularChar where
	show (CC c) = [c] -- cはCircularの定義より、Char

caesarCipherは[CircularChar]を引数にとっているため、(map CC "abcde")として文字列を変換しているのですが、出来れば文字列リテラル"abcde"をそのまま渡したいですね。
文字列リテラルはデフォルトでは[Char]なので、GHC拡張OverloadedStringsで(IsString a) => aにしておくと便利です。

{-# LANGUAGE OverloadedStrings #-}

これはHaskellで数値リテラル、たとえば1234が(Num a) => aであることの文字列版に相当します。

ghci> :type 1234
1234 :: Num a => a
ghci> :set -XOverloadedStrings
ghci> :type "abcde"
"abcde" :: IsString a => a

FlexibleInstancesは[CircularChar]をインスタンス化するために必要です。

{-# LANGUAGE FlexibleInstances #-}
import GHC.Exts (IsString (..)) 

instance IsString [CircularChar] where
    fromString = map CC

これで

"abcde" :: [CircularChar] 

と書くことも可能になりました。よってcaesarCipherの引数に文字列リテラルを直接書くことが出来ます。

main = print $ caesarCipher 3 "abcdwxyz" 
    -- => [d,e,f,g,z,b,a,c] 

既知の型の一部変更

まあここからが本題だったりするのですが。
CircularCharは「Charの一部の機能(Enum)を変更したもの」なのですが、
これをCharと同じように扱いたいときはどうするのがいいんでしょうね。
そういう「クラスの一部の機能だけ変更」というのは、Nominal subtypingを採用しているOOPLでは継承して変更部をオーバーライドすればいいのですが、型クラスで同じことしようとすると少し手間になります。

Charと同じ型クラスのインスタンスを定義していく(Bounded, Eq, Ord, Read, Show. これくらいなら単にderivingすれば済みますが、一般的にはそうではありません。)か、CircularCharからCharへ変換する関数を一個定義するか...
前者は定義が大変ですが、Charと同じように定義すればCharとあまり変わらず使えると。多分。そうでもないか。
後者の方が定義には簡単ですが、使う度に関数挟まなければならないですね。
強い型としては変換関数一つ定義するのがやはり定石でしょうか。


まあ本当に同じように扱いたいなら、Num, IsStringのような型クラスを作るのがいいかもしれないですね。


今回のソースまとめです。

{-# LANGUAGE OverloadedStrings, FlexibleInstances #-}
import GHC.Exts (IsString (..)) 

newtype CircularChar = CC Char

instance Show CircularChar where
	show (CC c) = [c] -- cはCircularの定義より、Char

instance IsString [CircularChar] where
    fromString = map CC

instance Enum CircularChar where
    succ (CC 'z') = CC 'a'
    succ (CC 'Z') = CC 'A'
    succ (CC c) = CC . succ $ c
    pred (CC 'a') = CC 'z'
    pred (CC 'A') = CC 'Z'
    pred (CC c) = CC . pred $ c
    toEnum n = CC . toEnum $ n
    fromEnum (CC c) = fromEnum c

caesarCipher :: Int -> [CircularChar] -> [CircularChar] 
caesarCipher 0 cs = cs
caesarCipher n cs | n > 0 = caesarCipher (n-1) (map succ cs)
                  | n < 0 = caesarCipher (n+1) (map pred cs)
main = print $ caesarCipher 3 "abcdwxyz" 
    -- => [d,e,f,g,z,b,a,c]