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

antoine-duchenet
Everything went smoothly today. Nothing to change to solve part 2 because I already used memoization for part 1 (it looked like an AoC e...
New
sneako
Note by the Moderators: This topic is to talk about the first day of the Advent of Code. For general discussion about the Advent of Code...
New
Aetherus
This topic is about Day 15 of the Advent of Code 2020 . Thanks to @egze, we have a private leaderboard: https://adventofcode.com/2020/l...
New
LostKobrakai
This one has been quite the ride. Struggled at first to find a good data format to suite the problem. I really like how that turned out b...
New
bjorng
Note: This topic is to talk about Day 25 of the Advent of Code 2019. There is a private leaderboard for elixirforum members. You can joi...
New
Aetherus
This topic is about Day 5 of the Advent of Code 2020 . Thanks to @egze, we have a private leaderboard: https://adventofcode.com/2020/le...
New
Aetherus
Finished Day 1 with Elixir :tada: Here’s my code: #!/usr/bin/env elixir defmodule Combination do @doc "Yields each combination of 2...
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
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 6 of the Advent of Code 2019. There is a private leaderboard for elixirforum members. You can join...
New

Other popular topics Top

Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: https://hexdocs.pm/ecto/Ecto.Schema.html#module-...
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 49522 488
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 42633 214
New

We're in Beta

About us Mission Statement