Разделение строк в clojure при чтении из файла
Я изучаю clojure в школе, и у меня есть экзамен. Я просто работал над несколькими вещами, чтобы убедиться, что я понял это.
Я пытаюсь читать из файла строку за строкой, и, как и я, я хочу разделить строку всякий раз, когда есть ";".
Вот мой код до сих пор
(defn readFile []
(map (fn [line] (clojure.string/split line #";"))
(with-open [rdr (reader "C:/Users/Rohil/Documents/work.txt.txt")]
(doseq [line (line-seq rdr)]
(clojure.string/split line #";")
(println line)))))
Когда я делаю это, я все еще получаю вывод:
"I;Am;A;String;"
Я что-то пропустил?
3 ответа
Я не уверен, нужно ли вам это в школе, но так как Гэри уже дал превосходный ответ, считайте это бонусом.
Вы можете выполнять элегантные преобразования в строках текста с помощью преобразователей. Ингредиент, который вам нужен, - это то, что позволяет вам рассматривать строки как сокращаемую коллекцию и закрывает читателя, когда вы закончите сокращать:
(defn lines-reducible [^BufferedReader rdr]
(reify clojure.lang.IReduceInit
(reduce [this f init]
(try
(loop [state init]
(if (reduced? state)
@state
(if-let [line (.readLine rdr)]
(recur (f state line))
state)))
(finally
(.close rdr))))))
Теперь вы можете сделать следующее, учитывая ввод work.txt
:
I;am;a;string
Next;line;please
Подсчитайте длину каждого "разделения"
(require '[clojure.string :as str])
(require '[clojure.java.io :as io])
(into []
(comp
(mapcat #(str/split % #";"))
(map count))
(lines-reducible (io/reader "/tmp/work.txt")))
;;=> [1 2 1 6 4 4 6]
Суммируйте длину всех "разбиений"
(transduce
(comp
(mapcat #(str/split % #";"))
(map count))
+
(lines-reducible (io/reader "/tmp/work.txt")))
;;=> 24
Суммируйте длину всех слов, пока мы не найдем слово длиннее 5
(transduce
(comp
(mapcat #(str/split % #";"))
(map count))
(fn
([] 0)
([sum] sum)
([sum l]
(if (> l 5)
(reduced sum)
(+ sum l))))
(lines-reducible (io/reader "/tmp/work.txt")))
или с take-while
:
(transduce
(comp
(mapcat #(str/split % #";"))
(map count)
(take-while #(> 5 %)))
+
(lines-reducible (io/reader "/tmp/work.txt")))
Прочитайте https://tech.grammarly.com/blog/building-etl-pipelines-with-clojure для получения более подробной информации.
TL;DR охватывают REPL и охватывают неизменность
Ваш вопрос был "что я пропускаю?" и на это я бы сказал, что вам не хватает одной из лучших функций Clojure - REPL.
Изменить: вы также можете упустить, что Clojure использует неизменяемые структуры данных, так
рассмотрите этот фрагмент кода:
(doseq [x [1 2 3]]
(inc x)
(prn x))
Этот код не печатает "2 3 4"
он печатает "1 2 3", потому что x не является изменяемой переменной.
Во время первой итерации (inc x)
вызывается, возвращает 2, и это выбрасывается, потому что оно не было передано ни в чем, затем (prn x)
печатает значение x, которое по-прежнему равно 1.
Теперь рассмотрим этот фрагмент кода:
(doseq [x [1 2 3]] (prn (inc x)))
Во время первой итерации inc передает свое возвращаемое значение в prn, так что вы получаете 2
Длинный пример:
Я не хочу лишать вас возможности решить проблему самостоятельно, поэтому я буду использовать другую проблему в качестве примера.
Учитывая файл "birds.txt"
с данными "1chicken\n 2duck\n 3Larry"
Вы хотите написать функцию, которая принимает файл и возвращает последовательность имен птиц
Давайте разберем эту проблему на более мелкие куски:
сначала давайте прочитаем файл и разделим его на строки
(slurp "birds.txt")
даст нам весь файл строку
clojure.string/split-lines
даст нам коллекцию с каждой строкой в качестве элемента в коллекции
(clojure.string/split-lines (slurp "birds.txt"))
получает нас ["1chicken" "2duck" "3Larry"]
В этот момент мы можем отобразить какую-то функцию над этой коллекцией, чтобы вырезать число как (map #(clojure.string/replace % #"\d" "") birds-collection)
или мы можем просто переместить этот шаг вверх по конвейеру, когда весь файл представляет собой одну строку.
Теперь, когда у нас есть все наши части, мы можем собрать их вместе в функциональный конвейер, где результат одного фрагмента передается в следующий.
В Clojure есть хороший макрос, чтобы сделать его более читабельным, ->
макрос
Он берет результат одного вычисления и вводит его в качестве первого аргумента для следующего
так что наш конвейер выглядит так:
(-> "C:/birds.txt"
slurp
(clojure.string/replace #"\d" "")
clojure.string/split-lines)
Последнее замечание о стиле, для функций Clojure вы хотите придерживаться кебаб случае так readFile
должно быть read-file
Я хотел бы сохранить это простым и кодировать это так:
(ns tst.demo.core
(:use tupelo.test)
(:require [tupelo.core :as t]
[clojure.string :as str] ))
(def text
"I;am;a;line;
This;is;another;one
Followed;by;this;")
(def tmp-file-name "/tmp/lines.txt")
(dotest
(spit tmp-file-name text) ; write it to a tmp file
(let [lines (str/split-lines (slurp tmp-file-name))
result (for [line lines]
(for [word (str/split line #";")]
(str/trim word)))
result-flat (flatten result)]
(is= result
[["I" "am" "a" "line"]
["This" "is" "another" "one"]
["Followed" "by" "this"]])
Заметить, что result
является двухслойной (2D) матрицей слов. Самый простой способ отменить это flatten
функция для производства result-flat
:
(is= result-flat
["I" "am" "a" "line" "This" "is" "another" "one" "Followed" "by" "this"])))
Вы также можете использовать apply concat
как в:
(is= (apply concat result) result-flat)
Если вы хотите избежать создания 2D-матрицы, вы можете использовать generator function
(а-ля Питон) через lazy-gen
а также yield
из библиотеки Тупело:
(dotest
(spit tmp-file-name text) ; write it to a tmp file
(let [lines (str/split-lines (slurp tmp-file-name))
result (t/lazy-gen
(doseq [line lines]
(let [words (str/split line #";")]
(doseq [word words]
(t/yield (str/trim word))))))]
(is= result
["I" "am" "a" "line" "This" "is" "another" "one" "Followed" "by" "this"])))
В этом случае, lazy-gen
создает функцию генератора. Заметить, что for
был заменен на doseq
и yield
Функция помещает каждое слово в ленивую последовательность вывода.