michael_teter

michael_teter

Feedback request - my simple GenServer demo

I recently took a code challenge for a job I applied to. My Elixir experience is limited to one production Phoenix project; but as most of you know, with a framework and a fairly simple project, there’s not a lot of skill required beyond basic language use and understanding how to use the framework correctly. Thus, I had not done anything interesting with BEAM before.

Here was the problem spec:

URLShortener

Create a GenServer that acts as a URLShortener which holds the URLs and IDs in its state (a simple map structure would be OK).

Create the following client functions and corresponding callbacks:

  1. shorten/2 , this takes a name/pid and a URL and returns an id
  2. get/2 , this takes a name/pid and an id and returns the url
  3. flush/1 , this takes a name/pid and flushes the state to an empty state
  4. count/1 , this takes a name/pid and returns a total count of the URLs in the state
  5. get_stats/1 , this takes a name/pid and returns the count of URLs per unique hosts from the urls
  6. get_click_stats/1 , this takes a name/pid and returns the total count of clicks on all urls combined (click count is generated from get/2 )
  7. get_click_stats/2 , this takes a name/pid and id and returns the count of clicks on url with given id (click count is generated from get/2 )

And here was my answer:
https://github.com/michaelteter/shorty

I followed a few examples I found online, and I used Elixir/GenServer documentation. However, the feedback I got was limited to “it’s not how we would have done it”. But as it is with job interviews, it’s quite rare to get details. So in my interest to learn, I would be happy to hear any comments from ElixirForum folks. TIA!

Most Liked

kokolegorille

kokolegorille

Sorry for your job interview…

I have seen some part I would change, but given I don’t know the rule it’s kind of difficult to answer fully.

  • Why start_server? I would use start_link
  • Why use tuple when there is no argument? atom is enough I think
  • Maintaining an id count is painful, if allowed, I would use UUID
  • Why create your regex to parse host when there is URI module
  • I would use cast for flush

I find also difficult to reason about the state you are passing.

As a simple example, I would store a simple tuple by given id, something like {host, url, count}

I don’t mean to have the perfect answer, but what about something like this?

  @impl GenServer
  def handle_call({:shorten, url}, _from, state) do
    host = URI.parse(url).host
    id = UUID.uuid4()
    new_state = Map.put(state, id, {host, url, 0})
    {:reply, id, new_state}
  end

  @impl GenServer
  def handle_call({:get, id}, _from, state) do
    case Map.get(state, id) do
      {host, url, count} ->
        {:reply, url, %{state | id => {host, url, count + 1}}}
      nil ->
        {:reply, nil, state}
    end
  end

  @impl GenServer
  def handle_call(:count, _from, state) do
    {:reply, Enum.count(state), state}
  end

  @impl GenServer
  def handle_call(:get_stats, _from, state) do
    reply = state
    |> Enum.group_by(fn {_k, {url, _, _}} -> url end)
    |> Enum.reduce(%{}, fn {key, list}, acc ->
      Map.put(acc, key, Enum.reduce(list, 0, fn {_k, {_, _, count}}, a -> a + count end))
    end)

    {:reply, reply, state}
  end

  @impl GenServer
  def handle_call(:get_click_stats, _from, state) do
    reply = state
    |> Enum.reduce(0, fn {_key, {_host, _url, count}}, acc -> acc + count end)
    {:reply, reply, state}
  end

  @impl GenServer
  def handle_call({:get_click_stats, id}, _from, state) do
    reply = state
    |> Enum.filter(fn {key, _value} -> key == id end)
    |> Enum.reduce(0, fn {_key, {_host, _url, count}}, acc -> acc + count end)
    {:reply, reply, state}
  end

  @impl GenServer
  def handle_cast(:flush, _state) do
    {:noreply, %{}}
  end
kokolegorille

kokolegorille

start_link is not only for historical reason, it means there is a link created between the parent and child processes, which is not the case with start.

And yes, our code makes about the same things :slight_smile:

For the cast… it’s true I start with call everywhere, but when I really don’t expect any response, I switch for cast.

Where Next?

Popular in Challenges Top

maennchen
Ok, that was a rough one today. I haven’t found a way to improve the algorithm further. Part 1 runs in .5 seconds, Part 2 in ~ 5 minutes...
New
Aetherus
Hello, guys. I’m back again, but only for the weekends, maybe. This topic is about Day 13 of the Advent of Code 2020 . Thanks to @egze,...
New
stevensonmt
Reasonably pleased with my solution. The bitstring packet problems are so well suited to Erlang/Elixir it’s almost not fair. defmodule D...
New
bjorng
Here is my solution: https://github.com/bjorng/advent-of-code-2021/blob/77bdef7a51b428b58ffb4ab76f540901e636f841/day12/lib/day12.ex
New
kwando
Phew, this one took a while to get right. My naive attempts was way to slow so I reached for Dijkstras shortest path algorithm.. and that...
New
bjorng
Here is my solution for day 1 of Advent of Code: defmodule Day01 do def part1(input) do all = parse(input) {first, second} = E...
New
bjorng
Note: This topic is to talk about Day 1 of the Advent of Code 2019.
New
seeplusplus
Hello all, hopefully I post this before someone else does and I don’t dupe. IMO Day 4 was much easier than Day 3 (yay, I can sleep befor...
New
Aetherus
Don’t know why the regex ~r/[\W && [^\.]]/x does not work in Elixir. It works pretty well in Ruby. Anyway, here is my solution: ...
New
stevensonmt
Trying to get more facility with dynamic programming concepts on Leetcode and having an issue I can’t find a way around. It’s a chutes an...
New

Other popular topics 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
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
marick
I had some trouble figuring out how to make many-to-many associations work. Once I got it working, I wrote a blog post. Because I’m a nov...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New
sergio
Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browse...
New

We're in Beta

About us Mission Statement