Haskell do notation edge case не может проверить тип

Я пытаюсь понять правила нотации do.

Вот код, который проверяет тип:

fff :: Maybe Int
fff = do
    _ <- pure $ Just 100
    (+10)
    <$> Just 50

Что в основном fff = (+10) <$> Just 50. Я бы предположил, что приведенное выше могло бы не проверить тип, потому что, безусловно, каждая строка должна быть в контексте Maybe, а (+10) - нет.

Почему вышеприведенная проверка типов? Вот более простой пример вышеописанного:

fff :: Int -> Maybe Int
fff i = do
    (+10)
    <$> Just i

Почему приведенный выше синтаксис считается допустимым? Разве это не «обессахаривает»:

fff i = ((+10) >>= (\i -> fmap (Just i))) i

Что действительно дает ошибку проверки типа в ghci.


Вот пример, в котором не проверяется тип после того же отступа, что и выше:

x :: Maybe Int
x = do
  _ <- Just 1
  undefined
  <$> Just 5

(Спасибо @cvlad из Slack-чата FP за приведенный выше пример)


person Chris Stryczynski    schedule 07.03.2020    source источник
comment
К вашему сведению Do-нотация объяснена в цветах.   -  person Will Ness    schedule 07.03.2020
comment
ваши вопросы касаются не нотации do, а внутренней работы парсера GHC. так что он не смог распознать это выражение, когда вы разбили его на две строки, ну и что. не делайте этого, не делая отступ второй строки больше, чем первый! :) и вы не включили сообщения об ошибках. Я ожидаю, что ошибки сообщают вам о задействованных подвыражениях, и таким образом вы можете видеть, как они читаются.   -  person Will Ness    schedule 07.03.2020
comment
@WillNess Хороший вопрос. Почему синтаксический анализатор/проверщик типов принимает одно, но не другое?   -  person chepner    schedule 07.03.2020
comment
Очевидно, что первый соответствует какому-то правилу; если вы не знаете, вам не нужно отклонять вопрос как не стоящий того, чтобы его задавать.   -  person chepner    schedule 07.03.2020
comment
@ Крис, ты резко изменил вопрос. ваша интерпретация блока do как кода привязки неверна: <$> - это бинарный оператор infix.   -  person Will Ness    schedule 07.03.2020
comment
<$> Just i само по себе является неправильным выражением.   -  person Will Ness    schedule 07.03.2020
comment
Извините, я некоторое время смотрел на этот код, поэтому у меня немного поджарились мозги. Но я думаю, что сузил его до того, в чем на самом деле заключается мой вопрос. По сути, я сравнивал два блока кода и выяснял, почему в одном выполняется проверка типов, а в другом нет.   -  person Chris Stryczynski    schedule 07.03.2020
comment
Так может ошибка ghc? @WillNess   -  person Chris Stryczynski    schedule 07.03.2020
comment
нет, смотрите мой ответ. Но сейчас вы не приводите неудачный пример.   -  person Will Ness    schedule 07.03.2020
comment
трудно преследовать ваш меняющийся вопрос. :) в следующий раз, пожалуйста, не делай этого. код изменился, но ответ тот же. блок do перед <$> равен Maybe t (из-за Just 1), и мы не можем fmap это.   -  person Will Ness    schedule 07.03.2020
comment
нотация do является абстракцией над абстракцией, и я считаю, что ее можно легко и фактически следует избегать. При работе с монадами используйте >>= и >> с правильным отступом, и это так же удобно для чтения и поддержки, как и предположительно императивное, но иногда обманчивое нотация do. Код Haskell не должен выглядеть императивным. Считать ли нотации вредными   -  person Redu    schedule 07.03.2020
comment
еще одна вещь, в вашем первом фрагменте кода, fff = do // _ <- pure $ Just 100 // (+10) // <$> Just 50, если вы удалите pure $, произойдет сбой. из-за pure блок do может быть любого монадического типа, поэтому для соответствия (+10) он становится ((->) r) t, поэтому на самом деле fff = (\r -> let _ = const (Just 100) r in (+10) r) <$> Just 50 (что действительно эквивалентно (+10) <$> Just 50. конечно, каждая строка должна быть в контексте Maybe, который (+10) не вы пропустил там pure, что делает контекст полиморфным, Just 100 — это просто чистое значение.   -  person Will Ness    schedule 08.03.2020


Ответы (2)


Это странное взаимодействие.

Я начал упрощать тестовый пример, и он работает нормально.

> x = do succ ; <$> Just 1
> x
Just 2

Для сравнения, это НЕ анализирует:

> y = do { succ ; <$> Just 1 }      
error: parse error

Однако это анализирует:

> z = do { succ } <$> Just 1      
> z
Just 2

Итак, вот что я думаю, что происходит. Поскольку токен <$> никогда не может запускать выражение, синтаксический анализ предварительно завершается ошибкой. Правило синтаксического анализатора do, по сути, является правилом максимального пережевывания: в случае неудачи добавьте неявное } и повторите попытку.

Из-за этого x выше анализируется как z. Поскольку succ является монадическим значением (строка (+10) в вопросе ОП), оно может появляться внутри do. Это делает проверку типов успешной.

Цитирование отчета Haskell 2.7

Закрывающая фигурная скобка также вставляется каждый раз, когда заканчивается синтаксическая категория, содержащая список макетов; то есть, если недопустимая лексема встречается в точке, где закрывающая фигурная скобка была бы допустимой, вставляется закрывающая скобка.

person chi    schedule 07.03.2020
comment
do 1 анализирует так же, как do "1". так что это даже не должно быть монадическим значением. - person Will Ness; 07.03.2020

fff :: Int -> Maybe Int
fff i = do
    (+10)
    <$> Just i

Почему приведенный выше синтаксис считается допустимым?

Потому что он анализируется как

fff i = do {        -- do { A } is just
    (+10) }         --      A
    <$> Just i

что эквивалентно

fff i =
    (+10) 
    <$> Just i

потому что <$> Just i само по себе является недопустимым выражением (поэтому fff i = ((+10) >>= (\i -> fmap (Just i))) i является неправильным переводом), и это ограничивает размер блока do в соответствии с правилом, указанным в ответе @chi.

Действительно, его тип выводится как

fff :: Num b => b -> Maybe b

Второй пример работает, если вы добавите пробел перед <$> в последней строке. Без пробела он снова анализируется как

inputTest :: FormInput -> IO (Either [String] (Int, Int))
inputTest fi = do {
    allErrors' <- undefined :: IO [String]
    undefined }
    <$> ((liftM2 ) (,) <$> undefined <*> undefined) fi

потому что <$> ... само по себе является недопустимым выражением. Действительно, когда я добавляю явные разделители,

inputTest2 :: String -> IO (Either [String] (Int, Int))
inputTest2 fi = do {
    allErrors2 <- undefined :: IO [String] ;
    undefined  }
    <$> ((liftM2 ) (,) <$> undefined <*> undefined) fi

Я получаю точно такое же сообщение об ошибке в TIO (пришлось использовать String вместо вашего типа).

Начиная с первого undefined :: IO [String], весь блок do имеет некоторый тип IO t, и мы не можем отобразить это ни на что.

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


Ваш новый пример

x :: Maybe Int
x = do          -- {     this is
  _ <- Just 1   -- ;       how it is
  undefined     -- }         parsed
  <$> Just 5

Код изменился, но ответ тот же. Блок do перед блоком <$> равен Maybe t (из-за Just 1), и мы не можем отобразить это.

Снова сделайте отступ в последней строке, и она скомпилируется, потому что undefined <$> Just 5 теперь будет проанализировано как одно выражение.

person Will Ness    schedule 07.03.2020