12 Oct 2023

Changing a Sum Type to a Type Class

  1. If any of the type's constuctors matches the type name rename it
  2. Split up the sum type, making each constructor a type. (These will be called the con types.) Create a type class with no methods and the name of the sum type. It will have one type variable corresponding to the con type. If the original sum type had type variables the class will also have these. The sum type instances must be converted to instances for the con types. Safecopy instances deserve special consideration.
    Old:
          data TableColumn v
              = ValueColumn
                { _columnPath :: v
                , _sortable :: Bool
                , _columnRole :: UserRole
                , _columnSpan :: Int
                , _columnHeaderClasses :: [Text]
                , _columnCellClasses :: [Text]
                }
              | RowControlColumn
              deriving (Generic, Show, Serialize, Eq, Ord, Functor)
        
    New:
          class TableColumn (col :: * -> *) (v :: *)
    
          data ValueColumn v =
            ValueColumn
            { _columnPath :: v
            , _sortable :: Bool
            , _columnRole :: UserRole
            , _columnSpan :: Int
            , _columnHeaderClasses :: [Text]
            , _columnCellClasses :: [Text]
            } deriving (Generic, Show, Serialize, Eq, Ord, Functor)
    
          instance SafeCopy v => SafeCopy (ValueColumn v) where version = 1; kind = base
    
          data RowControlColumn v = RowControlColumn v
            deriving (Generic, Show, Serialize, Eq, Ord, Functor)
    
          instance SafeCopy v => SafeCopy (RowControlColumn v) where version = 1; kind = base
        
  3. Occurences of the old type become type variables:
    Old:
          newtype TableColumns v = TableColumns (NonEmpty (v, [TableColumn v]))
        
    New:
          newtype TableColumns con v = TableColumns (NonEmpty (v, [con v]))
        
  4. Functions that used the old type become polymorphic:
    Old:
          filterTableColumns :: (TableColumn v -> Bool) -> TableColumns v -> TableColumns v
        
    New:
          filterTableColumns :: (con v -> Bool) -> TableColumns con v -> TableColumns con v
        
  5. Functions that case on the sum type become class methods.
    Old:
          w :: [TableColumn v] -> Int
          w = sum . fmap (\case ValueColumn{..} -> _columnSpan; _ -> 1)
        
    New:
          class TableColumn (con :: * -> *) (v :: *) where
            w :: [con v] -> Int
    
          instance TableColumn ValueColumn v where
            w = sum . fmap _columnSpan
    
          instance TableColumn RowControlColumn v where
            w _ = 1
          

    Lists of the sum type can be temporarily supported using ExistentialQuantifiers.

Changing a Type Class to a Record

  1. Methods become fields
  2. The constraint becomes a function argument
  3. The constraint is replaced by the superclasses

Changing a Record into functions

  1. Initial data TemplateTypeInfo template = TemplateTypeInfo { templateKeyPath :: PathTo 'L (ReportType template) (TemplateId template) , templateHtmlPath :: PathTo 'L (ReportType template) (ValueType template) , templateLibraryPath :: PathTo 'L (ProfileType template) (AssocList RenderedUnicode (ValueType template)) } deriving Generic
  2. Rename the fileds data TemplateTypeInfo template = TemplateTypeInfo { templateKeyPath' :: PathTo 'L (ReportType template) (TemplateId template) , templateHtmlPath' :: PathTo 'L (ReportType template) (ValueType template) , templateLibraryPath' :: PathTo 'L (ProfileType template) (AssocList RenderedUnicode (ValueType template)) } deriving Generic
  3. Add a class for each field: class TemplateKeyPath template a where templateKeyPath :: a -> PathTo 'L (ReportType template) (TemplateId template) instance TemplateKeyPath template (TemplateTypeInfo template) where templateKeyPath = templateKeyPath' @template class TemplateHtmlPath template a where templateHtmlPath :: a -> PathTo 'L (ReportType template) (ValueType template) instance TemplateHtmlPath template (TemplateTypeInfo template) where templateHtmlPath = templateHtmlPath' @template class TemplateLibraryPath template a where templateLibraryPath :: a -> PathTo 'L (ProfileType template) (AssocList RenderedUnicode (ValueType template)) instance TemplateLibraryPath template (TemplateTypeInfo template) where templateLibraryPath = templateLibraryPath' @template