8

Haskell である種のメッセージ パーサーを実装しようとしているので、コンストラクターではなく、メッセージの型に型を使用することにしました。

data DebugMsg  = DebugMsg String
data UpdateMsg = UpdateMsg [String]

.. 等々。Msgこのメッセージに関連するすべての情報/パーサー/アクションを含むメッセージに対して、たとえば型クラスを定義できるため、それは私にとってより便利だと思います。しかし、ここで問題があります。を使用して解析関数を記述しようとするとcase:

parseMsg :: (Msg a) => Int -> Get a
parseMsg code = 
    case code of
        1 -> (parse :: Get DebugMsg)
        2 -> (parse :: Get UpdateMsg)

..ケースの結果のタイプは、すべてのブランチで同じでなければなりません。解決策はありますか?そして、関数の結果に型クラスのみを指定して、それが完全にポリモーフィックであることを期待することさえ可能ですか?

4

2 に答える 2

9

実存型でこのようなことを達成することはできますが、それはあなたが望むようには機能しないので、実際にはそうすべきではありません。

あなたの例のように、通常のポリモーフィズムでそれを行うことは、まったく機能しません。あなたのタイプが言うことは、関数がすべて aに対して有効であるということです-つまり、呼び出し元は受信するメッセージの種類を選択することができます。ただし、数値コードに基づいてメッセージを選択する必要があるため、これは明らかに機能しません。

明確にするために:すべての標準Haskell型変数は、デフォルトで全称記号化されています。型署名はとして読み取ることができます∀a. Msg a => Int -> Get a。これが言うことは、引数が何であるかに関係なく、関数はのすべての値に対して定義されているということです。これは、どの引数を取得するかに関係なく、呼び出し元が望むa特定のものを返すことができなければならないことを意味します。a

あなたが本当に欲しいのはのようなものです∃a. Msg a => Int -> Get a。これが、実存型でそれができると私が言った理由です。ただし、これはHaskellでは比較的複雑であり(そのような型署名を完全に記述することはできません)、実際には問題を正しく解決できません。それは将来のために心に留めておくべきことです。

基本的に、このようなクラスとタイプを使用することは、Haskellではあまり慣用的ではありません。なぜなら、それはクラスが行うことを意図していることではないからです。メッセージには通常の代数的データ型を使用する方がはるかに良いでしょう。

私はこのような単一のタイプを持っているでしょう:

data Message = DebugMsg String
             | UpdateMsg [String]

parseしたがって、タイプごとに関数を使用する代わりに、parseMsg必要に応じて関数で解析を実行してください。

parseMsg :: Int -> String -> Message
parseMsg n msg = case n of
  1 -> DebugMsg msg
  2 -> UpdateMsg [msg]

(明らかに、実際にそこにあるロジックを入力してください。)

基本的に、これは通常の代数的データ型の古典的な使用法です。異なる種類のメッセージに対して異なるタイプを使用する理由はありません。同じタイプのメッセージを使用すると、作業がはるかに簡単になります。

他の言語のサブタイピングをエミュレートしようとしているようです。経験則として、他の言語でのサブタイプのほとんどの使用の代わりに、代数的データ型を使用します。これは確かにそのようなケースの1つです。

于 2013-01-31T03:17:47.507 に答える
9

はい、すべてのサブケースの右辺はすべて、まったく同じ型でなければなりません。この型は、式全体の型と同じでなければなりませんcase。これは機能です。実行時に型エラーが発生しないことをコンパイル時に言語が保証できるようにする必要があります。

あなたの質問に対するコメントのいくつかは、最も簡単な解決策は合計(別名バリアント)タイプを使用することであると述べています。

data ParserMsg = DebugMsg String | UpdateMsg [String]

この結果、一連の代替結果が事前に定義されます。これは、利点 (未処理のサブケースがないことをコードで確認できる) の場合もあれば、欠点 (サブケースの数には限りがあり、コンパイル時に決定される) の場合もあります。

場合によっては、より高度な解決策 (必要ないかもしれませんが、簡単に説明します) は、関数を data として使用するようにコードをリファクタリングすることです。アイデアは、フィールドとして関数 (またはモナド アクション) を持つデータ型を作成し、次に異なる動作 = 異なる関数をレコード フィールドとして作成するというものです。

この 2 つのスタイルをこの例と比較してください。まず、さまざまなケースを合計として指定します (これは GADT を使用しますが、理解するのに十分単純なはずです):

{-# LANGUAGE GADTs #-}

import Data.Vector (Vector, (!))
import qualified Data.Vector as V

type Size = Int    
type Index = Int

-- | A 'Frame' translates between a set of values and consecutive array 
-- indexes.  (Note: this simplified implementation doesn't handle duplicate
-- values.)
data Frame p where 
    -- | A 'SimpleFrame' is backed by just a 'Vector'
    SimpleFrame  :: Vector p -> Frame p
    -- | A 'ProductFrame' is a pair of 'Frame's.
    ProductFrame :: Frame p -> Frame q -> Frame (p, q)

getSize :: Frame p -> Size
getSize (SimpleFrame v) = V.length v
getSize (ProductFrame f g) = getSize f * getSize g

getIndex :: Frame p -> Index -> p
getIndex (SimpleFrame v) i = v!i
getIndex (ProductFrame f g) ij = 
    let (i, j) = splitIndex (getSize f, getSize g) ij
    in (getIndex f i, getIndex g j)

pointIndex :: Eq p => Frame p -> p -> Maybe Index
pointIndex (SimpleFrame v) p = V.elemIndex v p
pointIndex (ProductFrame f g) (p, q) = 
    joinIndexes (getSize f, getSize g) (pointIndex f p) (pointIndex g q)

joinIndexes :: (Size, Size) -> Index -> Index -> Index
joinIndexes (_, rsize) i j = i * rsize + j

splitIndex :: (Size, Size) -> Index -> (Index, Index)
splitIndex (_, rsize) ij = (ij `div` rsize, ij `mod` rsize)

この最初の例では、 aは aまたは a のFrameいずれかのみであり、すべての関数は両方のケースを処理するように定義する必要があります。SimpleFrameProductFrameFrame

次に、関数メンバーを持つデータ型 (両方の例に共通するコードを省略します):

data Frame p = Frame { getSize    :: Size
                     , getIndex   :: Index -> p
                     , pointIndex :: p -> Maybe Index }

simpleFrame :: Eq p => Vector p -> Frame p
simpleFrame v = Frame (V.length v) (v!) (V.elemIndex v)

productFrame :: Frame p -> Frame q -> Frame (p, q)
productFrame f g = Frame newSize getI pointI
    where newSize = getSize f * getSize g
          getI ij = let (i, j) = splitIndex (getSize f, getSize g) ij 
                    in (getIndex f i, getIndex g j)
          pointI (p, q) = joinIndexes (getSize f, getSize g) 
                                      (pointIndex f p) 
                                      (pointIndex g q)

ここで、Frame型はgetIndexおよびpointIndex操作を 自身のデータ メンバーとして受け取りFrameます。Framea の動作は、実行時に提供されるその要素関数によって決定されるため、サブケースのコンパイル時の固定セットはありません。したがって、これらの定義に触れることなく、次を追加できます。

import Control.Applicative ((<|>))

concatFrame :: Frame p -> Frame p -> Frame p
concatFrame f g = Frame newSize getI pointI
    where newSize = getSize f + getSize g
          getI ij | ij < getSize f = ij
                  | otherwise      = ij - getSize f
          pointI p = getPoint f p <|> fmap (+(getSize f)) (getPoint g p)

私はこの 2 番目のスタイルを「行動型」と呼んでいますが、それはまさに私です。

GHC の型クラスはこれと同様に実装されていることに注意してください — 隠された "辞書" 引数が渡されます. この辞書はクラス メソッドの実装をメンバーとするレコードです:

data ShowDictionary a { primitiveShow :: a -> String }

stringShowDictionary :: ShowDictionary String
stringShowDictionary = ShowDictionary { primitiveShow = ... }

-- show "whatever"
-- ---> primitiveShow stringShowDictionary "whatever"
于 2013-01-31T06:00:50.313 に答える