Building a Elixir & Phoenix Application

Learning Elixir

Ever since I ran into Elixir/Phoenix through a couple of popular Hacker News posts I've been interested in tinkering with the language. I have a little idea for an app that I'm just motivated enough to build that Elixir would work for. I've document my learning process below by logging my thoughts as I learned Elixir via a 'learning project'.

What I'm building

Here's what I'd like to build:

  • Web app which detects the user's location using the built-in location service in the browser
  • The zip code of that location is determined (server or client-side)
  • The zip code is handed off to a server-side process which renders a page with the zip code.

Here's what I'll need to learn:

  • Elixir programming language
  • Phoenix application framework
  • Managing packages and dependencies
  • Erlang runtime architecture
  • How client-side assets are managed in phoenix
  • How routing in Pheonix works

I'm not going to be worried about deploying the application in this project.

This is going to be fun, let's get started!

Learning Elixir & Pheonix

I've worked with Rails for a while now, so most of the conceptual mapping is going to be from Ruby => Elixir and Rails => Phoenix.

  • First, let's get a basic Pheonix dev environment up and running: https://hexdocs.pm/phoenix/installation.html
  • Wow: "An Erlang system running over one million (Erlang) processes may run one operating system process". Processes are not OS processes but are instead similar to green threads with much less overhead.
  • Some tooling equivalents: https://thoughtbot.com/blog/elixir-for-rubyists. asdf, exenv, kiex == rbenv. Looks like asdf is the most popular replacement.
    • Reading this through, I can see why rubyists are so angry about the pipe operator (|>). The elixir version is much different (better, actually useful) than the proposed ruby version. It takes the output of a previous function and uses it as the first input to the next function in the chain.
    • "Function declarations support guards and multiple clauses". What does that mean? It sounds like you can define a method multiple times by defining what the argument shape looks like. Instead of a bunch of if conditions at the top of a function to change logic based on inputs, you simply define the function multiple times. Makes control flow easier to reason about.
    • There's some great syntactical sugar for array iteration for document <- documents == documents.each { |document| ... }
    • "I believe Elixir and Ruby are interchangeable for simple web applications with no high-traffic or that don’t require very short response times." This has been my assumption thus far: Elixir is only really helpful when performance (specifically concurrent connections) is a critical component. We will see if this plays out as I learn more.
  • I'd recommend creating an elixir folder and cloning all of the open-source projects I reference below into it. Makes it very easy to grep (I'd recommend ripgrep, which is much better than grep) for various API usage patterns.
  • To install elixir: brew install elixir; elixir -v verifies that we have the minimum required erlang and elixir versions. I ran this check, we are ready to go!
  • mix is a task runner and package manager in one (rake + bundle + bin/*). It uses dot syntax instead of a colon for subcommands: bundle exec rake db:reset => mix ecto.reset
  • When I ran the install command for pheonix it asked for hex. Looks like bundler/rubygems for elixir. https://hex.pm
  • Webpack is used for frontend asset management and isn't tied into Elixir at all (which I really like). Postgres is configured as the default DB.
  • Now can I start running through the Phoenix hello world: https://hexdocs.pm/phoenix/up_and_running.html
  • etco == ActiveRecord, kind-of. Seems a bit more light weight.
  • Time to setup the database! config/dev.exs is the magic file. Looks like a very Rails-like folder structure at first glance. Interesting that they have a self-signed local https setup built in. That was a huge pain in ruby-land.
  • Looks like lib/NAME_web => app/
  • eex === erb and has ~same templating language
  • https://milligram.io looks like an interesting minimalist bootstrap. This was included in the default landing page.
  • elixir atom == ruby symbol
  • Erlang supports hot code updates: "We didn't need to stop and re-start the server while we made these changes." Very cool.
    • Later on, I learned that this isn't as cool/easy as it sounds. Most folks don't use this unless their applications have very specific requirements.
  • Routing (routes.ex) looks to be very similar to rails. The biggest difference is the ability to define unique middleware stacks ("pipelines") that match against specific URL routes or content-types.
    • Later on, I realized there aren't nearly as many configuration options compared to rails. For example, I don't believe you use regexes to define a URL param constraint.
  • Huh, alias seems to be like include within modules. Nope! Got this one wrong: Looks like it just makes it easier to type in a module reference. Instead of Some.Path.Object, with alias you can just use Object (without specifying the namespace). use is similar to include in ruby.
  • mix phx.server === bundle exec rails server
  • mix deps.get === bundle
  • Plugs seem similar to Rails engines. Nope! Plug is just a middleware stack. Umbrella applications are similar to Rails engines.
  • Dots . instead of double colons :: for nested modules: MyApp.TheModule == MyApp::TheModule
  • Huh, never ran into the HTTP HEAD method before https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD
  • Examples seem to indicate that router pipelines should be used for before_filter type of logic.
  • fn is a lambda function in Elixir. Doesn't look like there are multiple ways to do lambdas. Yay! I hated the many ways of defining anonymous functions in ruby that all worked slightly differently (procs, blocks, and lambdas).
    • There is shorthand syntax fn(arg) -> arg.something end == &(&1.something)
  • There's defp, def, and defmodule. What's the difference? After a bit of digging, these are core elements to elixir which slightly change how the methods are defined. defp is a private method, for instance.
  • Ahh func/2 references the implementation of func with two arguments. When referencing a function you must specify the number of arguments using this syntax.
  • use Phoenix.Endpoint references a macro, how exactly do macros work? Macros are Elixir's metaprogramming primitive. That's all for now, I'll read more later.
    • I ended up not doing any metaprogramming in my application but learned it a bit about it. It sounds like you essentially specify code you want to inject into a module by quoteing it within the defmacro __using__ function in your module. This __using__ function is automagically called when you use the module. This enables you to dynamically write the elixir code you want to include (you can think of a quote as dynamically eval'd code).
  • Live reload for the front and backend is installed by default and "just works" when running a development server. Yay! Hated all of the config in rails around this.
  • "It is also possible for an application to have multiple endpoints, each with its own supervision tree" sounds very cool. I'm guessing this allows for multiple applications to be developed within one codebase but to run as essentially separate running processes? Something to investigate in another project.
  • Interesting that the SSL config is passed directly to the core phoenix endpoint configuration. I wonder if there is something like unicorn/puma in the mix? It looks like there is, Cowboy is the unicorn/puma equivalent.
  • Ecto is not bundled in the Phoenix framework. It's a separate project. Looks like phoenix favors a layered vs all-in-one approach, but is opinionated about what packages which are installed by default (which I like).
  • I don't fully understand this yet, but it looks like there is an in-memory key-value store built into OTP, which is the elixir runtime (i.e. erlang). In other words, something like Redis is built-in. What are the trade-offs here? Why use this over Redis or another key/value store?
  • Because you can define multiple variations of a method, things like action_fallback is possible. Define error handling farther up the chain and just think about the happy path in the content of the method you are writing. Neat.
  • "EEx is the default template system in Phoenix...It is actually part of Elixir itself" Great, so this isn't something specific to Phoenix.
  • This made something click for me: "pattern matching is strong typing" https://news.ycombinator.com/item?id=18842123
  • It seems as though one of the goals behind pattern matching + function definitions is to eliminate nested conditionals. Elixir (and probably functional programming in general) seems to favor "flat" logic: I'm not seeing many nested if statements anywhere.
    • As I learned later, if statements are generally discouraged and hard to use as they have their own scope (you can't modify variables in the outer scope at all).
  • ^ 'pins' a variable. const in node, but slightly different because of this "matching not assignment" concept (which I don't fully get yet).
    • This is used a lot in Ecto queries, but I'm not sure why.
  • Gen prefix stands for Generic NOT Generate as I thought. i.e. GenServer == Generic Server.
  • I still don't understand this "Let it crash" philosophy. Like, if a sub-routine of some sort fails, it would corrupt the response of any downstream logic. I can see the benefits of this for some sort of async map-reduce process, but not a standard web stack. What am I missing?
  • After many rabbit holes, I'm ready to tackle my initial goal! I'm having a blast, it all seems very well designed: I'm getting the same feeling as when I first started learning Rails via Spree Commerce year ago.

What I'm missing from Ruby

Overall, I found the built-in Elixir tooling to be top-notch. There didn't seem to be too many obvious gaps and things generally "just worked". However, there's some tooling from the ruby ecosystem that I was missing as I went along.

  1. Automatically open up a REPL when an exception is thrown. In ruby, this is done via pry-rescue. Super helpful for quickly diving into the exact context where the error occurs.
  2. In Phoenix, it would be amazing if the debugging plug (which displays a page when an exception is thrown) displays the variables bound in a specific scope so I can reproduce & fix errors quickly. It would be even better if a REPL could be opened and interacted with on the exception page. better-errors does this in ruby. Given that all code in Elixir is functional, simply knowing the local variables in a specific scope would be enough to reproduce most errors and would make for a very quick debugging loop.
  3. iex -S mix phx.server feels weird. It would feel a bit nicer if there was a mix phx.console which setup IEx for you.
  4. The Allow? [Yn] prompt is annoying when I'm debugging a piece of code. It would be great if you could auto-accept require IEx; IEx.pry requests.
  5. In a debugging session, I couldn't figure out how to navigate up and down the call stack. Is there something like pry-nav available?
  6. Scan dependencies for security issues. In ruby, this is done via bundler-audit.
  7. I couldn't find a VS Code extension with Phoenix snippets.
  8. Built-in Structured Logging. In my experience, using structured logs is incredibly helpful in effectively debugging non-trival production systems. I've always found it frustrating that it's not built-in to the language (I built one for ruby). I think it would be amazing if this was provided as an optional feature in Elixir's logger: Logger.info "something happened", user: user.id => something happened user=1
  9. It doesn't seem possible to run a mix task in production when using Elixir releases. There are many scenarios where you'd want to run a misc task on production data (a report, migration, etc). In Rails-land, this has been a great tool to have to solve a myriad of operational problems when running a large-ish application.
  10. Ability to add multiple owners/authors to a hex package. This makes it challenging to hand off ownership of a package when the original creator doesn't have the time to maintain it anymore.
  11. Coming from Rails, phoenix_html feels very limited. There are many convenience methods I'm used to in Rails that I wasn't excited about re-implementing.
  12. In ruby, if you are working on improvements to a gem (package) you can locally override the dependency using bundle config local.gem_name ~/the_gem_path. This is a nice feature for quickly debugging packages. There's not a built-in way to do this.

I posted about this on the Elixir forums and got helpful workarounds along with confirmations about missing functionality.

Initial impressions

I enjoyed learning Elixir! It's a well designed language with great tooling and a very supportive community. However, it still feels too early to use for a traditional SaaS product.

Although there are packages for most needs, they just don't have as many users as the ruby/javascript ecosystem and there's a lot of work you'll need to do to get any given package working for you. Phoenix is great, but it's nowhere close to rails in terms of feature coverage and you'll find yourself having to solve problems the Rails community has already perfected over the years. The deployment story is really poor and is not natively supported on Lambda, Heroku, etc.

There are specific use-cases where Elixir is a great choice: applications that have high concurrency and/or performance demands (i.e. chat, real-time, etc) and IoT/embedded systems (via nerves) are both situations where Elixir will shine. The Elixir language has been more carefully curated compared to ruby and continues to improve at a great velocity. It's cool to see the creator of Elixir very active in the forums an actively listening to the users and incorporating feedback. It very much reminds me of the early days of Rails.

This is all to say, in my experience, Ruby + Rails is still the fastest way to build web applications that don't have intense concurrency/performance requirements on day one. The ecosystem, opinionated defaults, and hardened abstractions battle-tested by large companies (Shopify, GitHub, Stripe) are just too good. The dynamic nature of the language allows for tooling (better-errors, pry-rescue, byebug, etc) that materially increases development velocity.

Other Learnings

Community matters

When I first started learning how to program, Kirupa (which still exists, amazingly) was an incredible resource. Random people from the internet answered by basic programming questions. All of my initial freelancing work came from the job board. The Flash/Actionscript tutorials on the site were incredibly helpful. It was a relatively small tightly-nit community that was ready to help.

I've feel like we've lost that with StackOverflow and googling for random blog posts.

The ElixirForum.com community is awesome and has that same kind, tight-nit, open-to-beginners feel that the forums of the 90s had. I was impressed and enjoyed participating in the community.

Confirmation bias is very real

I already liked Elixir before I dug into it. It looked cool, felt hot, etc. I was looking for reasons to like it as I did this example project.

It was interesting to compare this to my experience with node. I already didn't like Javascript as a whole and was ready to find reasons I didn't like node.

I found them, but would I have found just as many frustrating aspects of Elixir if I didn't have a pre-existing positive bias towards Elixir?

Managing your psychology and biases is hard, but something to be aware of in any project.

Functional programming isn't complicated

"Functional programming" is an overloaded concept. Languages are touted as "functional programming languages", there are dedicated FP conferences, and fancy terms (like "monads") all make it harder for an outsider to understand what's going on.

I want to write up a deep-dive on functional programming at some point, but getting started with this style of programming is very easy:

  1. You can program in a functional style in any language.
  2. Don't store state (or store as little as possible) in objects.
  3. This forces you to declare all inputs needed for the function as arguments, instead of sourcing variables from an instance or class variable.
  4. Writing functions that don't depend on external state are deterministic/idempotent by default. In other words, running the function against the same set of inputs yields the same results.

Boom! You are programming in a functional style. There's more to it, but that's the core.

Per-language folders for easy code search

Having a set of repositories is very helpful is understanding how various libraries are used in production. I've found it super helpful to have a folder with any great open source applications I can find in the language I'm learning. This makes it very easy to grep for various keywords or function names to quickly understand patterns and real-world usage.

For example:

cd ~/Projects
mkdir elixir
cd elixir
git clone https://github.com/thechangelog/changelog.com

# ripgrep is a faster and much easier to use version of grep
rg -F 'Repo.'

Along these lines, grep.app is a great tool for quickly searching a subset of GitHub repositories (can't wait until GitHub fixes their code search).

Open questions

There's a bunch of concepts I didn't get a chance to look into. Here's some of the open questions I'd love to tackle via another learning project:

  • Processes/GenServer/GenStage. Although I did work with packages that create their own processes, I didn't work with Gen{Server,Stage} from scratch.
  • Macros / metaprogramming.
  • Testing.
  • Ecto/ORM.
  • Callbacks (what does @behaviour do?).
  • Clusters/Nodes (connecting multiple erlang VMs together to load balance)
  • Functional programming concepts. These were referenced around the edges but I never dug into them in a deep way.
  • Recommended Elixir style guide. I know there's a built-in formatter/linter, but I wonder if there's a community-driven opinionated style guide.
  • Background jobs.
  • Deployment.
  • VS Code/language server/development environment optimizations.
  • What's up with @spec? Is there typing coming to elixir?
  • Supervisor trees.
  • Built-in ETS tables. Looks like a built-in key-value store similar to redis.

Resources for learning Elixir & Phoenix

General

Specific Topics

Opinions

Videos

Example Applications

Clone these for easy local grepping.