Лень/строгость между данными и новым типом
Я пытаюсь понять, почему эти два фрагмента дают разные результаты при так называемом "анализе строгости человека".
В первом примере используется data
(при условии правильного аппликативного экземпляра):
data Parser t a = Parser {
getParser :: [t] -> Maybe ([t], a)
}
> getParser (pure (,) <*> literal ';' <*> undefined ) "abc"
*** Exception: Prelude.undefined
Второй использует newtype
. Нет другой разницы:
newtype Parser t a = Parser {
getParser :: [t] -> Maybe ([t], a)
}
> getParser (pure (,) <*> literal ';' <*> undefined ) "abc"
Nothing
literal x
- это синтаксический анализатор, который успешно использует один токен ввода, если его аргумент соответствует первому токену. Поэтому в этом примере он терпит неудачу, поскольку ;
не соответствует a
. Тем не менее, пример data
все еще видит, что следующий синтаксический анализатор undefined, в то время как пример newtype
не работает.
Я прочитал этот, этот и это, но не понимайте их достаточно хорошо, чтобы понять, почему первый пример - undefined. Мне кажется, что в этом примере newtype
более ленив, чем data
, что противоположно сказанному. (По крайней мере еще один человек тоже был смущен).
Почему переход от data
в newtype
изменяет определенность этого примера?
Вот еще одна вещь, которую я обнаружил: с этим аппликативным экземпляром анализатор data
выше выводит undefined:
instance Applicative (Parser s) where
Parser f <*> Parser x = Parser h
where
h xs =
f xs >>= \(ys, f') ->
x ys >>= \(zs, x') ->
Just (zs, f' x')
pure a = Parser (\xs -> Just (xs, a))
тогда как с этим экземпляром анализатор data
выше не выводит undefined (при условии правильного экземпляра Monad для Parser s
):
instance Applicative (Parser s) where
f <*> x =
f >>= \f' ->
x >>= \x' ->
pure (f' x')
pure = pure a = Parser (\xs -> Just (xs, a))
Полный фрагмент кода:
import Control.Applicative
import Control.Monad (liftM)
data Parser t a = Parser {
getParser :: [t] -> Maybe ([t], a)
}
instance Functor (Parser s) where
fmap = liftM
instance Applicative (Parser s) where
Parser f <*> Parser x = Parser h
where
h xs = f xs >>= \(ys, f') ->
x ys >>= \(zs, x') ->
Just (zs, f' x')
pure = return
instance Monad (Parser s) where
Parser m >>= f = Parser h
where
h xs =
m xs >>= \(ys,y) ->
getParser (f y) ys
return a = Parser (\xs -> Just (xs, a))
literal :: Eq t => t -> Parser t t
literal x = Parser f
where
f (y:ys)
| x == y = Just (ys, x)
| otherwise = Nothing
f [] = Nothing
Ответы
Ответ 1
Как вы, вероятно, знаете, основное различие между data
и newtype
заключается в том, что при data
это конструкторы data
ленивы, а newtype
является строгим, т.е. заданы следующие типы
data D a = D a
newtype N a = N a
затем D ⊥ `seq` x = x
, но N ⊥ `seq` x = ⊥
.
Однако, возможно, менее широко известно, что когда вы сопоставляете шаблон с этими конструкторами,
роли "перевернуты", т.е. с помощью
constD x (D y) = x
constN x (N y) = x
затем constD x ⊥ = ⊥
, но constN x ⊥ = x
.
Это то, что происходит в вашем примере.
Parser f <*> Parser x = Parser h where ...
С data
совпадение шаблона в определении <*>
будет расходиться сразу, если
аргументов ⊥
, но с newtype
конструкторы игнорируются, и это
как если бы вы написали
f <*> x = h where
который будет отклоняться только для x = ⊥
, если требуется x
.
Ответ 2
Разница между data
и newtype
заключается в том, что data
"снят", а newtype
- нет. Это означает, что data
имеет дополнительный ⊥ - в этом случае это означает, что undefined
/= Parser undefined
. Когда ваш шаблон кода Applicative
совпадает с Parser x
, он заставляет значение ⊥
, если конструктор.
Когда вы сопоставляете шаблон с конструктором data
, он оценивается и разбирается, чтобы убедиться, что он не ⊥. Например:
λ> data Foo = Foo Int deriving Show
λ> case undefined of Foo _ -> True
*** Exception: Prelude.undefined
Таким образом, сопоставление шаблонов в конструкторе data
является строгим и будет принудительным. A newtype
, с другой стороны, представляется точно так же, как и тип его конструктора. Поэтому сопоставление в конструкторе newtype
ничего не делает:
λ> newtype Foo = Foo Int deriving Show
λ> case undefined of Foo _ -> True
True
Возможно, есть два способа изменить вашу программу data
так, чтобы она не разбилась. Можно было бы использовать неопровержимое соответствие шаблонов в вашем экземпляре Applicative
, который всегда будет "успешным" (но использование согласованных значений в любом месте позже может завершиться неудачей). Каждое совпадение newtype
ведет себя как неопровержимый шаблон (поскольку нет конструктора для соответствия, строгости).
λ> data Foo = Foo Int deriving Show
λ> case undefined of ~(Foo _) -> True
True
Другим будет использование Parser undefined
вместо undefined
:
λ> case Foo undefined of Foo _ -> True
True
Это совпадение будет успешным, потому что существует допустимое значение Foo
, которое соответствует. Это происходит с undefined
, но это не имеет значения, так как мы его не используем - мы смотрим только на самый верхний конструктор.
В дополнение ко всем ссылкам, которые вы указали, вы можете найти эту статью.