Почему для отрицательных аргументов определяются `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]