Haskell 函数中针对类型类实例的模式匹配
Pattern matching against typeclass instances in Haskell function
我正在尝试在 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]
类型的列表。即类似的东西。
type Rule = Changeset -> IO ()
rules = [rule1, rule2, rule3]
我的第一次尝试是创建一个 Schema
类型类,但是 Haskell 仍然坚持将函数锁定为单一类型。数据种类似乎具有相同的限制。
由此,我确实有两个具体问题。
是否可以像在 Scala 中那样创建一个可以针对开放类型进行模式匹配的函数?
在Haskell中是否有更优雅的惯用方式来处理上述情况?
您可以在 Haskell 和 Data.Typeable
中做同样的事情。这不是特别自然的 Haskell 代码,表明您可能 XY Problem 伪装得非常深 [1],但它是您的 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] 这是我自以为是。我不喜欢这里 "open types" 的 Scala 方法,主要是因为类型不是第一个 class。这只是试图让他们变得更优先class。
我正在尝试在 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]
类型的列表。即类似的东西。
type Rule = Changeset -> IO ()
rules = [rule1, rule2, rule3]
我的第一次尝试是创建一个 Schema
类型类,但是 Haskell 仍然坚持将函数锁定为单一类型。数据种类似乎具有相同的限制。
由此,我确实有两个具体问题。
是否可以像在 Scala 中那样创建一个可以针对开放类型进行模式匹配的函数?
在Haskell中是否有更优雅的惯用方式来处理上述情况?
您可以在 Haskell 和 Data.Typeable
中做同样的事情。这不是特别自然的 Haskell 代码,表明您可能 XY Problem 伪装得非常深 [1],但它是您的 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] 这是我自以为是。我不喜欢这里 "open types" 的 Scala 方法,主要是因为类型不是第一个 class。这只是试图让他们变得更优先class。