Изучив все эти ответы о том, как легко написать файл в Scala, и некоторые из них довольно хороши, у меня возникло три вопроса:
- В ответе Jus12 использование каррирования для использования вспомогательного метода не очевидно для начинающих Scala / FP
- Необходимо инкапсулировать ошибки более низкого уровня с
scala.util.Try
- Необходимо показать новичкам в Scala / FP разработчиков Java, как правильно вкладывать зависимые ресурсы, поэтому метод
close
выполняется для каждого зависимого ресурса в обратном порядке - Примечание: закрытие зависимые ресурсы в обратном порядке ОСОБЕННО В СЛУЧАЕ ОТКАЗА является редко понимаемым требованием спецификации java.lang.AutoCloseable
, которая имеет тенденцию приводить к очень губительным и трудным для поиска ошибок и времени выполнения. неудачи
Прежде чем начать, моя цель - не лаконичность. Это облегчает понимание новичкам в Scala / FP, обычно начинающим с Java. В самом конце я соберу все биты вместе, а затем увеличу краткость.
Во-первых, необходимо обновить метод using
, чтобы использовать Try
(опять же, краткость здесь не является целью). Он будет переименован в tryUsingAutoCloseable
:
def tryUsingAutoCloseable[A <: AutoCloseable, R]
(instantiateAutoCloseable: () => A) //parameter list 1
(transfer: A => scala.util.Try[R]) //parameter list 2
: scala.util.Try[R] =
Try(instantiateAutoCloseable())
.flatMap(
autoCloseable =>
try
transfer(autoCloseable)
finally
autoCloseable.close()
)
Начало описанного выше метода tryUsingAutoCloseable
может сбивать с толку, поскольку он, кажется, имеет два списка параметров вместо обычного списка с одним параметром. Это называется карри. И я не буду вдаваться в подробности, как работает карри или где это иногда полезно. Оказывается, для этой конкретной проблемной области это правильный инструмент для работы.
Далее нам нужно создать метод tryPrintToFile
, который создаст (или перезапишет существующий) File
и напишет List[String]
. Он использует FileWriter
, который инкапсулирован в BufferedWriter
, который в свою очередь инкапсулирован в PrintWriter
. А для повышения производительности определяется размер буфера по умолчанию, намного превышающий размер по умолчанию для BufferedWriter
, defaultBufferSize
и ему присваивается значение 65536.
Вот код (и опять же, краткость здесь не является целью):
val defaultBufferSize: Int = 65536
def tryPrintToFile(
lines: List[String],
location: java.io.File,
bufferSize: Int = defaultBufferSize
): scala.util.Try[Unit] = {
tryUsingAutoCloseable(() => new java.io.FileWriter(location)) { //this open brace is the start of the second curried parameter to the tryUsingAutoCloseable method
fileWriter =>
tryUsingAutoCloseable(() => new java.io.BufferedWriter(fileWriter, bufferSize)) { //this open brace is the start of the second curried parameter to the tryUsingAutoCloseable method
bufferedWriter =>
tryUsingAutoCloseable(() => new java.io.PrintWriter(bufferedWriter)) { //this open brace is the start of the second curried parameter to the tryUsingAutoCloseable method
printWriter =>
scala.util.Try(
lines.foreach(line => printWriter.println(line))
)
}
}
}
}
Приведенный выше метод tryPrintToFile
полезен тем, что он принимает List[String]
в качестве входных данных и отправляет его File
. Давайте теперь создадим метод tryWriteToFile
, который берет String
и записывает его в File
.
Вот код (и я позволю вам угадать приоритет краткости здесь):
def tryWriteToFile(
content: String,
location: java.io.File,
bufferSize: Int = defaultBufferSize
): scala.util.Try[Unit] = {
tryUsingAutoCloseable(() => new java.io.FileWriter(location)) { //this open brace is the start of the second curried parameter to the tryUsingAutoCloseable method
fileWriter =>
tryUsingAutoCloseable(() => new java.io.BufferedWriter(fileWriter, bufferSize)) { //this open brace is the start of the second curried parameter to the tryUsingAutoCloseable method
bufferedWriter =>
Try(bufferedWriter.write(content))
}
}
}
Наконец, полезно иметь возможность извлекать содержимое File
как String
. В то время как scala.io.Source
предоставляет удобный метод для простого получения содержимого File
, метод close
должен использоваться в Source
для освобождения базовой JVM и дескрипторов файловой системы. Если этого не сделать, то ресурс не будет освобожден, пока JCM GC (Сборщик мусора) не сможет освободить сам экземпляр Source
. И даже в этом случае слабая JVM гарантирует, что GC будет вызывать метод finalize
для ресурса close
. Это означает, что клиент несет ответственность за явный вызов метода close
, точно так же, как это обязанность клиента - получить close
в экземпляре java.lang.AutoCloseable
. Для этого нам понадобится второе определение метода using, который обрабатывает scala.io.Source
.
Вот код для этого (все еще не является кратким):
def tryUsingSource[S <: scala.io.Source, R]
(instantiateSource: () => S)
(transfer: S => scala.util.Try[R])
: scala.util.Try[R] =
Try(instantiateSource())
.flatMap(
source =>
try
transfer(source))
finally
source.close()
)
А вот пример использования его в супер простом считывателе потоковых файлов (в настоящее время используется для чтения файлов с разделителями табуляции из вывода базы данных):
def tryProcessSource(
file: java.io.File
, parseLine: (String, Int) => List[String] = (line, index) => List(line)
, filterLine: (List[String], Int) => Boolean = (values, index) => true
, retainValues: (List[String], Int) => List[String] = (values, index) => values
, isFirstLineNotHeader: Boolean = false
): scala.util.Try[List[List[String]]] =
tryUsingSource(scala.io.Source.fromFile(file)) {
source =>
scala.util.Try(
( for {
(line, index) <-
source.getLines().buffered.zipWithIndex
values =
parseLine(line, index)
if (index == 0 && !isFirstLineNotHeader) || filterLine(values, index)
retainedValues =
retainValues(values, index)
} yield retainedValues
).toList //must explicitly use toList due to the source.close which will
//occur immediately following execution of this anonymous function
)
)
обновленная версия вышеуказанной функции была предоставлена в качестве ответа на другой, но связанный вопрос StackOverflow .
Теперь, собрав все это вместе с извлеченным импортом (значительно упростив вставку в рабочую таблицу Scala, присутствующую как в плагине Eclipse ScalaIDE, так и в IntelliJ Scala, упростить вывод данных на рабочий стол, чтобы их было легче изучить с помощью текста. редактор), вот как выглядит код (с повышенной краткостью):
import scala.io.Source
import scala.util.Try
import java.io.{BufferedWriter, FileWriter, File, PrintWriter}
val defaultBufferSize: Int = 65536
def tryUsingAutoCloseable[A <: AutoCloseable, R]
(instantiateAutoCloseable: () => A)(transfer: A => scala.util.Try[R]): scala.util.Try[R] =
Try(instantiateAutoCloseable())
.flatMap(
autoCloseable =>
try transfer(autoCloseable)) finally autoCloseable.close()
)
def tryUsingSource[S <: scala.io.Source, R]
(instantiateSource: () => S)(transfer: S => scala.util.Try[R]): scala.util.Try[R] =
Try(instantiateSource())
.flatMap(
source =>
try transfer(source)) finally source.close()
)
def tryPrintToFile(
lines: List[String],
location: File,
bufferSize: Int = defaultBufferSize
): Try[Unit] =
tryUsingAutoCloseable(() => new FileWriter(location)) { fileWriter =>
tryUsingAutoCloseable(() => new BufferedWriter(fileWriter, bufferSize)) { bufferedWriter =>
tryUsingAutoCloseable(() => new PrintWriter(bufferedWriter)) { printWriter =>
Try(lines.foreach(line => printWriter.println(line)))
}
}
}
def tryWriteToFile(
content: String,
location: File,
bufferSize: Int = defaultBufferSize
): Try[Unit] =
tryUsingAutoCloseable(() => new FileWriter(location)) { fileWriter =>
tryUsingAutoCloseable(() => new BufferedWriter(fileWriter, bufferSize)) { bufferedWriter =>
Try(bufferedWriter.write(content))
}
}
def tryProcessSource(
file: File,
parseLine: (String, Int) => List[String] = (line, index) => List(line),
filterLine: (List[String], Int) => Boolean = (values, index) => true,
retainValues: (List[String], Int) => List[String] = (values, index) => values,
isFirstLineNotHeader: Boolean = false
): Try[List[List[String]]] =
tryUsingSource(Source.fromFile(file)) { source =>
Try(
( for {
(line, index) <- source.getLines().buffered.zipWithIndex
values = parseLine(line, index)
if (index == 0 && !isFirstLineNotHeader) || filterLine(values, index)
retainedValues = retainValues(values, index)
} yield retainedValues
).toList
)
)
Будучи новичком в Scala / FP, я потратил много часов (в основном на расчесывание головы), зарабатывая вышеупомянутые знания и решения.Надеюсь, это поможет другим новичкам в Scala / FP быстрее справиться с этой проблемой.