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:
shorten/2, this takes a name/pid and a URL and returns an idget/2, this takes a name/pid and an id and returns the urlflush/1, this takes a name/pid and flushes the state to an empty statecount/1, this takes a name/pid and returns a total count of the URLs in the stateget_stats/1, this takes a name/pid and returns the count of URLs per unique hosts from the urlsget_click_stats/1, this takes a name/pid and returns the total count of clicks on all urls combined (click count is generated fromget/2)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 fromget/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
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
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 ![]()
For the cast… it’s true I start with call everywhere, but when I really don’t expect any response, I switch for cast.
Popular in Challenges
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance








