Как написать без Do нотации - PullRequest
       30

Как написать без Do нотации

11 голосов
/ 29 августа 2011

Я играл с ошибками компоновки и мне удалось написать функцию с подписью

getPerson :: IO (Maybe Person)

где человек:

data Person = Person String Int deriving Show

Это работает, и я написал это в нотации следующим образом:

import Control.Applicative

getPerson = do
    name <- getLine -- step 1
    age  <- getInt  -- step 2
    return $ Just Person <*> Just name <*> age 

, где

getInt :: IO (Maybe Int)
getInt = do
    n <- fmap reads getLine :: IO [(Int,String)]
    case n of
        ((x,""):[])   -> return (Just x)
        _ -> return Nothing

Я написал эту функцию с целью создания возможных сбоев. Хотя у меня мало опыта работы с монадами, отличными от Maybe и IO, похоже, что если бы у меня был более сложный тип данных с большим количеством полей, цепочечные вычисления не были бы сложными.

Мой вопрос: как бы я переписал это без записи? Поскольку я не могу связать значения с такими именами, как имя или возраст, я не совсем уверен, с чего начать.

Причина, по которой я спрашиваю, состоит в том, чтобы просто улучшить мое понимание (>> =) и (<*>) и сочинять ошибки и успехи (не путать мой код с неразборчивыми однострочниками).

Edit: я думаю, что я должен уточнить, "как я должен переписать getPerson без do-нотации", мне наплевать на функцию getInt вдвое меньше.

Ответы [ 3 ]

20 голосов
/ 29 августа 2011

Нотация Do-обозначений синтаксисом (>> =) выполняется следующим образом:

getPerson = do
    name <- getLine -- step 1
    age  <- getInt  -- step 2
    return $ Just Person <*> Just name <*> age

getPerson2 =
  getLine >>=
   ( \name -> getInt >>=
   ( \age  -> return $ Just Person <*> Just name <*> age ))

каждая строка в нотации после первой переводится в лямбду, которая затем привязывается к предыдущейлиния.Это полностью механический процесс привязки значений к именам.Я не понимаю, как использование do-notation или нет влияет на компоновку;это строго вопрос синтаксиса.

Ваша другая функция похожа:

getInt :: IO (Maybe Int)
getInt = do
    n <- fmap reads getLine :: IO [(Int,String)]
    case n of
        ((x,""):[])   -> return (Just x)
        _ -> return Nothing

getInt2 :: IO (Maybe Int)
getInt2 =
    (fmap reads getLine :: IO [(Int,String)]) >>=
     \n -> case n of
        ((x,""):[])   -> return (Just x)
        _             -> return Nothing

Несколько указателей для направления, в котором вы, кажется, движетесь:

При использовании Control.Applicative, часто полезно использовать <$> для поднятия чистых функций в монаду.В последней строке есть хорошая возможность для этого:

Just Person <*> Just name <*> age

становится

Person <$> Just name <*> age

Кроме того, вы должны изучить монадные трансформаторы.Пакет mtl является наиболее распространенным, поскольку он поставляется с платформой Haskell, но есть и другие варианты.Трансформаторы монад позволяют создавать новую монаду с комбинированным поведением основных монад.В этом случае вы используете функции типа IO (Maybe a).MTL (на самом деле базовая библиотека, преобразователи) определяет

newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }

Это то же самое, что и тип, который вы используете, с переменной m, созданной в IO.Это означает, что вы можете написать:

getPerson3 :: MaybeT IO Person
getPerson3 = Person <$> lift getLine <*> getInt3

getInt3 :: MaybeT IO Int
getInt3 = MaybeT $ do
    n <- fmap reads getLine :: IO [(Int,String)]
    case n of
        ((x,""):[])   -> return (Just x)
        _             -> return Nothing

getInt3 точно так же, за исключением конструктора MaybeT.По сути, каждый раз, когда у вас есть m (Maybe a), вы можете обернуть его в MaybeT, чтобы создать MaybeT m a.Это упрощает компоновку, как вы можете видеть по новому определению getPerson3.Эта функция совсем не беспокоится о сбое, потому что все это обрабатывается каналом MaybeT.Один оставшийся кусок - getLine, то есть IO String.Это поднято в монаду MaybeT с помощью функции lift.

Редактировать Комментарий newacct предлагает мне также предоставить пример сопоставления с образцом;это в основном то же самое с одним важным исключением.Рассмотрим этот пример (монада списка - это интересующая нас монада, Maybe как раз для сопоставления с образцом):

f :: Num b => [Maybe b] -> [b]
f x = do
  Just n <- x
  [n+1]

-- first attempt at desugaring f
g :: Num b => [Maybe b] -> [b]
g x = x >>= \(Just n) -> [n+1]

Здесь g делает то же самое, что и f,но что, если сопоставление с образцом не удастся?

Prelude> f [Nothing]
[]

Prelude> g [Nothing]
*** Exception: <interactive>:1:17-34: Non-exhaustive patterns in lambda

Что происходит?Этот частный случай является причиной одной из самых больших бородавок (IMO) в Haskell, метода Monad класса *1052*.В нотации do, когда не удается сопоставить шаблон, вызывается fail.Фактический перевод был бы ближе к:

g' :: Num b => [Maybe b] -> [b]
g' x = x >>= \x' -> case x' of
                      Just n -> [n+1]
                      _      -> fail "pattern match exception"

теперь у нас есть

Prelude> g' [Nothing]
[]

fail s полезность зависит от монады.Для списков это невероятно полезно, в основном заставляя сопоставление с образцом работать в списках.Это также очень хорошо в монаде Maybe, поскольку ошибка сопоставления с образцом может привести к неудачному вычислению, то есть именно тогда, когда Maybe должно быть Nothing.Для IO, возможно, не так много, поскольку он просто генерирует исключение пользовательской ошибки через error.

Вот и вся история.

4 голосов
/ 29 августа 2011

do -блоки формы var <- e1; e2 десугар в выражениях с использованием >>= следующим образом e1 >>= \var -> e2.Таким образом, ваш getPerson код становится:

getPerson =
    getLine >>= \name ->
    getInt  >>= \age ->
    return $ Just Person <*> Just name <*> age

Как вы видите, он не сильно отличается от кода, использующего do.

1 голос
/ 27 июля 2013

На самом деле, согласно этому объяснению , точный перевод вашего кода будет

getPerson = 
    let f1 name = 
                  let f2 age = return $ Just Person <*> Just name <*> age
                      f2 _ = fail "Invalid age"
                  in getInt >>= f2
        f1 _ = fail "Invalid name"
    in getLine >>= f1

getInt = 
    let f1 n = case n of
               ((x,""):[])   -> return (Just x)
               _ -> return Nothing
        f1 _ = fail "Invalid n"
    in (fmap reads getLine :: IO [(Int,String)]) >>= f1

И пример соответствия шаблона

f x = do
  Just n <- x
  [n+1]

переведен в

f x =
  let f1 Just n = [n+1]
      f1 _ = fail "Not Just n"
  in x >>= f1

Очевидно, что этот переведенный результат менее читаем, чем лямбда-версия, но он работает с или без сопоставления с образцом.

...