Вход-изход
В тази статия ще се запознаем с функциите, свързани с четене и писане.
Ще разгледаме няколко модула от стандартната библиотека, като IO
и File
.
Изход с IO.puts/2
и IO.write/2
Досега ползвахме IO.puts/2
за да извеждаме текст на стандартния изход.
Може би се чудите защо написахме функцията като функция на два аргумента.
Това е защото тя е такава, просто първият, device
има стойност по
подразбиране. Засега няма да разглеждаме тази стойност.
Тя е свързана с комуникацията между процеси.
Това което ще направим е да подадем други стойности. Примери:
IO.puts("По подразбиране пишем на стандартния изход.")
IO.puts(:stdio, "Можем да го направим и така.")
IO.puts(:stderr, "Или да пишем в стандартния изход за грешки.")
Всъщност puts
се държи по същия начин като друга функция в IO
- write
.
Разликата е, че puts
слага нов ред след текста, който му е подаден.
IO.write(:stderr, "Това е грешка!")
Какво е chardata
Както казахме, първият аргумент на write
и puts
е device
.
Вторият е нещо от тип chardata
.
Какво е chardata
?
В статията Списъци и потоци споменахме за iolist
, по-познат като iodata
в Elixir
.
Този тип е доста подобен.
Chardata
е:
- Низ, да речем
"далия"
. - Списък от
codepoint
-и, да речем[83, 79, 0x53]
или[?S, ?O, ?S]
или'SOS'
. - Списък от
codepoint
-и и низове -[83, 79, 83, "mayday!"]
. - Списък от
chardata
, тоест списък от нещата в горните три точки :[[83], [79, ["dir", 78]]]
.
Подавайки chardata
на функции като IO.puts/2
и IO.write/2
, можем да избегнем конкатенация на
низове, което е винаги хубаво нещо. Така няма копиране в паметта, data-та се изпраща направо в целта си.
Показахме ви как да изграждате HTML
темплейти с този тип данни.
Като цяло това е едно от чудесата на Erlang/Elixir
, ползвайте го вместо конкатенирани или интерполирани низове, когато можете.
В IO
има функция, която трансформира chardata
в низ.
IO.chardata_to_string([1049, [1086, 1091], "!"])
# "Йоу!"
Вход с IO.read/2
, IO.gets/2
, IO.getn/2
и IO.getn/3
Функцията read
също взима device
като първи аргумент (което е или атом, да речем :stdio
или PID
на процес).
Вторият аргумент може да бъде:
- Атомът
:all
- значи да се изчете всичко идващо отdevice
-а, докато не се достигнеEOF
, тогава се връща празен низ. - Атомът
:line
- прочита се всичко до нов ред илиEOF
. ПриEOF
, функцията връща:eof
. - Цяло число, по голямо от нула - прочита толкова символа от
device
или колкото може преди да достигнеEOF
.
Функцията връща прочетеното:
iex> IO.read(:line)
Хей, Хей<enter>
# "Хей, Хей\n"
Много подобна е и функцията IO.gets/2
. Тя приема prompt
като втори аргумент и чете до нов ред:
iex> IO.gets("Кажи нещо!\n")
Кажи нещо!
Нещо!<enter>
# "Нещо!\n"
Двете getn
функции прочитат брой байтове или unicode codepoint
-и, в зависимост от типа на device
-а.
Когато говорим за файлове ще разгледаме как можем да отворим файл в различни mode
-ове.
Какво е iodata
Подобно на chardata
, iodata
може да се дефинира като списък от data
.
За разлика от chardata
, iodata
списъкът е от цели числа които представляват байтове (0 - 255),
binary
с елементи със size
, кратен на 8 (могат да превъртат) и такива списъци.
Има функции, които боравят с iodata
- IO.binwrite
и IO.binread
.
Тези функции са по-бързи от не-bin*
вариантите им.
Не трансформират това което получават в utf8
.
В IO
има две функции за боравене с iodata
:
IO.iodata_length([1, 2|<<3, 4>>])
# 4
Връща дължината в байтове на iodata
-та.
IO.iodata_to_binary([1, << 2 >>, [[3], 4]])
# <<1, 2, 3, 4>>
Трансформира iodata
в binary
.
Файлове
Модулът File
съдържа функции за работа с файлове. Някои от тях ни позволяват да отваряме
файловете за писане и четене. По подразбиране всички файлове се отварят в binary mode
и
функциите IO.binwrite/2
и IO.binread/2
трябва да се използват за работа с тях.
Разбира се файл може да бъде отворен и в utf8 mode
. По този начин байтовете, записани или прочетени,
ще се интерпретират като UTF8
codepoint-и.
{:ok, file} = File.open("test.txt", [:write])
# {:ok, #PID<0.855.0>}
IO.binwrite(file, "some text!")
File.close(file)
Както можем да видим, File.open/2
връща наредена двойка - {:ok, device}
.
Ако имаше някаква грешка щяхме да получим {:error, reason}
.
Това е нормално при повечето функции свързани с файлове. Разбира се има и функции,
които хвърлят грешка при проблем и връщат резултата направо. Те имат същите имена, но завършващи на !
.
Да речем File.open!/2
.
В модула има много функции за създаване и триене на файлове и директории, за проверки дали съществуват, за промяна и показване на съдържанието на директория. Прочетете за тях в документацията.
Процеси и файлове
Така нареченият device
всъщност е PID
на процес или атом, който сочи към PID
на процес.
По принцип, когато отваряме файл се създава нов процес, който знае file descriptor
-а на файла
и управлява писането и четенето към и от него.
Това е много хубаво нещо. От една страна това означава, че IO
функциите на един node
,
могат да четат/пишат файл на друг node
, или един компютър да управлява файлове на друг.
От друга, означава че можем да си създаваме лесно свои device
-и, чрез процеси, които
знаят какво съобщение да очакват.
Разбира се това означава и, че всяка операция с файла минава през комуникация между процеси. Когато искаме оптимално писане/четене на файл, това не е плюс.
Именно за това има функции, които направо работят с файлове, като File.read/1
, File.read!/1
,
File.write/3
, File.write!/3
.
Тези функции отварят файла и пишат/четат в/от него като една операция, след това го затварят.
Потоци и файлове
Ако не искаме да прочетем цял файл в паметта, можем да го отворим и да си направим поток към него:
{:ok, file} = File.open("program.txt", [:read])
# {:ok, #PID<0.82.0>}
IO.stream(file, :line)
|> Stream.map(fn line -> line <> "!" end)
|> Stream.each(fn line -> IO.puts(line) end)
|> Stream.run
Това ще прочете файла ред по ред, трансформирайки редовете и ще ги изведе на стандартния изход.
Разбира се IO.stream
има и IO.binstream
версия.
Ако искаме по бързо четене/писане, без преминаване през комуникация между процеси, ползваме
File.stream!
:
File.stream!(filename, read_ahead: 10_000)
По подразбиране, когато използваме File.stream!
, файловете се отварят в raw binary read_ahead mode
.
Това ще рече, че няма трансформация към UTF8 codepoint
-и има буфериране в паметта. В примера по горе,
показваме как можем да зададем големина на буфера.
Ако искаме наистина бързо четене от файл на части, трансформиране и записване в друг файл е добре да следваме следния шаблон:
File.stream!(<input_name>, read_ahead: <buffer_size>)
|> Stream.<transform-or-filter>
...
|> Stream.into(File.stream!(<output_name>, [:delayed_write]))
|> Stream.run
По този начин комбинирайки read_ahead
и delayed_write
се получава буфериране с
добра скорост. Повече по темата тук.
Модула IO.ANSI
Този модул съдържа функции които контролират цвета, и форматирането в теминала.
Много добре се комбинират в chardata
списък с текст:
IO.puts [IO.ANSI.blue(), "text", IO.ANSI.reset()]
# Ще отпечата 'text' в синьо, ако терминалът ви поддържа ANSI цветове
Модула StringIO и файлове в паметта
Използвайки този модул, ние можем да четем/пишем от/в низове в паметта:
{:ok, pid} = StringIO.open("data")
#PID<0.136.0>}
StringIO.contents(pid)
# {"data", ""}
IO.write(pid, "doom!")
#:ok
StringIO.contents(pid)
# {"data", "doom!"}
IO.read(pid, :line)
# "data"
StringIO.contents(pid)
# {"", "doom!"}
StringIO.close(pid)
# {:ok, {"", "doom!"}}
Както виждаме в паметта се държат два низа - един за вход, един за изход. Можем да четем от изхода, докато стане празен и да пишем във входа.
Това не е точно поведението при един истински файл, за който нямаме две пространства, а само едно.
Ако искаме псевдо-файл в паметта, който се държи като истински файл, можем да го направим така:
File.open("data", [:ram])
# {:ok, {:file_descriptor, :ram_file, #Port<0.1578>}}
IO.binread(file, :all)
# "data"
Опцията при отваряне ram
, създава файл в паметта със съдържание първия аргумент на функцията open
.
Ако сега направим:
IO.binread(file, :all)
# ""
Ще получим празен низ. Това е защото сме в края на файла, можем да променим това, с Erlang
функцията :file.postion/2
.
:file.position(file, :bof)
IO.binread(file, :all)
# "data"
Така отиваме в :bof
- beginning of file
и четем. В Elixir
няма random access
функции, но могат да се ползват
тези от erlang
.
Модула Path
Много от функциите във File
изискват пътища. Модулът Path
, ни предоставя спомагателни функции за работа с пътища.
Примери:
Path.join("some", "path")
# "some/path"
Path.expand("~/development")
# "/home/meddle/development"
По добре е да си строим пътищата с функции от Path
.
Те се справят с различията в операционните системи - знаят на какво вървят.
Заключение
Това беше всичко от нас за файловете. Тази статия е последната преди навлизането в
процесите и по advanced
темите.
Научихме за структурите от данни в Elixir
, типовете, controw flow
конструкциите, грешките, работата с файлове.
Време е да разберем по какъв начин работят процесите в които върви кодът ни и как си комуникират те.
Това ще направим в следващата статия.