Проблема с вложенными монадами при написании простого генератора URL QuickCheck

Еще один вопрос новичка, который, вероятно, возник из-за того, что я не понял Monadic 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 и, наконец, заключить его в новый 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 неверна. Где я ошибаюсь? Какой должна быть правильная интуиция?

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


person Ulrich Schuster    schedule 07.05.2020    source источник
comment
Возможно, вы могли бы использовать suchThatMap и экземпляр MonadThrow для Maybe   -  person moonGoose    schedule 07.05.2020
comment
Важная деталь: что вы хотите, чтобы программа делала в случае сбоя? Должен ли он повторять попытку, пока вы не получите действительный URI, или он должен вернуть значение, указывающее на сбой?   -  person Hjulle    schedule 08.05.2020


Ответы (1)


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

-- 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 как предложил MoonGoose. Например вот так:

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’
person Hjulle    schedule 08.05.2020
comment
Большое спасибо за вашу помощь. Насколько я понимаю: помимо вопроса, что делать в случае сбоя, моя ошибка типа возникла из-за того, что я использовал монадическую привязку для url вместо привязки let? Поскольку внешняя монада Gen вместо MonadThrow, эта привязка монад не проверяет тип? - person Ulrich Schuster; 08.05.2020
comment
@UlrichSchuster Я обновил свой ответ. Это проясняет? Да, ты во многом прав. Единственная деталь заключается в том, что MonadThrow — это не просто один Monad, а класс монад, в который входят Maybe, Either и IO, но не Gen. - person Hjulle; 08.05.2020
comment
@UlrichSchuster Если вы посмотрите на то, как do-нотация обесахаривает, это может помочь вам понять, почему она эквивалентна: en.wikibooks.org/wiki/Haskell/do_notation - person Hjulle; 08.05.2020