#scala #functional-programming #state
#скала #функциональное программирование #состояние
Вопрос:
Я хочу организовать свою программу вокруг неизменяемого состояния (используя класс case) и цепочки функций, которые его обновляют.
case class State(conn: Connection, file: FileInputStream, data: Data)
Первые две функции в этой цепочке просто открывают соединение и входной поток, а остальные используют эти ресурсы. Итак, это мои варианты, как я их вижу:
- Использование нулей при инициализации состояния:
State(conn = null, file = null, data = new Data())
.pipe(getConnection)
.pipe(openStream)
.pipe(processData)
.pipe(doCleanUp)
Где состояние обновляется в подобных функциях:
val newConn = ???
st.copy(conn = newConn)
Таким образом, код остается относительно чистым, но если порядок функций будет изменен в будущем, я столкнусь с ужасным NullPointerException.
- Объявление conn и file в качестве параметров и перенос состояния в любой из них.
case class State(conn: Option[Connection], file: Option[FileInputStream], data: Data)
Right(State(conn = null, file = null, data = new Data()))
.pipe(...
При этом проблема с nulls элегантно решается, но все функции, которые имеют дело с данными, должны реализовывать большую часть шаблонного кода, чтобы развернуть параметры.
{
case Left(ex) => Left(ex)
case Right(st) if st.conn.isEmpty => Left(new Exception("Connection is missing"))
case Right(st) if st.file.isEmpty => Left(new Exception("File stream is missing"))
case Right(st) =>
...
}
Результирующий код является более надежным и функциональным, но такой шаблон в каждой функции немного утомителен. Есть ли более идиоматичный и гибкий подход к этой проблеме?
Комментарии:
1. Входной поток является изменяемым, поэтому вы ничего не выигрываете, перенося его в класс case. Если вы хотите правильно следовать этому стилю, я бы порекомендовал вам взглянуть на потоковые реализации, такие как fs2 , AkkaStreams , Monix и т. Д.
Ответ №1:
Я думаю, вы переоцениваете это… на самом деле это не состояние, а всего лишь пара параметров (состояние — это то, что мутирует во время процесса, здесь это не так).
Почему бы просто не сделать параметры явными:
def openConnection(): Connection = ???
def openStream(c: Connection): InputStream = ???
def processData(stream: InputStream, data: Data): Unit = ???
def cleanup(c: Connection): Unit
Таким образом, вам не нужны уродливые нули, и никто не сможет вызвать эти
функции неправильно или в неправильном порядке по ошибке.
Вы можете сделать его более красивым / причудливым, если обернете возвращаемые значения в некоторую монаду ( Option
Future
возможно, или, возможно Try
, или Either
для обработки ошибок):
def openConnection(): Future[Connection] = ???
def openStream(c: Connection): Future[InputStream] = ???
def processData(stream: InputStream, data: Data): Future[Unit] = ???
def cleanup(stream: InputStream): Unit
Теперь вы можете написать это как
val result: Future[Unit] = openConnection()
.flatMap(openStream)
.flatMap { stream =>
val f = processData(stream, data)
.onComplete(cleanup(stream)
f // it's too bad `.onComplete` doesn't return `this` would make things like this look a lot nicer :(
}
Ответ №2:
Вы должны использовать .map
вместо .pipe
при работе с эффектами, такими как Option
или Either
. Есть много хороших руководств и примеров эффектов и того, как с ними обращаться. Мне действительно нравится подход Скотта Влашина к обучению. Вы можете найти его на YouTube здесь.
В вашем примере вы можете сделать что-то вроде этого:
Right(State(conn = None, file = None data = new Data()))
.map(getConnection)
.map(openStream)
.map(...)
.map
запускает функцию для a State
, если это a Right
, и игнорирует ее с помощью a Left
.
Кстати, вы действительно никогда не должны использовать null
в Scala. Используется None
для представления отсутствующего значения.