2015-02-08 79 views
4

我试图在Haskell中编写一个数据处理模块,它接受与不同模式有关的changesets,并通过一系列可选择地根据数据执行操作的规则传递这些模块。 (这主要是一个学术活动,以更好地了解了Haskell)模式与Haskell函数中的类型实例匹配

为了更好地解释我在做什么,这里是斯卡拉

// 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] 

工作的例子。重要的是,这两个“纲要”和'规则'是可以扩展的,这部分特别是在我尝试在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类型类来代替,但哈斯克尔仍然坚持对功能锁定到一个单一的类型。数据种类似乎具有相同的限制。

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

  1. 是否有可能创建一个功能,可以模式匹配打开类型,就像我们可以在斯卡拉?

  2. 在Haskell中有没有更优雅的惯用方式处理上述场景?

+0

你的意思是'rules :: [Rule]',而不是'='? – 2015-02-08 09:54:45

+0

没有。我的意思是'规则= [规则1,规则2,规则3]'。我看到我的错字 - 将会修改。 – 2015-02-08 11:12:53

回答

3

你可以在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]这是我正在固执己见。我不喜欢这里的“开放类型”的Scala方法,很大程度上是因为类型不是头等舱。这只是试图扭转他们变得更加一流。

+0

谢谢。我认为你的断言是正确的 - 根据现有的面向对象解决方案,问题的框架太多了。我渴望更自然的方式,所以我可能会回到潜在的问题,看看是否有更适合的东西弹出。 – 2015-02-10 10:13:00