В этом эпизоде ​​ моей саги о боевых действиях в Эрланге я расскажу о чем-то, что меня сильно беспокоит, но я понимаю, насколько все это может быть неуместным для большинства эрлангеров. Просто потому, что я придирчив к семантике. Но, в любом случае, вот моя напыщенная речь о строках Erlang ...

Множество строк Erlang

В большинстве языков есть один или, может быть, два типа / класса для управления строками. Но не Эрланг. В Erlang у нас есть…

Строки

1> String = “this is a string”.
“this is a string”
2> StringToo = [$t, $h, $i, $s, $,, $\s, $t, $o, $o].
“this, too”

Строки - это оригинальные строки, просто списки целых чисел с некоторым синтаксическим сахаром, которые красиво напечатаны как символы.

Двоичные файлы

3> Binary = <<”this is a binary”>>.
<<”this is a binary”>>
4> BinaryToo = <<$t, $h, $i, $s, $,, “ too”>>.
<<”this, too”>>

Двоичные файлы - это последовательности байтов. Для этого также есть синтаксический сахар, и вы можете их прочитать.

Списки IO

5> IOList = [“this”, <<” is “>>, [“an”, <<” IO “>>] | <<”List”>>].
[“this”,<<” is “>>,[“an”,<<” IO “>>]|<<”List”>>]
6> io:format(“~s~n”, [IOList]).
this is an IO List

Двоичные файлы и строки были хороши, но чаще всего вы обнаруживаете, что их много конвертируют и объединяют. Для оптимизации существует более удобный способ создания строк, которые могут интерпретироваться функциями в модуле io (и многих других): IOLists. IOLists - это (возможно, неправильные) списки строк, двоичных файлов, символов или IOList. Но, как вы можете видеть, поскольку списки ИОЛ являются списками, отдельный двоичный файл не является допустимым списком ИОЛ, хотя он может хорошо работать в тех же сценариях. Чтобы приспособиться к этому факту, у нас есть более широкий тип…

Данные ввода-вывода

7> IOData = [“this”, <<” is “>>, [“IO”] | <<” data”>>].
[“this”,<<” is “>>,[“IO”]|<<” data”>>]
8> IODataToo = <<”This is IOData, too”>>.
<<”This is IOData, too”>>
9> io:format(“~s~n”, [IOData]).
this is IO data
ok

IOData - это просто IOList или двоичный файл.

Тип Характеристики

Итак, у нас есть как минимум 4 разных типа строк, но, к счастью, каждая из них имеет свой собственный тип, определенный OTP. Если вы покопаетесь в документации Erlang, то обнаружите, что это все встроенные типы, определенные следующим образом

binary() :: <<_:_*8>>.
string() :: [char()].
iolist() :: maybe_improper_list( byte() | binary() | iolist()
                               , binary() | []
                               ).
iodata() :: iolist() | binary().

К бинарным файлам!

Теперь IOData (и, следовательно, все другие типы) хорошо работают с функциями io. Но иногда вам действительно нужен двоичный файл. На этом этапе вам нужно преобразовать все данные, которые у вас есть, в двоичную форму. И снова у вас есть несколько способов сделать это:

16> erlang:list_to_binary(String).
<<"this is a string">>
17> binary:list_to_bin(String).
<<"this is a string">>
18> erlang:iolist_to_binary(String).
<<"this is a string">>

Но если вы посмотрите спецификации этих функций, вы обнаружите что-то странное ...

% erlang.erl
-spec list_to_binary(IoList) -> binary() when
 IoList :: iolist().
-spec iolist_to_binary(IoListOrBinary) -> binary() when
 IoListOrBinary :: iolist() | binary().
% binary.erl
-spec list_to_bin(ByteList) -> binary() when
      ByteList :: iodata().

Итак, list _to_binary / 1 фактически преобразует iolist () в binary () и iolist _to_binary / 1 преобразует iodata () в binary () (не даже назвав его своим именем). И худший из всех нарушителей: list _to_bin / 1 преобразует iodata () в binary (). !!

Как я уже сказал, я считаю, что слишком разборчив в этих вещах, но может ли кто-нибудь обвинить новичков в замешательстве, когда они спрашивают «эй! как мне преобразовать эти йоданные в строку? », и они получают ответ типа « это очевидно! Вам просто нужно использовать двоичный файл: list_to_bin / 1 ”? И я даже не говорю о преобразовании йодата в строку! Для этого у вас нет другого выбора, кроме как сначала получить двоичный файл, а затем превратить этот двоичный файл в строку ... Да, тоже не заставляйте меня начинать с юникода.

Что тут происходит?

На самом деле это довольно просто объяснить. Если вам нужно подробное объяснение, вы можете проверить историю OTP в git, но, фактически не делая этого, я сделаю обоснованное предположение и предположу, что функции и типы не были определены в том порядке, в котором я их представил выше. Они выросли органически, и поэтому, например, когда был создан iodata (), iolist_to_binary / 1 уже принимал iolist () | binary () в качестве входных данных, и никто не изменил его спецификацию.

Что-то подобное должно было произойти с другими функциями, они изначально использовались, как указывает их название, но затем кому-то действительно потребовалось расширить поддержку других типов строк в качестве входных данных, но он не хотел возвращаться назад совместимость.

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

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