jifangege 2019-06-21
接触elixir时, 学会的第一行代码是 IO.puts "Hello world!"
. 出于好奇, 我观察了一下 IO.puts
函数的实现.
我们先到elixir源文件中的IO模块里看看.
IO.puts
的定义@doc """ Writes `item` to the given `device`, similar to `write/2`, but adds a newline at the end. """ @spec puts(device, chardata | String.Chars.t) :: :ok def puts(device \\ :stdio, item) do :io.put_chars map_dev(device), [to_chardata(item), ?\n] end
首先看它的类型, 这里我们需要先了解一点, 在erlang和elixir中, 字符串的类型是不同的.
比如 erlang 中 [97]
等同于 "a"
, 而 elixir 中 <<97>>
等同于 "a"
.
device
, chardata
的定义@type device :: atom | pid @type chardata() :: :unicode.chardata()
map_dev/1
, to_chardata/1
.内联函数的作用是在编译时, 调用内联函数的地方会被替换成该函数的函数体.
@compile {:inline, map_dev: 1, to_chardata: 1} # Map the Elixir names for standard IO and error to Erlang names defp map_dev(:stdio), do: :standard_io defp map_dev(:stderr), do: :standard_error defp map_dev(other) when is_atom(other) or is_pid(other) or is_tuple(other), do: other defp to_chardata(list) when is_list(list), do: list defp to_chardata(other), do: to_string(other)
to_string/1
函数的定义@doc """ Converts the argument to a string according to the `String.Chars` protocol. This is the function invoked when there is string interpolation. ## Examples iex> to_string(:foo) "foo" """ defmacro to_string(arg) do quote do: String.Chars.to_string(unquote(arg)) end
String.Chars
protocol 对不同类型的参数的实现import Kernel, except: [to_string: 1] defprotocol String.Chars do @moduledoc ~S""" The `String.Chars` protocol is responsible for converting a structure to a binary (only if applicable). The only function required to be implemented is `to_string` which does the conversion. The `to_string/1` function automatically imported by `Kernel` invokes this protocol. String interpolation also invokes `to_string` in its arguments. For example, `"foo#{bar}"` is the same as `"foo" <> to_string(bar)`. """ def to_string(term) end defimpl String.Chars, for: Atom do def to_string(nil) do "" end def to_string(atom) do Atom.to_string(atom) end end defimpl String.Chars, for: BitString do def to_string(term) when is_binary(term) do term end def to_string(term) do raise Protocol.UndefinedError, protocol: @protocol, value: term, description: "cannot convert a bitstring to a string" end end defimpl String.Chars, for: List do def to_string(charlist), do: List.to_string(charlist) end defimpl String.Chars, for: Integer do def to_string(term) do Integer.to_string(term) end end defimpl String.Chars, for: Float do def to_string(term) do IO.iodata_to_binary(:io_lib_format.fwrite_g(term)) end end
注意这里使用了一个神奇的 erlang 函数 -- :io_lib_format.fwrite_g
用来转换 Float, 我看了一下 erlang 的源代码, 发现了一段注释
%% Writes the shortest, correctly rounded string that converts %% to Float when read back with list_to_float/1. %% %% See also "Printing Floating-Point Numbers Quickly and Accurately" %% in Proceedings of the SIGPLAN '96 Conference on Programming %% Language Design and Implementation.
具体的实现挺复杂的, 有兴趣的朋友可以去看看 https://github.com/erlang/otp...
IO.iodata_to_binary/1
的定义@doc """ Converts iodata (a list of integers representing bytes, lists and binaries) into a binary. The operation is Unicode unsafe. Notice that this function treats lists of integers as raw bytes and does not perform any kind of encoding conversion. If you want to convert from a charlist to a string (UTF-8 encoded), please use `chardata_to_string/1` instead. If this function receives a binary, the same binary is returned. Inlined by the compiler. ## Examples iex> bin1 = <<1, 2, 3>> iex> bin2 = <<4, 5>> iex> bin3 = <<6>> iex> IO.iodata_to_binary([bin1, 1, [2, 3, bin2], 4 | bin3]) <<1, 2, 3, 1, 2, 3, 4, 5, 4, 6>> iex> bin = <<1, 2, 3>> iex> IO.iodata_to_binary(bin) <<1, 2, 3>> """ @spec iodata_to_binary(iodata) :: binary def iodata_to_binary(item) do :erlang.iolist_to_binary(item) end
内联函数的作用是在编译时, 调用内联函数的地方会被替换成该函数的函数体, 适用于短小的函数. 定义的内联函数的方法是使用模块属性 @compile {:inline, fun_a: 1, fun_b: 1}
.
使用 defprotocal
来定义协议. 使用 defimpl .., for: ..
来为某种数据类型实现协议.
quote do:
中间没有逗号