Как писать без Обозначения

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

getPerson :: IO (Maybe Person)

где Person:

data Person = Person String Int deriving Show

Он работает, и я написал его в do-notation следующим образом:

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, это похоже на то, что если бы у меня был более сложный тип данных со многими другими полями, вычисления цепочек не были бы сложными.

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

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

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

Ответ 1

Условные обозначения Do-notation для ( → =) синтаксиса следующим образом:

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 после первого преобразуется в лямбда, которая затем привязана к предыдущей строке. Это полностью механический процесс привязки значений к именам. Я не вижу, как использование 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, но есть и другие варианты. Трансформаторы Monad позволяют создавать новую монаду с комбинированным поведением лежащих в основе монадов. В этом случае вы используете функции с типом 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, который является просто a IO String. Это поднимается в монаду MaybeT с помощью функции lift.

Edit Комментарий 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 class fail. В do-notation, когда совпадение шаблона терпит неудачу 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 Полезность зависит от монады. Для списков это невероятно полезно, в основном, работа с шаблоном работает в понимании списков. Он также очень хорош в монаде Maybe, так как ошибка совпадения с образцом приведет к неудачному вычислению, что точно, когда Maybe должно быть Nothing. Для IO, возможно, не так много, поскольку он просто выдает исключение ошибки пользователя через error.

Это полная история.

Ответ 2

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

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

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

Ответ 3

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

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

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