Разрушение «Может быть (а, б)»

Своего рода продолжение моего последнего вопроса. Я прохожу курс Брента Йорги по Haskell и пытаюсь решить упражнение, в котором нам предлагается создать экземпляр Applicative для следующего типа:

newtype Parser a = Parser { runParser :: String -> Maybe (a, String) }

runParser анализирует строку и возвращает токен и оставшуюся строку. В этом случае p1 <*> p2 должен применить функцию, сгенерированную runParser p1, к токену, сгенерированному runParser p2 (применяется к тому, что осталось от строки после запуска runParser p1).

Пока у меня есть:

(Parser { runParser = run }) <*> (Parser { runParser = run' }) = Parser run''
  where run'' s = (first <$> f) <*> (s' >>= run')
          where f = fst <$> run s
                s' = snd <$> run s

(first <$> f) <*> (s' >>= run') кажется мне довольно кратким, но вложенные where и странная деструктуризация run s выглядят «не так». Есть ли более приятный способ написать это?

Небольшое примечание к стилю: Parser { runParser = run } — довольно однозначный способ деструктурирования newtype. Более привычным является просто вызов p или что-то в этом роде, а затем использование runParser p для доступа к функции внутри.

bradrn 26.12.2020 13:00

Вы можете получить Applicative через StateT String Maybe, используя DerivingVia

Iceland_jack 30.12.2020 06:35
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
2
173
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

На мой взгляд, нет ничего постыдного в простоте, используя только базовое сопоставление с образцом, не слишком полагаясь на <*>, <$>, first и другие библиотечные функции.

Parser pF <*> Parser pX = Parser $ \s -> do
   (f, s' ) <- pF s
   (x, s'') <- pX s'
   return (f x, s'')

Блок do выше находится в монаде Maybe.

Ответ принят как подходящий

Во-первых, позвольте мне немного переписать это, чтобы избежать сопоставления с образцом:

p <*> q = Parser run
  where run s = (first <$> f) <*> (s' >>= runParser q)
          where f = fst <$> runParser p s
                s' = snd <$> runParser p s

Здесь я просто использовал метод доступа к полю runParser :: Parser a -> String -> Maybe (a, String) вместо прямого сопоставления с образцом для аргументов. Это считается более идиоматичным методом доступа к функциям newtyped в Haskell.

Далее, есть некоторые очевидные упрощения, которые можно сделать, в частности, встраивая некоторые функции:

p <*> q = Parser $ \s -> (first <$> f) <*> (s' >>= runParser q)
  where
    f = fst <$> runParser p s
    s' = snd <$> runParser p s

(Обратите внимание, что s теперь нужно явно передать функциям в блоке where, чтобы они могли получить к нему доступ. Не волнуйтесь, я избавлюсь от этого через минуту.)

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

p <*> q = Parser $ \s ->
    let qResult = s' s >>= runParser q
    in first <$> f s <*> qResult
  where
    f s = fst <$> runParser p s
    s' s = snd <$> runParser p s

Далее, давайте избавимся от этих надоедливых определений f и s'. Мы можем сделать это, используя сопоставление с образцом. Путем сопоставления с образцом на выходе runParser p s мы можем получить прямой доступ к этим значениям:

p <*> q = Parser $ \s ->
    case runParser p s of
        Nothing -> Nothing 
        Just (f, s') ->
            let qResult = runParser q s'
            in first f <$> qOutput

(Обратите внимание, что, поскольку f и s' больше не находятся в Maybe, большая часть аппликативной и монадической сантехники, которая требовалась раньше, теперь не нужна. Один <$> все еще остается, поскольку runParser q s' все еще может выйти из строя).

Давайте немного перепишем это, встроив qResult:

p <*> q = Parser $ \s ->
    case runParser p s of
        Nothing -> Nothing 
        Just (f, s') -> first f <$> runParser q s'

Теперь обратите внимание на закономерность в этом коде. Это делает runParser p s, терпит неудачу, если это терпит неудачу; в противном случае он использует значение в другом вычислении, которое может завершиться ошибкой. Это просто звучит как монадическая последовательность! Итак, давайте перепишем это с помощью >>=:

p <*> q = Parser $ \s -> runParser p s >>= \(f, s') -> first f <$> runParser q s'

И, наконец, все это можно переписать в do-нотации для удобства чтения:

p <*> q = Parser $ \s -> do
     (f, s') <- runParser p s
     qResult <- runParser q s'
     return $ first f qResult

Намного легче читать! И что делает эту версию особенно приятной, так это то, что легко увидеть, что происходит — запустить первый анализатор, получить его вывод и использовать его для запуска второго анализатора, а затем объединить результаты.

Отличный ответ, теперь я тоже, наконец, понимаю, для чего на самом деле хорош do.

Peter 26.12.2020 13:53

@Peter Рад, что смог помочь! do действительно удивительно полезен, когда вы полностью понимаете, как это работает.

bradrn 26.12.2020 14:07

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