Возвращение нулевого значения в Haskell без сжимания полиморфизма

Я следую "Real World Haskell", и я перехожу к главе 2, где упражнение - создать функцию "lastButOne", которая возвращает второе в последнее значение в списке. Мой исходный код:

lastButOne xs =     if null (tail (tail xs))
                    then head xs
                    else lastButOne (tail xs)

Это работает отлично, если я не дам ему список только с 1 или 2 пунктами. Поэтому я изменил это на следующее:

lastButOne xs = if null xs || null (tail xs)
                then head xs
                else
                        if null (tail (tail xs))
                        then head xs
                        else lastButOne (tail xs)

Что проверяет, имеет ли он только 1 или 2 элемента, но затем выходит из строя, если у него только 1 элемент из-за вызова в голову. Моя проблема в том, что я не могу придумать ничего другого, кроме как "head xs", в идеале я хочу, чтобы он возвращал значение null или что-то в этом роде, но я не могу найти "нуль", который позволяет функции по-прежнему полиморфный.

Ответ 1

Возможно, вы ищете тип Maybe:

lastButOne :: [a] -> Maybe a
lastButOne [x,_] = Just x
lastButOne (_:y:ys) = lastButOne (y:ys)
lastButOne _ = Nothing

Ответ 2

[I] честно, я хочу, чтобы он возвращал значение null или что-то в этом роде, но я не могу найти "null", который позволяет функции по-прежнему быть полиморфной.

Уже есть ряд ответов, посвященных тому, как это сделать, поэтому вместо этого позвольте мне ответить на более фундаментальный вопрос: "Почему нет" нулевого "?"

Это часть философии проверки типов в Haskell. большинство языков включают некоторую форму специального значения, которое сигнализирует об ошибке или отсутствии данных. Примеры включают 0, -1 "" (пустая строка) JavaScripts null или Ruby nil. Проблема в том, что это создает целый класс потенциальных ошибок, в которых вызывающая функция имеет дело только с "хорошими" значениями, а сбой программы или ухудшает повреждение данных при возвращении специального значения. Существует также вопрос о том, что особое значение предназначено как регулярное значение, то есть 0 на самом деле является числом, а не состоянием отказа.

В вашей проблеме может быть задан вопрос: "Будет ли функция, вызывающая lastButOne знать, что делать с null?"

Проблема отсутствия правильного ответа для некоторых хорошо сформированных входов является очень распространенной в Haskell и имеет два типичных решения. Первое взято из Erlang: быстро и чисто. В рамках этого решения абоненту необходимо обеспечить, чтобы данные были "хорошими". Если это не программа, она умирает с ошибкой (или исключение поймано, но это сложнее в Haskell, затем на других языках). Это решение, используемое большинством функций списка, например. head, tail и т.д.

Другое решение состоит в том, чтобы явно указать, что функция может не создавать хорошие данные. Тип Maybe a является самым простым из этих решений. Он позволяет вам вернуть значение Nothing, которое работает так же, как null, которое вы ищете. Но для этого есть затраты. Ваша подпись типа функции становится lastButOne :: [a] -> Maybe a. Теперь каждая вызывающая функция должна принимать не a, а Maybe a, и прежде чем она сможет получить ответ, она должна сказать Haskell, что он собирается делать, если ответ Nothing.

Ответ 3

Ну, это зависит от того, сколько ошибок вы действительно хотите сделать.

Я подозреваю, что, учитывая это только в главе 2, вы, вероятно, должны игнорировать этот случай края. Сам Haskell выдает исключение:

Prelude> head []
*** Exception: Prelude.head: empty list

Альтернативой, если это случай, когда вам придется иметь дело с большим количеством, было бы изменить тип возврата lastButOne на Maybe a и вернуть Just (head xs), когда он будет работать, и Nothing, когда он не работает. Но это означает, что вы всегда будете иметь дело с проживанием внутри Maybe monad, что может быть не идеальным (так как вам придется "развернуть" Just значение для использования).

Ответ 4

Почему вы не используете сопоставление шаблонов для обработки случая, когда у вас есть только 1 или 2 элемента? Что-то вроде:

lastButOne :: [a] -> Maybe a
lastButOne [x,y] = Just x
lastButOne (x:y:t) = lastButOne (y:t)
lastButOne  _ = Nothing

Ответ 5

Примером "расширенной обработки ошибок", упомянутой @Andrew Jaffe, может быть:

{-# LANGUAGE FlexibleContexts #-} module Main where

import Control.Monad.Error

lastButOne :: MonadError String m => [a] -> m a
lastButOne (_ : y : []) = return y 
lastButOne (_ : t) = lastButOne
lastButOne _ = throwError "Error in lastButOne"

В этом случае можно вернуть Either тип: Right x в случае успеха и Left "Error in lastButOne" в случае сбоя, но возможны многие другие экземпляры MonadError String m.

Также читайте 8 способов сообщить об ошибках в Haskell для получения дополнительных возможностей.