Лучший способ считывания содержимого файла в набор в Clojure

#clojure

#clojure

Вопрос:

Я изучаю Clojure и в качестве упражнения хотел написать что-то вроде команды unix «comm».

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

После долгого времени воспроизведения я придумал что-то вроде этого для части создания набора:

 (def contents (ref #{}))
(doseq [line (read-lines "/tmp/a.txt")]
  (dosync (ref-set contents (conj @contents line))))
  

(Я использую duck-streams / read-lines для сортировки содержимого файла).

Это мой первый опыт в любом виде функционального программирования или lisp / Clojure. Например, я не мог понять, почему, когда я выполнял conj для набора, набор все еще был пуст. Это привело меня к изучению ссылок.

  1. Есть ли лучший Clojure / функциональный способ сделать это? Используя ref-set, я просто искажаю код для нефункционального мышления или мой код соответствует тому, как это должно быть сделано?
  2. Есть ли библиотека, которая уже делает это? Это кажется относительно обычным делом, но я не смог найти ничего подобного.

Комментарии:

1. Ответ Брайана Карпера хорош. Избегайте использования утиных потоков. Он устарел, большая часть его функциональности сведена к clojure.core и clojure.java.io .

2. О вашем комментарии о том, что набор «все еще был пуст»; похоже, вы ожидаете изменяемого поведения. Помните, что в clojure типы данных неизменяемы. Создание коллекций выполняется рекурсивно, отсюда и использование reduce , как показано Брайаном Карпером ( into используется reduce внутри).

3. @DaveRay Я не знал о состоянии утиных потоков. Спасибо за информацию.

4. @AlexStoddard Да, это была именно моя ошибка. Одно дело читать о неизменяемости, совсем другое — действительно понимать это.

Ответ №1:

Clojure 1.3:

 user> (require '[clojure.java [io :as io]])
nil
user> (line-seq (io/reader "foo.txt"))
("foo" "bar" "baz")
user> (into #{} (line-seq (io/reader "foo.txt")))
#{"foo" "bar" "baz"}
  

line-seq дает вам ленивую последовательность, где каждый элемент в последовательности является строкой в файле.

into выгружает все это в набор. Чтобы сделать то, что вы пытались сделать (добавить каждый элемент по одному в набор), а не doseq и ссылки, вы могли бы сделать:

 user> (reduce conj #{} (line-seq (io/reader "foo.txt")))
#{"foo" "bar" "baz"}
  

Обратите внимание, что Unix comm сравнивает два отсортированных файла, что, вероятно, является более эффективным способом сравнения файлов, чем пересечение наборов.

Редактировать: Дэйв Рэй прав, чтобы избежать утечки дескрипторов открытых файлов, лучше сделать это:

 user> (with-open [f (io/reader "foo.txt")]
        (into #{} (line-seq f)))
#{"foo" "bar" "baz"}
  

Комментарии:

1. @BrianCarper Спасибо, это действительно полезно. Использование line-seq кажется более естественным. Я видел, что reduce используется только для суммирования значений в последовательности или чего-то подобного, поэтому интересно увидеть, как он используется для создания другой коллекции.

2. Вы можете использовать функцию set вместо (into #{} ...

Ответ №2:

Я всегда читаю с slurp и после этого разделяю с re-seq из-за моих потребностей.