I'm learning Elixir as my first functional-style language. As a first simple project to familiarize myself with the environment and syntax, I chose to build a simple program that computes the prime factors for a number provided on the command line. This is my first solution:
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)}"
It works, but runs rather slowly. On my laptop, run times are around 11 seconds to compute the prime factors of 50,000,000.
As I read more, it seems like this original solution is not very Elixir-like. So I restructured the code to this:
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)}"
Typical run time of this code to compute the prime factors of 50,000,000 is substantially worse: over 17 seconds.
I built equivalent programs in Swift and Ruby. Optimized Swift runs in just over 0.5 seconds, and Ruby (2.2, and never known for its speed) runs in a bit over 6 seconds.
My primary question is: How should the Elixir code be structured to be more idiomatic and to avoid the performance problems I'm seeing?
I'm also left with some concerns that given such a simple problem, it's possible to write Elixir code that varies wildly in efficiency. Perhaps this is mostly my inexperience in functional styles showing?
++operator when you want to add an element to a list. Elixir lists are made of cons cells, so adding an element to the end of a list should be a O(n) operation while adding it to the head of the list is O(1). A common pattern is to prepend elements to a list ([new_el|list]) and useEnum.reverse/1to reverse the list before using it. - whatyouhidetimemeasures Erlang VM startup time which may affect the result. I really don't think this is a good example though, because once you optimize the code algorithmically, the difference may become insignificant. - sasajuric