Обход полиморфных структур и выполнение преобразования только в нескольких случаях
Предположим, мы представляем иерархию компании следующим образом:
{-# 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
дано и не может быть изменено).
Поэтому мой вопрос заключается в том, существует ли способ обхода структур данных таким образом, чтобы избежать вышеприведенного примера.
Этот вопрос связан с аналогичным, который я опубликовал, но в этом случае использование everywhere'
не поможет, поскольку есть случаи, когда зарплаты не должны повышаться.
2 ответа
Это может быть достигнуто с Traversal
за CompanyAsset
, Вы можете написать это самостоятельно или использовать uniplate
или же 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
Решение, которое использует схемы рекурсии, а также немного 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
Алгебра возвращает функцию для того, чтобы информация "должна получить повышение" передавалась от корня к листьям.