Как бы вы написали чистую функцию, удаляющую элемент из списка?

На Java (или на любом подобном языке) как бы вы написали чистую функцию (или метод), которая удаляет элемент из списка.

Если элемент находится в списке, мы просто возвращаем новый (в идеале неизменяемый) список, содержащий все элементы входного списка за вычетом элемента, который мы удалили.

Но как бы вы поступили в случае, если элемент не найден в списке?

Предположим, что метод получает 2 параметра, list и element, которые нужно удалить:

public SOMETHING remove(final List<String> list, final String element){
    // Copy the input list and remove the first occurrence of 'element', if I find it.
    // 
    // if I don't find it ... do something clever here ...
}

Если я вызываю этот метод, а element не содержится внутри list:

  • Вызов исключения предположительно сделает метод «нечистым» (?)
  • Изменение списка ввода и возврат логического значения (аналогично List # remove ()) предположительно сделает метод" нечистым "(изменение ввода будет побочным эффектом)
  • Если бы я вызывал этот метод, мне показалось бы, что возвращать входные данные в качестве выходных данных не интуитивно.
  • вернуть Optional.of(listCopy) (РЕДАКТИРОВАТЬ: добавлен в мой вопрос после его публикации)
  • Есть другие идеи?

РЕДАКТИРОВАТЬ

Я должен был упомянуть, что я хочу удалить только первое вхождение element, поэтому, если вход list имеет несколько вхождений element, я не хочу удалять их все с помощью sigle-вызова моего метода remove () (например, используя stream().filter() ). Я только что отредактировал комментарий в моем примере кода, чтобы отразить это. Однако это не полностью относится к моему вопросу, так как мой главный вопрос вращается вокруг того, как сделать метод интуитивно понятным для использования И оставаться «чистым».

РЕДАКТИРОВАТЬ 2

Я добавил дополнительную идею к приведенным выше предложениям. Возврат Optional.of(listCopy) кажется самым элегантным решением из предложенных до сих пор. Он заставляет вызывающего пользователя проверить, была ли запрошенная операция успешной или нет, И (если она была успешной), он возвращает новый список, тем самым не изменяя исходный список ввода. Если операция завершилась неудачно (element не было найдено внутри list), метод возвращает Optional.empty(). Мне кажется, это также обеспечит указанную ниже ссылочную целостность.


person Chris    schedule 09.08.2019    source источник
comment
Есть аналогичный вопрос с ответами: ссылка   -  person Andrzej Sydor    schedule 09.08.2019
comment
Его возвращаемое значение одинаково для тех же аргументов - вы бы интерпретировали это как тот же тип, ссылку на тот же объект или ту же ссылку, которая была передана? Я уверен, что любой выбор можно оспорить.   -  person Jacob G.    schedule 09.08.2019
comment
@AndrzejSydor, спасибо за предложения, но я совсем не об этом!   -  person Chris    schedule 09.08.2019
comment
@JacobG. Я думаю, что единственным чистым программным решением было бы вернуть новый список того же типа, что и входной список. Снова ошибка, как нам справиться со случаем, когда удаляемый элемент не найден в списке ...   -  person Chris    schedule 09.08.2019


Ответы (5)


Проходя по пунктам, о которых вы упомянули:

  1. Throwing an exception would presumably make method "impure" (?)
    • This is an implementation decision, if you want to return the same list or throw an exception. as for 'pure function' - as long as you're consistent and the same input yields the same result (or exception), you're good
  2. Modifying the input list and returning a boolean (similar to List#remove()) would presumable make the method "impure" (modifying the input would be a side-effect)
    • this will make it non pure functional, as you'll have a side effect
  3. Returning the input as output would seem unintuitive to me if I was calling this method.
    • don't see any problem with that actually. again, your call if it's a valid scenario or not. in terms of 'pure function', you're ok here

Что касается реализации, я думаю, что поток является самым простым и возвращает новый список, так что с точки зрения чистоты все в порядке

return list.stream()
 .filter(e -> !e.equals(element))
 .collect(Collectors.toList());
person Nir Levy    schedule 09.08.2019

Как насчет:

public List<String> remove(final List<String> list, final String element) {
    List<String> result = new ArrayList<String>(list);
    result.remove(element);
    return result;
}

Из Википедии он должен:

  • имеют одинаковый возврат для одних и тех же входов - не вижу никаких причин, по которым это было бы неверно здесь, без случайных чисел, без ввода-вывода и т. д.
  • быть без состояния - в этой функции обрабатываются только параметры. Может также сделать это static

ИЗМЕНИТЬ (только что видел комментарий о возвращении нового списка каждый раз)

Если вы пытаетесь создать код функционального стиля на Java, то тот факт, что это новый список, вызывает проблемы. Вероятно, вы имеете в виду свойство под названием ссылочная прозрачность. Подводя итог, можно сказать, что значение имеет ссылочную прозрачность, если его можно заменить ссылкой на себя, и наоборот, без изменения семантики программы. Java на самом деле не поддерживает это, и это одна из причин, по которой Java не работает по-настоящему.

В Haskell (почти) все выражения имеют ссылочную прозрачность, то есть список [1, 2, 3, 4] не просто эквивалентен другому списку [1, 2, 3, 4], это то же самое. Это похоже на то, как в математике 7 это то же самое, что 7, не имеет смысла говорить, что "что 7 не то же 7, что у меня в этом уравнении".

Возвращаясь к Java, вы требуете, чтобы последующие вызовы возвращали тот же результат. Поэтому вам решать, что вы подразумеваете под словом «то же самое». В функциональном мире это обычно определяется значениями (т. Е. Использованием Arrays.equals()). Однако, если вы хотите использовать == (т.е. 2 списка равны тогда и только тогда, когда они занимают одно и то же место в памяти), функциональный стиль, вероятно, не то, что вам нужно.

Надеюсь, что это проясняет ситуацию.

person cameron1024    schedule 09.08.2019
comment
Это не возвращает одно и то же значение для определенного ввода, поскольку каждый раз создается другой ArrayList, независимо от его содержимого. - person Jacob G.; 09.08.2019
comment
@JacobG, если вам требуется ссылочное равенство, это просто невозможно. Вам нужно сохранить состояние, чтобы сопоставить входы с предыдущими выходами. Кроме того, это также будет означать, что если вы добавляете / удаляете элементы в / из списка ввода между вызовами, вы волшебным образом возвращаете тот же результат, что и раньше, но с исправленным содержанием. - person Zastai; 09.08.2019

Любая мутация существующего списка не соответствует «Свойству 1» согласно Википедии.

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

Ваша единственная реальная надежда здесь - вернуть новый List со всеми скопированными элементами, кроме того, который вы хотите удалить.

public List<String> remove(final List<String> list, final String element) {
    return list.stream()
                   .filter(e -> !Objects.equals(e, element))
                   .collect(Collectors.toList());
}

Это также обрабатывает сценарий «элемент не в списке», поскольку вы получаете новый экземпляр списка с теми же элементами, что и старый. Фактически, если состояние ваших данных не меняется, тогда нет никакого реального вреда в возврате того же состояния.

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

person Makoto    schedule 09.08.2019

Вы можете создать новый List и пройти через входной массив, добавляя каждый элемент, который не был element, а затем вернуть новый список

person Marc Sloth Eastman    schedule 09.08.2019
comment
Как бы это удовлетворить. Его возвращаемое значение одинаково для тех же аргументов, если бы вы возвращали новый List каждый раз, независимо от того, содержат ли они одни и те же элементы? - person Jacob G.; 09.08.2019
comment
Поскольку список, который вы возвращаете, имеет те же значения, если вы вызываете его с одними и теми же аргументами несколько раз - person Marc Sloth Eastman; 09.08.2019
comment
Да, но remove(list, element) == remove(list, element) вернет false. - person Jacob G.; 09.08.2019
comment
Вы должны использовать функцию List equals docs.oracle.com/ javase / 7 / docs / api / java / util / List.html - person Marc Sloth Eastman; 09.08.2019
comment
Я согласен с @MarcSlothEastman в том счастливом случае, когда удаляемый element фактически содержится внутри входного списка. Но опять же, что нам делать, если элемент не находится внутри входного списка? - person Chris; 09.08.2019
comment
Не имеет значения, лично я бы вернул новый, чтобы упростить функцию @Chris - person Marc Sloth Eastman; 09.08.2019

Например, используя ListIterator:

ListIterator<Book> iter = books.listIterator();
while(iter.hasNext()){
    if(iter.next().getIsbn().equals(isbn)){
        iter.remove();
    }
}
person Andrzej Sydor    schedule 09.08.2019
comment
Нет :-) (если вы измените исходный список, это уже не чистая функция) - person Chris; 09.08.2019