возвращая два разных типа из одной функции - PullRequest
2 голосов
/ 27 февраля 2012

Как я могу вернуть значения нескольких типов из одной функции?

Я хочу сделать что-то вроде:

take x y  | x == []   = "error : empty list"
          | x == y    = True
          | otherwise = False

У меня есть опыт в императивных языках.

Ответы [ 4 ]

9 голосов
/ 27 февраля 2012

Существует несколько решений вашей проблемы, в зависимости от вашего намерения: хотите ли вы указать в своем типе, что ваша функция может выйти из строя (и в этом случае вы хотите вернуть причину сбоя, которая может быть излишней) если есть только один режим сбоя, как здесь), или вы считаете, что получение пустого списка в этой функции вообще не должно происходить, и поэтому хотите немедленно завершиться с ошибкой и сгенерировать исключение?

Поэтому, если вы хотите явно указать возможность сбоя в вашем типе, вы можете использовать Maybe, чтобы просто указать сбой без объяснения причин (в конце концов, в вашей документации):

take :: (Eq a) => [a] -> [a] -> Maybe Bool
take [] _ = Nothing
take x y  = x == y

Или либо указать причину сбоя (обратите внимание, что в любом случае ответом будет «возврат двух типов из одной функции», хотя ваш код более конкретен):

take :: (Eq a) => [a] -> [a] -> Either String Bool
take [] _ = Left "Empty list"
take x y  = Right $ x == y

Наконец, вы можете сигнализировать, что этот сбой является совершенно ненормальным и не может быть обработан локально:

take :: (Eq a) => [a] -> [a] -> Bool
take [] _ = error "Empty list"
take x y  = x == y

Обратите внимание, что при этом последнем способе сайт вызова не должен немедленно обрабатывать сбой, на самом деле это не может, поскольку исключения могут быть перехвачены только в монаде ввода-вывода. Первыми двумя способами сайт вызова должен быть модифицирован для обработки случая сбоя (и может), если только для себя вызов «ошибки».

Существует одно окончательное решение, которое позволяет вызывающему коду выбирать, какой режим сбоя вы хотите (используя пакет сбоя http://hackage.haskell.org/package/failure):

take :: (Failure String m, Eq a) => [a] -> [a] -> m Bool
take [] _ = failure "Empty list"
take x y  = return $ x == y

Это может имитировать решение Maybe и Either, или вы можете использовать take в качестве IO Bool, который выдает исключение в случае сбоя. Он может даже работать в контексте [Bool] (в случае сбоя возвращает пустой список, что иногда полезно).

9 голосов
/ 27 февраля 2012

Существует конструктор типов с именем Either, который позволяет вам создать тип, который может быть одним из двух типов. Он часто используется для обработки ошибок, как в вашем примере. Вы бы использовали это так:

take x y | x == []   = Left "error : empty list"
         | x == y    = Right True
         | otherwise = Right False

Тогда тип take будет примерно таким: Eq a => [a] -> [a] -> Either String Bool. Соглашение с Either для обработки ошибок заключается в том, что Left представляет ошибку, а Right представляет нормальный тип возврата.

Если у вас есть тип Either, вы можете сопоставить шаблон с ним, чтобы увидеть, какое значение оно содержит:

case take x y of
  Left errorMessage -> ... -- handle error here
  Right result      -> ... -- do what you normally would
3 голосов
/ 27 февраля 2012

Вы можете использовать функции error для исключений:

take :: Eq a => [a] -> [a] -> Bool
take [] _ = error "empty list"
take x y = x == y
1 голос
/ 27 февраля 2012

Три ответа, которые вы получили до сих пор (от Тихона Джелвиса, Джедая и Филиппа), охватывают три общепринятых способа решения такой ситуации:

  • Использование функции error сигнализирует об ошибке. Однако это часто осуждается, потому что это затрудняет программы, использующие вашу функцию, для восстановления после ошибки.
  • Используйте Maybe, чтобы указать случай, когда невозможно получить Boolean ответ.
  • Используйте Either, который часто используется для того же действия, что и Maybe, но может дополнительно содержать дополнительную информацию о сбое (Left "error : empty list").

Я бы поддержал подходы Maybe и Either и добавил бы один лакомый кусочек (что немного сложнее, но, возможно, вы захотите в конце концов): и 10101 *, и Either a можно превратить в монады, и это может быть использовано для написания кода, который нейтрален между выбором между этими двумя. В этом блоге обсуждаются восемь различных способов решения вашей проблемы , включая три упомянутых выше, четвертый, использующий класс типа Monad для абстрагирования разницы между Maybe и Either, и еще четыре.

Запись в блоге с 2007 года, так что она выглядит несколько устаревшей, но мне удалось заставить # 4 работать следующим образом:

{-# LANGUAGE FlexibleInstances #-}

take :: (Monad m, Eq a) => [a] -> [a] -> m Bool
take x y  | x == []   = fail "error : empty list"
          | x == y    = return True
          | otherwise = return False

instance Monad (Either String) where
    return = Right
    (Left err) >>= _ = Left err
    (Right x) >>= f = f x
    fail err = Left err

Теперь эта take функция работает в обоих случаях:

*Main> Main.take [1..3] [1..3] :: Maybe Bool
Just True
*Main> Main.take [1] [1..3] :: Maybe Bool
Just False
*Main> Main.take [] [1..3] :: Maybe Bool
Nothing

*Main> Main.take [1..3] [1..3] :: Either String Bool
Right True
*Main> Main.take [1] [1..3] :: Either String Bool
Right False
*Main> Main.take [] [1..3] :: Either String Bool
Left "error : empty list"

Хотя важно отметить, что fail противоречив, поэтому я ожидаю разумных возражений против этого подхода. Использование fail здесь не является обязательным, однако его можно заменить любой функцией f :: (Monad m, ErrorClass m) => String -> m a, такой, что f err будет Nothing в Maybe и Left err в Either.

...