【问题标题】:Optimize or remove the usage of a reduce_while call in Elixir优化或删除 Elixir 中 reduce_while 调用的使用
【发布时间】:2017-08-14 17:16:21
【问题描述】:

我正在关注最近关于 Compilers 的 Destroy All Software 截屏视频。所有示例代码都是用 Ruby 编写的,但我正在尝试使用 Elixir 实现相同的输出。

简单来说,我的方法是这样的:

  1. 将一串代码传递给递归函数 (call_tokenizer)
  2. 遍历标记类型,并使用当前标记类型运行正则表达式
  3. 如果找到匹配项,则将该结果附加到累加器删除 从字符串开头的结果,并传递剩余的 字符串和累加器到call_tokenizer。此外,:halt `reduce_while 循环,并返回累积的令牌。
  4. 如果没有找到结果,继续循环

我在 Elixir 中编写了几个工作的函数,因为我可以从我的输入中获得预期的输出。但是,我认为我的代码至少存在一个问题。

我正在使用Enum.reduce_while,但我不认为我正在使用它,因为它打算被使用。我的假设是有一些更好的方法可以将reduce_while 重写为递归函数。

如果我需要在一个问题中总结,那将是 如何在不依赖 reduce_while 给我的暂停/继续属性的情况下获得相同的结果?我应该注意此代码示例的其他问题吗?

这是我的代码和预期输出:

// Code
defmodule Compiler.Token do
  defstruct [:type, :value]
end

defmodule Compiler.Tokenizer do
  @token_types [
    {:def, "\\bdef\\b"},
    {:end, "\\bend\\b"},
    {:identifier, "\\b[a-zA-Z]+\\b"},
    {:integer, "\\b[0-9]+\\b"},
    {:oparen, "\\("},
    {:cparen, "\\)"}
  ]

  def tokenize() do
    code = "def f()
      1
    end"

    IO.inspect call_tokenize(code, [])
  end

  def call_tokenize("", accumulator) do
    accumulator
  end

  def call_tokenize(code, accumulator) do
    Enum.reduce_while(@token_types, "", fn {type, re}, acc ->
      result = Regex.run(~r/\A#{re}/, code)

      if result do
        value = hd(result)
        base = byte_size(value)
        token = %Compiler.Token{type: type, value: value}
        tokens = binary_part(code, base, byte_size(code) - base)
          |> String.trim()
          |> call_tokenize(accumulator ++ [token])
        {:halt, tokens}
      else
        {:cont, acc}
      end
    end)
  end
end


// Expected output
[%Compiler.Token{type: :def, value: "def"},
 %Compiler.Token{type: :identifier, value: "f"},
 %Compiler.Token{type: :oparen, value: "("},
 %Compiler.Token{type: :cparen, value: ")"},
 %Compiler.Token{type: :integer, value: "1"},
 %Compiler.Token{type: :end, value: "end"}]    

【问题讨论】:

    标签: elixir


    【解决方案1】:

    我看到您已经想出了如何在答案中用显式递归替换 reduce_while。这是一种更惯用的方法,您将在 Elixir 和 Erlang 中看到大多数手写标记器使用。这种方法比基于正则表达式的标记器要快得多,并且还允许添加纯正则表达式的标记器无法添加的逻辑(尽管在这种情况下您不需要它)。

    这是带有一些内联 cmets 的代码:

    defmodule Compiler.Tokenizer.Dogbert do
      def tokenize(code), do: tokenize(code, [])
    
      # We're done. Reverse the tokens since we collected them in reverse order.
      defp tokenize("", acc), do: Enum.reverse(acc)
      # Remove leading whitespace.
      defp tokenize(<<h, rest::binary>>, acc) when h in ' \t\r\n', do: tokenize(rest, acc)
      # Identifier
      defp tokenize(binary = <<h, _::binary>>, acc) when h in ?a..?z do
        {value, rest} = take_while(binary, fn b -> b in ?a..?z end)
        type = case value do
          "def" -> :def
          "end" -> :end
          _ -> :identifier
        end
        tokenize(rest, [%Compiler.Token{type: type, value: value} | acc])
      end
      # Number
      defp tokenize(binary = <<h, _::binary>>, acc) when h in ?0..?9 do
        {value, rest} = take_while(binary, fn b -> b in ?0..?9 end)
        tokenize(rest, [%Compiler.Token{type: :integer, value: value} | acc])
      end
      # (
      defp tokenize("(" <> rest, acc), do: tokenize(rest, [%Compiler.Token{type: :oparen, value: "("} | acc])
      # )
      defp tokenize(")" <> rest, acc), do: tokenize(rest, [%Compiler.Token{type: :cparen, value: ")"} | acc])
    
      # A simple helper function that extracts the leading part of the binary as long as `fun` returns `true` when called with a byte, starting from the first byte of the binary. It returns the extracted binary and the remaining binary.
      # We use indexes to track the position for efficiency.
      # Using accumulators like for lists can be inefficient for binaries since we have to allocate memory which can be avoided if we deal with byte offsets and do a `binary:part` at the end.
      defp take_while(binary, fun), do: take_while(binary, fun, 0)
      defp take_while(binary, fun, byte) do
        if byte < byte_size(binary) && fun.(:binary.at(binary, byte)) do
          take_while(binary, fun, byte + 1)
        else
          <<value::binary-size(byte), rest::binary>> = binary
          {value, rest}
        end
      end
    end
    

    测试:

    code = "def f()
      1
    end"
    
    IO.inspect Compiler.Tokenizer.Dogbert.tokenize(code)
    

    输出:

    [%Compiler.Token{type: :def, value: "def"},
     %Compiler.Token{type: :identifier, value: "f"},
     %Compiler.Token{type: :oparen, value: "("},
     %Compiler.Token{type: :cparen, value: ")"},
     %Compiler.Token{type: :integer, value: "1"},
     %Compiler.Token{type: :end, value: "end"}]
    

    这是使用benchee 将您的实现与我的实现进行比较的基准。您的实现有一些容易解决的低效问题(例如不在每次运行时都构建正则表达式并避免++),但我希望基于正则表达式的方法总是比我使用的方法慢。

    defmodule Compiler.Token do
      defstruct [:type, :value]
    end
    
    defmodule Compiler.Tokenizer.Dogbert do
      def tokenize(code), do: tokenize(code, [])
    
      # We're done. Reverse the tokens since we collected them in reverse order.
      defp tokenize("", acc), do: Enum.reverse(acc)
      # Remove leading whitespace.
      defp tokenize(<<h, rest::binary>>, acc) when h in ' \t\r\n', do: tokenize(rest, acc)
      # Identifier
      defp tokenize(binary = <<h, _::binary>>, acc) when h in ?a..?z do
        {value, rest} = take_while(binary, fn b -> b in ?a..?z end)
        type = case value do
          "def" -> :def
          "end" -> :end
          _ -> :identifier
        end
        tokenize(rest, [%Compiler.Token{type: type, value: value} | acc])
      end
      # Number
      defp tokenize(binary = <<h, _::binary>>, acc) when h in ?0..?9 do
        {value, rest} = take_while(binary, fn b -> b in ?0..?9 end)
        tokenize(rest, [%Compiler.Token{type: :integer, value: value} | acc])
      end
      # (
      defp tokenize("(" <> rest, acc), do: tokenize(rest, [%Compiler.Token{type: :oparen, value: "("} | acc])
      # )
      defp tokenize(")" <> rest, acc), do: tokenize(rest, [%Compiler.Token{type: :cparen, value: ")"} | acc])
    
      # A simple helper function that extracts the leading part of the binary as long as `fun` returns `true` when called with a byte, starting from the first byte of the binary. It returns the extracted binary and the remaining binary.
      # We use indexes to track the position for efficiency.
      # Using accumulators like for lists can be inefficient for binaries since we have to allocate memory which can be avoided if we deal with byte offsets and do a `binary:part` at the end.
      defp take_while(binary, fun), do: take_while(binary, fun, 0)
      defp take_while(binary, fun, byte) do
        if byte < byte_size(binary) && fun.(:binary.at(binary, byte)) do
          take_while(binary, fun, byte + 1)
        else
          <<value::binary-size(byte), rest::binary>> = binary
          {value, rest}
        end
      end
    end
    
    defmodule Compiler.Tokenizer.PaulRuescher do
      @token_types [
        {:def, "\\bdef\\b"},
        {:end, "\\bend\\b"},
        {:identifier, "\\b[a-zA-Z]+\\b"},
        {:integer, "\\b[0-9]+\\b"},
        {:oparen, "\\("},
        {:cparen, "\\)"}
      ]
    
      def tokenize(code_string) do
        call_tokenize(code_string, [])
      end
    
      def call_tokenize("", accumulator) do
        accumulator
      end
    
      def call_tokenize(code_string, accumulator) do
        {type, value} = attempt_tokenize(@token_types, code_string)
        base = byte_size(value)
        token = %Compiler.Token{type: type, value: value}
        binary_part(code_string, base, byte_size(code_string) - base)
          |> String.trim()
          |> call_tokenize(accumulator ++ [token])
      end
    
      def attempt_tokenize(token_types, code_string, index \\ 0) do
        {type, re} = Enum.at(token_types, index)
    
        case Regex.run(~r/\A#{re}/, code_string) do
          nil -> attempt_tokenize(token_types, code_string, index + 1)
          value -> {type, hd(value)}
        end
      end
    end
    
    code = String.duplicate("def f()
      1
    end", 1000)
    
    IO.inspect Compiler.Tokenizer.PaulRuescher.tokenize(code) == Compiler.Tokenizer.Dogbert.tokenize(code)
    
    Benchee.run(%{
      "@paulruescher" => fn -> Compiler.Tokenizer.PaulRuescher.tokenize(code) end,
      "@Dogbert" => fn -> Compiler.Tokenizer.Dogbert.tokenize(code) end,
    })
    

    结果:

    true
    ...
    
    Name                    ips        average  deviation         median
    @Dogbert             442.18        2.26 ms    ±17.03%        2.43 ms
    @paulruescher         11.78       84.92 ms     ±8.37%       83.67 ms
    
    Comparison:
    @Dogbert             442.18
    @paulruescher         11.78 - 37.55x slower
    

    【讨论】:

    • 啊,是的,所以带守卫的模式匹配几乎总是要走的路?
    • 对于编写分词器,是的。 Erlang 中还包含一个名为leex 的lex 端口。如果您的分词器可以用leex 表示,您可能想尝试一下。这是一个完全不使用正则表达式的手动编码标记器示例,您可能想阅读一次以了解一些常见模式:github.com/elixir-lang/elixir/blob/master/lib/eex/lib/eex/…。
    【解决方案2】:

    我已经重构了我之前的代码示例,并且对此已经很满意了。我可以看到一些边缘情况,例如标记器从不匹配正则表达式,但我认为我现在可以接受。

    defmodule Compiler.Token do
      defstruct [:type, :value]
    end
    
    defmodule Compiler.Tokenizer do
      @token_types [
        {:def, "\\bdef\\b"},
        {:end, "\\bend\\b"},
        {:identifier, "\\b[a-zA-Z]+\\b"},
        {:integer, "\\b[0-9]+\\b"},
        {:oparen, "\\("},
        {:cparen, "\\)"}
      ]
    
      def tokenize(code_string) do
        call_tokenize(code_string, [])
      end
    
      def call_tokenize("", accumulator) do
        accumulator
      end
    
      def call_tokenize(code_string, accumulator) do
        {type, value} = attempt_tokenize(@token_types, code_string)
        base = byte_size(value)
        token = %Compiler.Token{type: type, value: value}
        binary_part(code_string, base, byte_size(code_string) - base)
          |> String.trim()
          |> call_tokenize(accumulator ++ [token])
      end
    
      def attempt_tokenize(token_types, code_string, index \\ 0) do
        {type, re} = Enum.at(token_types, index)
    
        case Regex.run(~r/\A#{re}/, code_string) do
          nil -> attempt_tokenize(token_types, code_string, index + 1)
          value -> {type, hd(value)}
        end
      end
    end
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2022-12-12
      • 1970-01-01
      • 2011-06-23
      • 1970-01-01
      • 2012-04-16
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多