首页 文章

在Haskell函数中对类型类实例进行模式匹配

提问于
浏览
4

我正在尝试在Haskell中编写一个数据处理模块,该模块接受与不同模式相关的 changesets ,并通过一系列规则传递这些模块,这些规则可选择根据数据执行操作 . (这主要是为了更好地理解Haskell而进行的学术练习)

为了更好地解释我正在做什么,这里是Scala中的一个工作示例

// We have an open type allowing us to define arbitrary 'Schemas' 
// in other packages.
trait Schema[T]

// Represents a changeset in response to user action - i.e. inserting some records into a database.
sealed trait Changeset[T]
case class Insert[T]( schema:Schema[T], records:Seq[T]) extends Changeset[T]
case class Update[T]( schema:Schema[T], records:Seq[T]) extends Changeset[T]
case class Delete[T]( schema:Schema[T], records:Seq[T]) extends Changeset[T]


// Define a 'contacts' module containing a custom schema. 
package contacts {
    object Contacts extends Schema[Contact]
    case class Contact( firstName:String, lastName:String )   
}

// And an 'accounts' module
package accounts {
    object Accounts extends Schema[Account]
    case class Account( name:String )
}


// We now define an arbitrary number of rules that each
// changeset will be checked against
trait Rule {
    def process( changeset: Changeset[_] ):Unit
}

// As a contrived example, this rule keeps track of the 
// number of contacts on an account
object UpdateContactCount extends Rule {
    // To keep it simple let's pretend we're doing IO directly here
    def process( changeset: Changeset[_] ):Unit = changeset match {

        // Type inference correctly infers the type of `xs` here.
        case Insert( Contacts, xs ) => ??? // Increment the count
        case Delete( Contacts, xs ) => ??? // Decrement the count
        case Insert( Accounts, xs ) => ??? // Initialize to zero
        case _ => () // Don't worry about other cases 
    }
}

val rules = [UpdateContactCount, AnotherRule, SomethingElse]

重要的是,'Schema'和'Rule'都可以进行扩展,而这部分特别是在我尝试在Haskell中执行此操作时会引发一些曲线球 .

到目前为止我在Haskell中所拥有的是

{-# LANGUAGE GADTs #-}

-- In this example, Schema is not open for extension.
-- I'd like it to be    
data Schema t where
    Accounts :: Schema Account
    Contacts :: Schema Contact

data Account = Account { name :: String } deriving Show
data Contact = Contact { firstName :: String, lastName :: String } deriving Show

data Changeset t = Insert (Schema t) [t]                             
                 | Update (Schema t) [t]
                 | Delete (Schema t) [t]



-- Whenever a contact is inserted or deleted, update the counter
-- on the account. (Or, for new accounts, set to zero)
-- For simplicity let's pretend we're doing IO directly here.
updateContactCount :: Changeset t -> IO ()
updateContactCount (Insert Contacts contacts) = ???
updateContactCount (Delete Contacts contacts) = ???
updateContactCount (Insert Accounts accounts) = ???
updateContactCount other = return ()

这个例子运行正常 - 但是我想扩展这个,这样两个 Schema 都可以是一个开放类型(即我不知道 updateContactCount 函数的时间头,我只是传递一个类型为 [Rule] 的列表 . 即ie就像是 .

type Rule = Changeset -> IO ()
rules = [rule1, rule2, rule3]

我的第一次尝试是通过创建一个 Schema 类型类,但是Haskell仍然坚持将函数锁定为单一类型 . 数据种类似乎具有相同的限制 .

由此,我确实有两个具体问题 .

  • 是否可以创建一个可以与开放类型进行模式匹配的函数,就像在Scala中一样?

  • 在Haskell中处理上述场景是否有更优雅的惯用方法?

1 回答

  • 3

    您可以使用 Data.Typeable 在Haskell中执行相同的操作 . 这不是特别自然的Haskell代码,暗示你可能在伪装[1]中有一个非常深的XY Problem,但它是你的Scala代码的密切翻译 .

    {-# LANGUAGE DeriveDataTypeable #-}
    {-# LANGUAGE ExistentialQuantification #-}
    {-# LANGUAGE ScopedTypeVariables #-}
    
    import Data.Typeable (Typeable, gcast)
    import Control.Applicative ((<|>), empty, Alternative)
    import Data.Maybe (fromMaybe)
    
    -- The Schema typeclass doesn't require any functionality above and
    -- beyond Typeable, but we probably want users to be required to
    -- implement for explicitness.
    class Typeable a => Schema a where
    
    -- A changeset contains an existentially quantified list, i.e. a [t]
    -- for some t in the Schema typeclass
    data Changeset = forall t. Schema t => Insert [t]
                   | forall t. Schema t => Update [t]
                   | forall t. Schema t => Delete [t]
    
    data Contact = Contact  { firstName :: String
                            , lastName  :: String }
                   deriving Typeable
    instance Schema Contact where
    
    data Account = Account { name :: String }
                   deriving Typeable
    instance Schema Account where
    
    -- We somehow have to let the type inferer know the type of the match,
    -- either with an explicit type signature (which here requires
    -- ScopedTypeVariables) or by using the value of the match in a way
    -- which fixes the type.
    --
    -- You can fill your desired body here.
    updateContactCount :: Changeset -> IO ()
    updateContactCount c = choiceIO $ case c of
      Insert xs -> [ match xs (\(_ :: [Contact]) ->
                                    putStrLn "It was an insert contacts")
                   , match xs (\(_ :: [Account]) ->
                                    putStrLn "It was an insert accounts") ]
      Delete xs -> [ match xs (\(_ :: [Contact]) ->
                                    putStrLn "It was a delete contacts") ]
      _         -> []
    
    main :: IO ()
    main = mapM_ updateContactCount [ Insert [Contact "Foo" "Bar"]
                                    , Insert [Account "Baz"]
                                    , Delete [Contact "Quux" "Norf"]
                                    , Delete [Account "This one ignored"]
                                    ]
    

    它需要这些辅助组合器 .

    choice :: Alternative f => [f a] -> f a
    choice = foldr (<|>) empty
    
    maybeIO :: Maybe (IO ()) -> IO ()
    maybeIO = fromMaybe (return ()) 
    
    choiceIO :: [Maybe (IO ())] -> IO ()
    choiceIO = maybeIO . choice
    
    match :: (Typeable a1, Typeable a) => [a1] -> ([a] -> b) -> Maybe b
    match xs = flip fmap (gcast xs)
    

    结果是

    ghci> main
    It was an insert contacts
    It was an insert accounts
    It was a delete contacts
    

    [1]这是我的观点 . 我不喜欢这里的“开放类型”的Scala方法,主要是因为类型不是一流的 . 这只是试图扭曲他们变得更加一流 .

相关问题