2015-02-08 126 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