Проблема с вложенными монадами при написании простого генератора URL QuickCheck - PullRequest
2 голосов
/ 07 мая 2020

Еще один новый ie вопрос, который, вероятно, возникает из-за того, что я не понял Monadi c do в Haskell: Я хочу написать простой генератор QuickCheck для правильно сформированных URI, используя тип Text.URI из пакет modern-uri. Насколько я понимаю, здесь задействованы два типа монад: MonadThrow для обработки ошибок при построении URI и Gen из QuickCheck.

Вот моя попытка реализовать генератор. Он не проверяет тип:

import qualified Text.URI as URI

uriGen :: Gen URI.URI
uriGen = do
    sc <- elements ["https", "http", "ftps", "ftp"]
    tld <- elements [".com", ".org", ".edu"]
    hostName <- nonEmptySafeTextGen -- (:: Gen Text), a simple generator for printable text. 
    uri <- do
        scheme <- URI.mkScheme sc
        host <- URI.mkHost $ (hostName <> "." <> tld)
        return $ URI.URI (Just scheme) (Right (URI.Authority Nothing host Nothing)) Nothing [] Nothing
    return uri

Насколько я понимаю, внешний блок do относится к монаде Gen, а внутренний - к MonadThrow. Я пытаюсь развернуть Text частей из их Gen, а затем использовать развернутые Text для создания URI частей, развернуть их из их MonadThrow, затем заново собрать весь URI и, наконец, обернуть его в new Gen.

Однако я получаю следующую ошибку проверки типа:

    • No instance for (MonadThrow Gen)
        arising from a use of ‘URI.mkScheme’
    • In a stmt of a 'do' block: scheme <- URI.mkScheme sc
      In a stmt of a 'do' block:
        uri <- do scheme <- URI.mkScheme sc
                  host <- URI.mkHost $ (hostName <> "." <> tld)
                  return
                    $ URI.URI
                        (Just scheme)
                        (Right (URI.Authority Nothing host Nothing))
                        Nothing
                        []
                        Nothing

Из-за ошибки я подозреваю, что моя интуиция относительно разворачивания и упаковки частей URI неверна. Где я ошибаюсь? Какая была бы правильная интуиция?

Большое спасибо за вашу помощь!

1 Ответ

1 голос
/ 08 мая 2020

Самым простым решением было бы вложить монады друг в друга, например, вот так:

-- One instance for MonadThrow is Maybe, so this is a possible type signature
-- uriGen :: Gen (Maybe URI.URI)
uriGen :: MonadThrow m => Gen (m URI.URI)
uriGen = do
    sc <- elements ["https", "http", "ftps", "ftp"]
    tld <- elements [".com", ".org", ".edu"]
    hostName <- nonEmptySafeTextGen -- (:: Gen Text), a simple generator for printable text. 
    let uri = do
          scheme <- URI.mkScheme sc
          host <- URI.mkHost $ (hostName <> "." <> tld)
          return $ URI.URI
                   { uriScheme = Just scheme
                   , uriAuthority = Right (URI.Authority Nothing host Nothing)
                   , uriPath = Nothing  
                   , uriQuery = []
                   , uriFragment = Nothing
                   }

    return uri

Теперь переменная uri интерпретируется как чистое значение по отношению к Gen монаде а MonadThrow будет заключен в отдельный слой внутри него.

Если вы хотите, чтобы он повторял попытку до тех пор, пока не добьется успеха, вы можете использовать suchThatMap как Луна Goose предложено. Например, вот так:

uriGen' :: Gen URI.URI
uriGen' = suchThatMap uriGen id

Это работает, потому что suchThatMap имеет тип

suchThatMap :: Gen a -> (a -> Maybe b) -> Gen b

, поэтому, когда вы даете ему функцию идентификации в качестве второго аргумента, он становится

\x -> suchThatMap x id :: Gen (Maybe b) -> Gen b

, что соответствует типу выше: uriGen :: Gen (Maybe URI.URI).


EDIT: Чтобы ответить на ваш вопрос в комментариях:

MonadThrow - это класс типов, который является суперклассом Monad (см. Документацию ). То, что вы написали, эквивалентно


uriGen :: Gen URI.URI
uriGen = do
    sc <- elements ["https", "http", "ftps", "ftp"]
    tld <- elements [".com", ".org", ".edu"]
    hostName <- nonEmptySafeTextGen
    scheme <- URI.mkScheme sc
    host <- URI.mkHost $ (hostName <> "." <> tld)
    URI.URI (Just scheme) (Right (URI.Authority Nothing host Nothing)) Nothing [] Nothing

Другими словами, вложение do не имеет никакого эффекта и пытается интерпретировать все в монаде Gen. Так как Gen не входит в список экземпляров для MonadThrow, вы получите сообщение об ошибке.

Вы можете проверить, какие экземпляры реализует тип, а какие типы реализуют тип класс, используя :i в ghci:

Prelude Test.QuickCheck> :i Gen
newtype Gen a
  = Test.QuickCheck.Gen.MkGen {Test.QuickCheck.Gen.unGen :: Test.QuickCheck.Random.QCGen
                                                            -> Int -> a}
    -- Defined in ‘Test.QuickCheck.Gen’
instance [safe] Applicative Gen -- Defined in ‘Test.QuickCheck.Gen’
instance [safe] Functor Gen -- Defined in ‘Test.QuickCheck.Gen’
instance [safe] Monad Gen -- Defined in ‘Test.QuickCheck.Gen’
instance [safe] Testable prop => Testable (Gen prop)
  -- Defined in ‘Test.QuickCheck.Property’
...