遍历多态结构并仅在少数情况下执行转换

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"信息从根流向叶