我正在学习Elixir作为我的第一个功能风格的语言。作为第一个熟悉环境和语法的简单项目,我选择构建一个简单的程序来计算命令行中提供的数字的素因子。这是我的第一个解决方案:
defmodule Prime do
defp is_factor?(number, divisor) do
cond do
rem(number, divisor) == 0 -> divisor
true -> nil
end
end
defp not_nil?(thing) do
!is_nil(thing)
end
def factors(number) when number == 1 do
[]
end
def factors(number) do
1..div(number, 2)
|> Enum.map(&(is_factor?(number, &1)))
|> Enum.filter(¬_nil?/1)
end
def is_prime?(number) when number == 1 do
true
end
def is_prime?(number) do
factors(number) == [1]
end
def prime_factors(number) do
factors(number)
|> Enum.filter(&is_prime?/1)
end
end
input = hd(System.argv)
number = String.strip(input) |> String.to_integer
IO.puts "Prime factors of #{number} are #{inspect Prime.prime_factors(number)}"
它有效,但运行得相当慢。在我的笔记本电脑上,运行时间约为11秒,以计算50,000,000的素因子。
正如我读到的更多,似乎这个原始解决方案并不像Elixir那样。所以我将代码重组为:
defmodule PrimeFactors do
def of(n) do
_factors(n, div(n, 2))
end
defp _factors(_n, 1) do
[1]
end
defp _factors(n, divisor) when rem(n, divisor) == 0 do
cond do
is_prime?(divisor) -> _factors(n, divisor - 1) ++ [divisor]
true -> _factors(n, divisor - 1)
end
end
defp _factors(n, divisor) do
_factors(n, divisor - 1)
end
defp is_prime?(1) do
true
end
defp is_prime?(n) do
of(n) == [1]
end
end
input = hd(System.argv)
number = String.strip(input) |> String.to_integer
IO.puts "Prime factors of #{number} are #{inspect PrimeFactors.of(number)}"
此代码计算50,000,000的素因子的典型运行时间差得多:超过17秒。
我在Swift和Ruby中构建了相同的程序。优化的Swift运行时间超过0.5秒,Ruby(2.2,从未以速度着称)运行时间超过6秒。
我的主要问题是:如何构建Elixir代码以使其更具惯用性并避免出现我遇到的性能问题?
我还留下了一些问题,如果遇到这样一个简单的问题,就可以写出效率差别很大的Elixir代码。也许这主要是我对功能样式的缺乏经验?
答案 0 :(得分:12)
让我先快速咆哮,然后我们将转向答案。我相信我们在这里担心错误的事情。一旦你发布了Ruby代码,我首先想到的是:为什么Elixir代码看起来不像Ruby那样干净?
让我们先解决这个问题:
defmodule PrimeFactors do
def of(n) do
factors(n, div(n, 2)) |> Enum.filter(&is_prime?/1)
end
def factors(1, _), do: [1]
def factors(_, 1), do: [1]
def factors(n, i) do
if rem(n, i) == 0 do
[i|factors(n, i-1)]
else
factors(n, i-1)
end
end
def is_prime?(n) do
factors(n, div(n, 2)) == [1]
end
end
IO.inspect PrimeFactors.of(50_000_000)
好多了。让我们运行这个更清洁的版本?在我的机器上3.5秒(相比之前的24秒)。
现在使用更清晰的代码,可以更轻松地比较实施中的错误。您的_factors
函数实际上是_factors_and_prime
,因为您已经在检查该数字是否为素数。因此,当您检查is_prime?
时,您实际上正在计算“因子和素数”,其计算成本比实际“因子”高得多,因为它最终再次以递归方式调用is_prime?
。
某人在某处某地说:
:)
答案 1 :(得分:8)
优化工作在一秒钟内:
defmodule PF do
@doc "Calculates the unique prime factors of a number"
def of(num) do
prime_factors(num)
|> Enum.uniq
end
@doc """
Calculates all prime factors of a number by finding a low factor
and then recursively calculating the factors of the high factor.
Skips all evens except 2.
Could be further optimized by only using known primes to find factors.
"""
def prime_factors(num , next \\ 2)
def prime_factors(num, 2) do
cond do
rem(num, 2) == 0 -> [2 | prime_factors(div(num, 2))]
4 > num -> [num]
true -> prime_factors(num, 3)
end
end
def prime_factors(num, next) do
cond do
rem(num, next) == 0 -> [next | prime_factors(div(num, next))]
next + next > num -> [num]
true -> prime_factors(num, next + 2)
end
end
end
奖金,测试:
ExUnit.start
defmodule PFTest do
use ExUnit.Case
test "prime factors are correct" do
numbers = [4, 15, 22, 100, 1000, 2398, 293487,
32409850, 95810934857, 50_000_000]
Enum.map(numbers, fn (num) ->
assert num == Enum.reduce(PF.prime_factors(num), &*/2)
end)
end
end
我们最终通过减少问题域来编写更有文化/惯用的灵药。可以实现进一步的优化,但可能在没有显着性能增益的情况下丧失可读性。此外,随着文档和测试内置到平台中,包括它们是无痛的并且使代码更具可读性。 :)