10527


Haskellで設計を行っている時に、どうしてもDRY原則に反し、煩雑になってしまうコードが有ります。  
特に、型クラスを使っている時にこの問題が発生することがあります。  
実際の状況はかなり複雑で書ききれないので、重要な部分だけを取り出して問題形式にしました。  
どのようにすればこの問題を解決できるでしょうか？  
また、根本的に問題の捉え方が誤っているなどがあればご指摘いただければ幸いです。

[code]

    {-
    以下の条件で実装をするものとする。
    まず、データ型X, Y, Zがあり、データ型は頻繁に追加されることが予想される。
    （つまり、拡張性を高く保っておく必要がある）
    あなたは、標準出力から、一行目にデータ型の種類を指定され、二行目にパース可能なデータが与えられる。
    そのデータをパースし、データに対してある動作B(サンプル実装ではfuncBとした)を行ったあとに、
    所与の動作A(funcA)を実行する必要がある。
    サンプル実装では、test関数にその処理をまとめたが、これは明らかにDRY原則に反している。
    サンプル実装をどのように改善すればDRY原則を守ることができるか。
    （サンプル実装の中身はすべて書き換え可、動作Bはundefinedでよい）
    -}
    module Problem where
    
    ----------------------------------------------------------------------
    -- 条件
    
    data X = X
    data Y = Y
    data Z = Z
    
    class Parsable a where
      parse :: String -> a
    instance Parsable X where
      parse = undefined
    instance Parsable Y where
      parse = undefined
    instance Parsable Z where
      parse = undefined
    
    class ClassA a where
      funcA :: a -> IO ()
    instance ClassA X where
      funcA = undefined
    instance ClassA Y where
      funcA = undefined
    instance ClassA Z where
      funcA = undefined
    
    ----------------------------------------------------------------------
    -- サンプル実装
    
    class ClassB a where
      funcB :: a -> IO ()
    instance ClassB X where
      funcB = undefined
    instance ClassB Y where
      funcB = undefined
    instance ClassB Z where
      funcB = undefined
    
    test = do
      t <- getLine
      d <- getLine
      case t of
       "X" -> do
         let obj = parse d :: X
         funcB obj
         funcA obj
       "Y" -> do
         let obj = parse d :: Y
         funcB obj
         funcA obj
       "Z" -> do
         let obj = parse d :: Z
         funcB obj
         funcA obj
    
[/code]


CC BY-SA 3.0
2015-05-22T17:57:40.943
10525
2015-05-23T11:33:12.463
9847
question
haskell
HaskellにおいてDRY原則に反した型設計の改善


コードに繰り返し現われる処理である、「パースし、`funcB`を呼び、`funcA`を呼ぶ」という部分を切り出して関数にすることができます。

[code]

    common
      :: (Parsable a, ClassA a, ClassB a)
      => a -> String -> IO ()
    common dummy d = do
       let obj = parse d `asTypeOf` dummy
       funcB obj
       funcA obj
    
[/code]

`common`の最初の引数はダミーで、その値は無視されます。これは`parse d`の結果の型を指定するために使っています。

これを使って`test`を書き換えると以下のようになります。

[code]

    test = do
      t <- getLine
      d <- getLine
      case t of
       "X" -> common (undefined :: X) d
       "Y" -> common (undefined :: Y) d
       "Z" -> common (undefined :: Z) d
    
[/code]

これで答えになっているでしょうか。

なお、`Data.Proxy`にある`Proxy`型と、GHC拡張である`ScopedTypeVariables`を使うと、「`common`の第一引数には型を指定する役割しかない」ということを明確にしたコードを書くことができます。

[code]

    common
      :: forall a. (Parsable a, ClassA a, ClassB a)
      => Proxy a -> String -> IO ()
    common _ d = do
       let obj = parse d :: a
       funcB obj
       funcA obj
    
[/code]

この場合、`common`は`common (Proxy :: Proxy X) d`のように使うことになります。

# 追記: 分岐処理自体も抽出したい場合

条件分岐の部分を別関数として抽出したい場合は以下のようにすることができます。

[code]

    switch
      :: String
      -> (forall a. (Parsable a, ClassA a, ClassB a) => Proxy a -> r)
      -> r
    switch t f = case t of
      "X" -> f (Proxy :: Proxy X)
      "Y" -> f (Proxy :: Proxy Y)
      "Z" -> f (Proxy :: Proxy Z)
    
[/code]

多相的な関数を引数として取るために、`RankNTypes`拡張を使っています。

これを使うと`test`は次のように書き換えられます。

[code]

    test = do
      t <- getLine
      d <- getLine
      switch t $ \proxy -> common proxy d
    
[/code]


CC BY-SA 3.0
2015-05-23T02:25:54.420
10527
2015-05-23T11:33:12.463
2015-05-23T11:33:12.463
8654
8654
10525
answer