Fl4m3Ph03n1x

Fl4m3Ph03n1x

Product of consecutive Fib numbers - how to cache?

Background

In my never ending quest to learn Elixir I am now doing another Codewars exercise. The description is as follows:

Given a number, say prod (for product), we search two Fibonacci numbers F(n) and F(n+1) verifying

F(n) * F(n+1) = prod.

The real challenge

This is one of my favorites, taking me back to the University days. As any seasoned developer knows, Fibonnaci exercises are mainly used for two things:

  1. Introducing recursivity
  2. Forcing you to think about code optimization

In this specific case, it is the second. The code is supposed to pass tests with the following parameters:

ProdFib.product_fib(2563195080744681828)

Naive implementation

The first, and most naive, way to solve this exercise is to simply do it without a cache:

Spoiler
    defmodule ProdFib do
      def product_fib(n) do
        product_fib(0, 1, n)
      end

      defp product_fib(f1, f2, prod) do
        fib_1 = fibonacci(f1)
        fib_2 = fibonacci(f2)
        cond do
          fib_1 * fib_2 == prod  -> [fib_1, fib_2, true]
          fib_1 * fib_2 > prod   -> [fib_1, fib_2, false]
          true -> product_fib(f1+1, f2+1, prod)
        end
      end

      defp fibonacci(0), do: 0
      defp fibonacci(1), do: 1
      defp fibonacci(n) do
          fibonacci(n-1) + fibonacci(n-2)
      end

    end

Now this code works. But you will eventually start to have timeouts because you are repeating a lot of computations you don’t need.

How to improve it?

We add a cache !

The Bad cache

So my first approach was to use module attributes to save a map which would work as a cache:

Spoiler
    defmodule ProdFib do
      @fib_table %{}

      def product_fib(n) do
        product_fib(0, 1, n)
      end

      defp product_fib(f1, f2, prod) do
        fib_1 = fibonacci(f1)
        fib_2 = fibonacci(f2)
        cond do
          fib_1 * fib_2 == prod  -> [fib_1, fib_2, true]
          fib_1 * fib_2 > prod   -> [fib_1, fib_2, false]
          true -> product_fib(f1+1, f2+1, prod)
        end
      end

      defp fibonacci(0), do: 0
      defp fibonacci(1), do: 1
      defp fibonacci(n) do
        case Map.get(@fib_table, n) do
          nil ->
            val = fibonacci(n-1) + fibonacci(n-2)
            Map.put(@fib_table, n, val)
            val
          res -> res
        end
      end

    end

This does NOT work. After checking the documentation for module attributes I understand that module attributes are defined at compile time, and thus cannot not mutate at runtime:

However, I still needed a cache…

The Ugly cache

So I have been reading about ETS and I thought that since I need a global cache for this exercise, and since one of ETS’s main uses is to cache things, that it would fit perfectly !

Spoiler
    defmodule ProdFib do

      def product_fib(n) do
        try do
          :ets.new(:fibo_cache, [:protected, :named_table, :set])
          product_fib(0, 1, n)
        catch
          _, _ -> product_fib(0, 1, n)
        end
      end

      defp product_fib(f1, f2, prod) do
        fib_1 = fibonacci(f1)
        fib_2 = fibonacci(f2)
        cond do
          fib_1 * fib_2 == prod  -> [fib_1, fib_2, true]
          fib_1 * fib_2 > prod   -> [fib_1, fib_2, false]
          true -> product_fib(f1+1, f2+1, prod)
        end
      end

      defp fibonacci(0), do: 0
      defp fibonacci(1), do: 1
      defp fibonacci(n) do

        case :ets.lookup(:fibo_cache, n) do
          [] ->
            val = fibonacci(n-1) + fibonacci(n-2)
            :ets.insert(:fibo_cache, {n, val})
            val
          [{_key, val}] -> val
        end

      end

    end

So, this code works. It passes all tests and never times out. But it is really ugly.

Problems with Ugly cache

  1. I am using a try/catch for control flow. We don’t use errors for control flow.
  2. This example is not really testable. I am using a global ETS table as a cache. Globals are evil. Adding to this point, there is a chance that some of you may think ETS to be overkill for this. And to be honest I wouldn’t have any arguments to prove you wrong.

I need a Good cache

So, the solution to the previous problem would be:

  1. Don’t use errors for control flow. Just don’t.
  2. Instead of relying in a global table for cache, one could create an empty map, and pass it around as state. The problem with this approach is that I failed miserably with it. Hence, the usage of ETS.

This works and all but …

But I was wondering if my example could be improved?
I am pretty much sure someone here will have ideas on how to solve issue 1 ( remove control flow via errors ) and as far as issue 2 goes perhaps there is a different approach I am not yet familiar with.

Any ideas?

Marked As Solved

peerreynders

peerreynders

No it isn’t, that is body recursive:

  • first fibonacci(n-1) is calculated.
  • second fibonacci(n-2) is calculated
  • both values added together once they are available and then finally that value is returned.

This is tail recursive:

defp fibonacci(_, acc, i, n) when i == n,
  do: acc
defp fibonacci(fi_2, fi_1, i, n) when i < n,
  do: fibonacci(fi_1, fi_2 + fi_1, i + 1, n)

def fibonacci(0),
  do: 0
def fibonacci(1),
  do: 1
def fibonacci(n) when n > 1 and is_integer(n),
  do: fibonacci(fibonacci(0), fibonacci(1), 1, n)
  • there is no need to put the values on the stack because it can be simply replaced with the result from the last function call.

Ultimately the proposed solution looks very similar to this.

Also Liked

NobbZ

NobbZ

I’ve read through the exercise again and now realise that my function won’t help you (that) much. Have to do to much in the office right now to solve that one for myself :frowning:

Looks like a fun task…


edit

As everyone went to dinner early and I have an appointement for dinner in an hour, I took the time to solve it.

Using a streamified version of my algorithm from above I finished the task in less than 5 minutes and a handfull of lines.

Sign in | Codewars

NobbZ

NobbZ

No, as I expect the stream of fibonacci numbers potentially infinite. You can’t do this with eagerly evaluated Enum. And always remember, a Stream is just a lazy evaluated Enum.

I could have done this using explicit recursion and propably that would have been more efficient, but hey, it had to fit in the time of a smoking break and I didn’t want to think much :wink: The streaming version was already in my head.

OvermindDL1

OvermindDL1

Instead of caching, why not just make sure that the optimal algorithm is not already fast enough:

defmodule ProductFib do
  def run(n) when n>=0 do
    {a, b} = fib_(n)
    a
  end

  def fib_(0), do: {0, 1}
  def fib_(n) do
    {a, b} = fib_(div(n, 2))
    c = a * (b * 2 - a)
    d = a * a + b * b
    cond do
      rem(n, 2) == 0 -> {c, d}
      true -> {d, c + d}
    end
  end
end

Getting the value of 1 million takes:

iex(2)> :timer.tc(ProductFib, :run, [1000000])|>elem(0)
618248

0.618 seconds on my slow desktop here. It’s not memoized but it also skips the great majority of values that need to be calculated too. But how well does that one work when used in the context of your prod stuff question? If you want you could even thread a map through to memoize ‘future’ operations as well, though it gains you nothing in a single call.

Where Next?

Popular in Questions Top

senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
Tee
can someone please explain to me how Enum.reduce works with maps
New
Kurisu
For example for a current url like http://localhost:4000/cosmetic/products?_utf8=✓&amp;query=perfume&amp;page=2, I would like to get: ...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lists...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
mgjohns61585
Could someone help me? I’m making my first elixir program, number guessing game. I can’t figure out how to convert the user’s guess from ...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New

Other popular topics Top

9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
985 43657 311
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 31194 112
New
grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 52774 488
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
jaysoifer
Is there a way to rollback a specific migration and only that one (“skipping” all the other ones)? Would mix ecto.rollback -v 200809061...
New
dogweather
I wrote this comment on r/haskell, and it’s not popular there. :wink: But I think I’m on to something… Haskell reminds me of Java, and e...
New

We're in Beta

About us Mission Statement