Почему для отрицательных аргументов определяются `take` и` drop`?
Prelude показывает примеры для take
и drop
с отрицательными аргументами:
take (-1) [1,2] == []
drop (-1) [1,2] == [1,2]
Почему они определены так, как они есть, когда, например, x !! (-1)
делает "безопасную" вещь и падает? Это похоже на хакерский и очень не-Haskell-подобный способ сделать эти функции полными, даже если аргумент не имеет смысла. Есть ли еще одна философия дизайна, которую я не вижу? Является ли это поведение гарантированным стандартом, или именно так GHC решил его реализовать?
Ответы
Ответ 1
В основном было бы одна из причин сделать частичное: take
: он может гарантировать, что в списке результатов, если таковой имеется, всегда есть запрошенное количество элементов.
Теперь take
уже нарушает это в другом направлении: когда вы пытаетесь взять больше элементов, чем есть в списке, просто требуется столько, сколько есть, то есть меньше запрошенных. Возможно, это не самая элегантная вещь, но на практике это имеет тенденцию развиваться весьма полезной.
Основной инвариант для take
объединяется с drop
:
take n xs ++ drop n xs ≡ xs
и это верно, даже если n
отрицательно.
Хорошей причиной для того, чтобы не проверять длину списка, является то, что он отлично выполняет функции в ленивых бесконечных списках: например,
take hugeNum [1..] ++ 0 : drop hugeNum [1..]
немедленно даст 1
как первый элемент результата. Это было бы невозможно, если take
и drop
сначала должны были проверить, достаточно ли элементов на входе.
Ответ 2
Я думаю, что это вопрос выбора дизайна здесь.
Текущее определение гарантирует, что свойство
take x list ++ drop x list == list
выполняется для любого x
, включая отрицательные, а также те, которые больше, чем length list
.
Однако я могу видеть значение в варианте take
/drop
, который вызывает ошибки: иногда сбой является неправильным результатом.
Ответ 3
x !! (-1)
делает "безопасную" вещь и сбой
Сбой не безопасен. Выполнение функции без тотального уничтожения вашей способности
рассуждать о поведении функции, основанной на ее типе.
Представим себе, что take
и drop
имели "крушение по отрицательному" поведению. Рассмотрим их тип:
take, drop :: Int -> [a] -> [a]
Одна вещь этого типа определенно не говорит вам, что эта функция может сработать! Это полезно для определения кода, как если бы мы использовали общий язык, хотя мы и не являемся - идеей, называемой быстрой и свободной аргументацией, - но для уметь это делать, вам следует избегать использования (и написания) неточных функций как можно больше.
Что делать, тогда о действиях, которые могут потерпеть неудачу или не иметь результата? Типы - это ответ! По-настоящему безопасный вариант (!!)
будет иметь тип, который моделирует случай сбоя, например:
safeIndex :: [a] -> Int -> Maybe a
Это предпочтительнее типа (!!)
,
(!!) :: [a] -> Int -> a
Который, по простому наблюдению, не может иметь (совокупных) жителей - вы не можете "придумать" a
, если список пуст!
Наконец, вернемся к take
и drop
. Хотя их тип не в полной мере говорит, что такое поведение, в сочетании с их именами (и в идеале несколькими свойствами QuickCheck) мы получаем довольно хорошую идею. Как отмечали другие респонденты, это поведение во многих случаях подходит. Если у вас действительно есть необходимость отклонить входы отрицательной длины, вам не нужно выбирать между нетотальной (сбой) или возможностью неожиданного поведения (принятая отрицательная длина) - моделировать возможные результаты ответственно с типами.
Этот тип дает понять, что "нет результата"
для некоторых входов:
takePos, dropPos :: Int -> [a] -> Maybe [a]
Более того, этот тип использует натуральные числа;
функции с этим типом даже не могут быть
применяется к отрицательному числу!
takeNat, dropNat :: Nat -> [a] -> [a]