[elixir! #0023] 引擎盖下, `IO.puts` 如何运作

`IO.puts "Hello world!"

接触elixir时, 学会的第一行代码是 IO.puts "Hello world!". 出于好奇, 我观察了一下 IO.puts 函数的实现.

IO

我们先到elixir源文件中的IO模块里看看.

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)

Kernel

Kernel 模块中 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:中间没有逗号

相关推荐