遍历多态结构并仅在少数情况下执行转换
Traversing polymorphic structures and performing a transformation only in few cases
假设我们用以下方式表示公司层次结构:
{-# LANGUAGE DeriveDataTypeable #-}
import Data.Data
import Data.Generics.Aliases
import Data.Generics.Schemes
data CompanyAsset = Employee Name Salary
| Plant Name
| Boss Name Performance Salary [CompanyAsset]
| Pet Name
| Car Id
| Guild [CompanyAsset]
| Fork CompanyAsset CompanyAsset
-- ... and imagine 100 more options that recursively use `CompanyAsset`.
deriving (Show, Data)
-- Performance of the department.
data Performance = Good | Bad deriving (Show, Data)
type Name = String
type Id = Int
newtype Salary = Salary Double deriving (Show, Data, Typeable)
raise :: Salary -> Salary
而且我想定义一个函数来提高公司资产的薪水,这些公司资产没有 Boss
祖先,其部门有 Bad
绩效。这样的函数可以很容易地定义如下:
raiseSalaries :: CompanyAsset -> CompanyAsset
raiseSalaries (Boss n Good s as) = Boss n Good (raise s) (raiseSalaries <$> as)
raiseSalaries a@(Boss _ Bad _ _) = a -- The salaries of everything below are not raised if the performance is 'Bad'
raiseSalaries ... -- and from here onwards we have **boilerplate**!
问题是这需要很多样板文件(为了便于讨论,请假设 CompanyAsset
已给出且无法更改)。
所以我的问题是是否有一种遍历数据结构的方法可以避免上面的样板文件。
这个问题与 similar one I posted, but in this case the use of everywhere'
相关,因为在某些情况下不应提高工资。
无济于事。
这可以通过镜头 Traversal
for CompanyAsset
. You can write it yourself, or use uniplate
or plate
来实现。
为了便于说明,我将明确地为 CompanyAsset
编写一个遍历。它对公司资产的每个直接后代应用一个操作(我将其称为 p
,在 pure
中)。请注意 traverse_ca pure == pure
.
traverse_ca :: Applicative f => (CompanyAsset -> f CompanyAsset) -> CompanyAsset -> f CompanyAsset
traverse_ca p ca =
case ca of
Fork ca1 ca2 -> Fork <$> p ca1 <*> p ca2
Boss n perf s cas -> Boss n perf s <$> traverse p cas
Guild cas -> Guild <$> traverse p cas
otherwise -> pure ca
这本身就足以定义 raiseSalaries
,无需任何额外的样板文件。
import Data.Functor.Identity
raiseSalaries :: CompanyAsset -> CompanyAsset
raiseSalaries (Boss n Good s as) = Boss n Good (raise s) (raiseSalaries <$> as)
raiseSalaries a@(Boss _ Bad _ _) = a -- The salaries of everything below are not raised if the performance is 'Bad'
raiseSalaries a = runIdentity $ traverse_ca (pure . raiseSalaries) a
使用 recursion-schemes, and also a bit of Template Haskell 生成基础 CompanyAssetF
仿函数的解决方案:
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveTraversable #-}
import Data.Functor.Foldable (cata,embed)
import Data.Functor.Foldable.TH (makeBaseFunctor)
$(makeBaseFunctor ''CompanyAsset)
raiseSalaries :: CompanyAsset -> CompanyAsset
raiseSalaries asset = cata go asset raise'
where
go c raiser = embed $
case c of
BossF _ Bad _ _ -> fmap ($ id) c
_ -> raiser $ fmap ($ raiser) c
raise' (BossF name perf salary rec) = BossF name perf (raise salary) rec
raise' (EmployeeF name salary) = EmployeeF name (raise salary)
raise' other = other
代数returns一个函数,目的是让"should get raise"信息从根流向叶
假设我们用以下方式表示公司层次结构:
{-# LANGUAGE DeriveDataTypeable #-}
import Data.Data
import Data.Generics.Aliases
import Data.Generics.Schemes
data CompanyAsset = Employee Name Salary
| Plant Name
| Boss Name Performance Salary [CompanyAsset]
| Pet Name
| Car Id
| Guild [CompanyAsset]
| Fork CompanyAsset CompanyAsset
-- ... and imagine 100 more options that recursively use `CompanyAsset`.
deriving (Show, Data)
-- Performance of the department.
data Performance = Good | Bad deriving (Show, Data)
type Name = String
type Id = Int
newtype Salary = Salary Double deriving (Show, Data, Typeable)
raise :: Salary -> Salary
而且我想定义一个函数来提高公司资产的薪水,这些公司资产没有 Boss
祖先,其部门有 Bad
绩效。这样的函数可以很容易地定义如下:
raiseSalaries :: CompanyAsset -> CompanyAsset
raiseSalaries (Boss n Good s as) = Boss n Good (raise s) (raiseSalaries <$> as)
raiseSalaries a@(Boss _ Bad _ _) = a -- The salaries of everything below are not raised if the performance is 'Bad'
raiseSalaries ... -- and from here onwards we have **boilerplate**!
问题是这需要很多样板文件(为了便于讨论,请假设 CompanyAsset
已给出且无法更改)。
所以我的问题是是否有一种遍历数据结构的方法可以避免上面的样板文件。
这个问题与 similar one I posted, but in this case the use of everywhere'
相关,因为在某些情况下不应提高工资。
这可以通过镜头 Traversal
for CompanyAsset
. You can write it yourself, or use uniplate
or plate
来实现。
为了便于说明,我将明确地为 CompanyAsset
编写一个遍历。它对公司资产的每个直接后代应用一个操作(我将其称为 p
,在 pure
中)。请注意 traverse_ca pure == pure
.
traverse_ca :: Applicative f => (CompanyAsset -> f CompanyAsset) -> CompanyAsset -> f CompanyAsset
traverse_ca p ca =
case ca of
Fork ca1 ca2 -> Fork <$> p ca1 <*> p ca2
Boss n perf s cas -> Boss n perf s <$> traverse p cas
Guild cas -> Guild <$> traverse p cas
otherwise -> pure ca
这本身就足以定义 raiseSalaries
,无需任何额外的样板文件。
import Data.Functor.Identity
raiseSalaries :: CompanyAsset -> CompanyAsset
raiseSalaries (Boss n Good s as) = Boss n Good (raise s) (raiseSalaries <$> as)
raiseSalaries a@(Boss _ Bad _ _) = a -- The salaries of everything below are not raised if the performance is 'Bad'
raiseSalaries a = runIdentity $ traverse_ca (pure . raiseSalaries) a
使用 recursion-schemes, and also a bit of Template Haskell 生成基础 CompanyAssetF
仿函数的解决方案:
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveTraversable #-}
import Data.Functor.Foldable (cata,embed)
import Data.Functor.Foldable.TH (makeBaseFunctor)
$(makeBaseFunctor ''CompanyAsset)
raiseSalaries :: CompanyAsset -> CompanyAsset
raiseSalaries asset = cata go asset raise'
where
go c raiser = embed $
case c of
BossF _ Bad _ _ -> fmap ($ id) c
_ -> raiser $ fmap ($ raiser) c
raise' (BossF name perf salary rec) = BossF name perf (raise salary) rec
raise' (EmployeeF name salary) = EmployeeF name (raise salary)
raise' other = other
代数returns一个函数,目的是让"should get raise"信息从根流向叶