Невозможно обобщить операции с данными в Haskell, неоднозначный тип возврата

Я пытаюсь обобщить некоторый код, который у меня есть, для генерации отчета по различным представлениям за диапазон дат, например, представление может выглядеть так:

      | Signups | Active users 
------|---------|--------------
 2018 | 155     | 3241         
 2017 | 139     | 3083         
 2016 | 125     | 2543         

Или это может быть переключено на месячный вид как это:

         | Signups | Active users 
---------|---------|--------------
 08/2018 | 15      | 324            
 07/2018 | 13      | 308          
 06/2018 | 12      | 254          

Вы поняли... Итак, я подумал, что было бы целесообразно обобщить каждый "режим", в котором вы можете установить эти данные для отображения, он поддерживает дни / недели / месяцы / кварталы / годы.

data DateField
  = DateFieldDay CalendarDay
  | DateFieldWeek CalendarWeek
  | DateFieldMonth CalendarMonth
  | DateFieldQuarter CalendarQuarter
  | DateFieldYear CalendarYear

instance Calendar DateField where
  fromDay = DateFieldDay . fromDay
  fallsWithin = \case
    DateFieldDay calendar -> fallsWithin calendar
    DateFieldWeek calendar -> fallsWithin calendar
    DateFieldMonth calendar -> fallsWithin calendar
    DateFieldQuarter calendar -> fallsWithin calendar
    DateFieldYear calendar -> fallsWithin calendar

instance ToJSON DateField where
  toJSON = \case
    DateFieldDay calendar -> toJSON calendar
    DateFieldWeek calendar -> toJSON calendar
    DateFieldMonth calendar -> toJSON calendar
    DateFieldQuarter calendar -> toJSON calendar
    DateFieldYear calendar -> toJSON calendar

class Calendar c where
  fromDay :: Day -> c
  fallsWithin :: c -> Day -> Bool

newtype CalendarDay = CalendarDay Day   

instance Calendar CalendarDay where
  fromDay = CalendarDay
  fallsWithin (Calendar dayField) day = day == dayField 

instance ToJSON CalendarDay where
  toJSON (CalendarDay day) = toJSON day

-- instances for CalendarWeek, CalendarMonth etc etc

Так что моя проблема возникает с тем фактом, что случаи из моего типа суммы DateField должны реализовать одну и ту же функцию дважды extractCalendar... Но я не могу обобщить это, и мне нужно эффективно оставить это определенным дважды, я не могу определить что-то вроде этого:

extractCalendar :: (Calendar c, ToJSON c) => DateField -> c
extractCalendar = \case
  DateFieldDay calendar -> calendar
  DateFieldWeek calendar -> calendar
  DateFieldMonth calendar -> calendar
  DateFieldQuarter calendar -> calendar
  DateFieldYear calendar -> calendar

Потому что все внутренние типы newtypes и подпись типа для extractCalendar несколько бессмысленно.

Я не уверен, что это на самом деле ухудшает код, пытаясь так обобщить, и не могу понять, где взять код отсюда.

Другая часть этого кода, которой я хотел бы поделиться, выглядит следующим образом:

generateRange :: MonadIO m => DateView -> m [DateField]
generateRange view = rangeToFields . takeWhile includeDate <$> dateStream
  where
    rangeToFields dates =
      case view of
        DateViewDay -> DateFieldDay <$> foldr calendarDays [] dates
        DateViewWeek -> DateFieldWeek <$> foldr calendarWeeks [] dates
        DateViewMonth -> DateFieldMonth <$> foldr calendarMonths [] dates
        DateViewQuarter -> DateFieldQuarter <$> foldr calendarQuarters [] dates
        DateViewYear -> DateFieldYear <$> foldr calendarYears [] dates

    calendarDays day acc =
      if True
         then fromDay day : acc
         else acc

    calendarWeeks day acc =
      if True
         then fromDay day : acc
         else acc

    calendarMonths day acc =
      if dayDate day == 1
         then fromDay day : acc
         else acc

    calendarQuarters day acc =
      if dayDate day == 1 && (dayMonth day `mod` 3 == 0)
         then fromDay day : acc
         else acc

    calendarYears day acc =
      if dayDate day == 1 && dayMonth day == 1
         then fromDay day : acc
         else acc

    includeDate day =
      dayYear day >= 2014

Это занимает поток дней, от сегодняшнего дня до прошедшего одного дня после следующего, а затем складывает их, генерируя соответствующие DateField в зависимости от того DateView предоставлен.

Я чувствую, что неправильно использую typeclass построить здесь... но я не уверен, как добиться обобщения я хочу в Haskell с любой другой структурой...

2 ответа

Случаи Calendar Кажется, по существу, различные интервалы времени. Тогда почему бы вам не сделать методы

  asInterval :: c -> (UTCTime, UTCTime)
  -- e.g. asInterval (Day 2018-8-15) ≡ (2018-8-15_0:0:0, 2018-8-16_0:0:0)
  containingInterval :: (UTCTime, UTCTime) -> Maybe c
  -- e.g. containingInterval (2018-8-5_0:0:0, 2018-8-22_0:0:0) :: Maybe Month
  --            ≡ Just (Month 2018-8)
  --      containingInterval (2018-8-5_0:0:0, 2018-8-22_0:0:0) :: Maybe Week
  --            ≡ Nothing

Это позволило бы

containing :: (Calendar c, Calendar c') => c -> Maybe c'
containing = containingInterval . asInterval

и это может быть использовано

extractCalendar :: (Calendar c, ToJSON c) => DateField -> Maybe c
extractCalendar = \case
  DateFieldDay calendar -> containing calendar
  DateFieldWeek calendar -> containing calendar
  DateFieldMonth calendar -> containing calendar
  DateFieldQuarter calendar -> containing calendar
  DateFieldYear calendar -> containing calendar

Ваш instance ToJSON DateField проблематично, потому что он не различает типы DateField. Вы должны добавить тег типа в JSON, например так:

instance ToJSON DateField where
  toJSON (DateFieldDay calendar) = object [
       "type" .= "DateFieldDay",
       "calendar" .= calendar ]
  -- etc

Затем в экземпляре FromJSON вы можете использовать оператор case для различения "type" поле.

Другие вопросы по тегам