Не удалось сопоставить тип «HandlerSite m0» с «HandlerSite m» при «изменении» поля с тем же значением - PullRequest
0 голосов
/ 18 октября 2018

Я работаю над приложением Yesod и хотел бы иметь альтернативу textField с измененным fieldView.Для начала я попробовал это:

textField
  :: ( Monad m
     , RenderMessage (HandlerSite m) FormMessage
     )
  => Field m Text
textField = I.textField
    { fieldView = fieldView I.textField
    }

Насколько я вижу, этот textField должен быть идентичен I.textField.Однако я получаю следующую ошибку:

Foo.hs:37:19: error:
    • Couldn't match type ‘HandlerSite m0’ with ‘HandlerSite m’
      Expected type: FieldViewFunc m Text
        Actual type: FieldViewFunc m0 Text
      NB: ‘HandlerSite’ is a type function, and may not be injective
      The type variable ‘m0’ is ambiguous
    • In the ‘fieldView’ field of a record
      In the expression: I.textField {fieldView = fieldView I.textField}
      In an equation for ‘textField’:
          textField = I.textField {fieldView = fieldView I.textField}
    • Relevant bindings include
        textField :: Field m Text
          (bound at Foo.hs:36:1)

Интересно, что этот альтернативный способ написания этого прекрасно работает:

textField
  :: ( Monad m
     , RenderMessage (HandlerSite m) FormMessage
     )
  => Field m Text
textField = f
    { fieldView = fieldView
    }
  where
    f@Field {..} = I.textField

Проблема в использовании fieldView в качестве функции?Я прямо сейчас в замешательстве.Я пытался использовать ScopedTypeVariables, чтобы связать m с m0, но это не сработало, и я не понимаю, зачем это вообще понадобилось.Что мешает m совпадению с m0?

РЕДАКТИРОВАТЬ: Я только что попытался:

textField
  :: ( Monad m
     , RenderMessage (HandlerSite m) FormMessage
     )
  => Field m Text
textField = I.textField
    { fieldView = fieldView
    }
  where
    Field {..} = I.textField

И это не удалось, поэтому я думаю, что проблема связана с упоминанием I.textField дважды,Это странноНе похоже, что I.textField является членом класса типов с множеством определений для выбора, и, даже если бы это было так, я не вижу, что мешает ghc сделать вывод, что m и m0 одинаковы ...ok HandlerSite - это семейство типов, поэтому, я думаю, с точки зрения проверки типов это может привести к различным экземплярам RenderMessage и таким разным определениям кода, который так или иначе связан с I.textField.Я думаю, что начинаю видеть свет.

РЕДАКТИРОВАТЬ 2: Я думал, что мог бы связать их следующим образом:

textField
  :: ( Monad m
     , RenderMessage (HandlerSite m) FormMessage
     )
  => Field m Text
textField = (I.textField :: Field m Text)
    { fieldView = fieldView (I.textField :: Field m Text)
    }

с включенным ScopedTypeVariables, но, видимо, нет.

РЕДАКТИРОВАТЬ 3: Следуя логике, это работает:

textField
  :: ( Monad m
     , RenderMessage (HandlerSite m) FormMessage
     )
  => Field m Text
textField = f
    { fieldView = fieldView f
    }
  where
    f = I.textField

Так что я думаю, это как-то связано с локальными привязками верхнего уровня?

1 Ответ

0 голосов
/ 18 октября 2018

И это не удалось, поэтому я думаю, что проблема связана с упоминанием I.textField дважды.Это странно.

На самом деле, это довольно часто, когда участвуют семейства типов.Позвольте мне показать проблему в более простом случае.Предположим, у нас есть семейство типов следующим образом:

type family F a
type instance F Int  = String
type instance F Bool = String

Обратите внимание, что F Int и F Bool на самом деле одного типа, то есть String.Это возможно, поскольку F может быть неинъективной функцией.

Теперь, если у нас под рукой будет следующая функция

foo :: F a -> SomeResultType

, мы обнаружим, что вообще не можемНазовите это как

foo "some string"

Почему?Что ж, компилятор не может определить, какой тип использовать для a: это может быть Int или Bool, так как оба параметра сделают F a равным String.Вызов неоднозначный, поэтому он вызывает ошибку типа.

Еще хуже, если бы мы использовали дважды , который вызывает наш код, например,

bar (foo "string") (foo "string")

, это было быдаже можно выбрать a = Int для первого вызова и a = Bool для второго вызова!

Кроме того, рассмотрим, что произойдет, если у нас будет полиморфное значение, которое может дать любое F a.

x :: forall a . F a

Тогда у нас может возникнуть соблазн позвонить foo x.В конце концов, foo занимает F a, а x может дать F a для любого a.Это выглядит хорошо, но это еще раз неоднозначно.В самом деле, что следует выбрать для a?Много вариантов применяются.Мы можем попытаться исправить это с помощью сигнатуры типа

foo (x :: F Int)

, но это полностью эквивалентно любому из

foo (x :: String)
foo (x :: F Bool)

, поэтому он действительно выбирает тип a!

В вашем коде возникает похожая проблема.Давайте разберем ошибку типа:

Couldn't match type ‘HandlerSite m0’ with ‘HandlerSite m’
    Expected type: FieldViewFunc m Text
    Actual type:   FieldViewFunc m0 Text
NB: ‘HandlerSite’ is a type function, and may not be injective

Это говорит нам о том, что в какой-то момент нам нужно указать FieldViewFunc m Text.Этот тип включает семейство типов HandlerSite m, которое из-за неинъективности может быть того же типа, что и HandlerSite m0 для некоторой другой монады m0.

Теперь I.textField может выдавать значение "для любого m".Следовательно, использование его как-то похоже на использование foo x выше.Ваш код более своеобразен, поскольку, если мы используем «тот же» вызов I.textField, компилятор может сделать вывод, что мы действительно хотим получить «правильный» m.Здесь «тот же» вызов означает определение некоторого идентификатора, такого как ваш f до I.textField, и дважды используйте f.Вместо этого, два вызова I.textField позволяют GHC выбрать два различных m s, по одному для каждого вызова, и возникает неоднозначность.

Если вы растерялись, не беспокойтесь - это немногосложно понять, особенно в относительно реальной среде, такой как Yesod.

Как решить эту проблему?Есть много способов, но, на мой взгляд, лучший современный способ устранить такие неоднозначности - это включить расширение TypeApplications (за пределами ScopedTypeVariables), а затем указать, что мы действительно хотим выбрать m в качестве внешнего m, следующим образом:

textField :: forall m . 
     ( Monad m
     , RenderMessage (HandlerSite m) FormMessage
     )
     => Field m Text
textField = I.textField @ m
    { fieldView = fieldView (I.textField @ m)
    }

Синтаксис @ m используется для выбора типа, переопределяя механизм вывода типов.Во многих случаях это имеет эффект, аналогичный написанию аннотации типов, но работает даже в «неоднозначных» случаях, когда аннотации типов этого не делают.Например, foo (x @ Int) сработал бы в более простом примере выше.

(я не знаком с Yesod, поэтому приведенное выше может не сработать, если I.textField также параметризован переменными других типов, в этом случае мынужно больше @ type приложений, например I.textField @type @type2 ... одно из которых @m.)

...