For the past many many years my goto language has been Python. I’ve written all sorts of applications using Python:
- Django web apps
- Client side GUI applications with PyQt
- Data science stuff with numpy, pandas, etc.
- Alexa applications
- Serverless systems and web APIs
- Microservices and a microservice library
- Many backend services to power various SAAS and non-SAAS applications
Whenever there is some type of problem I need to solve programatically, I reach for Python. Sure, I can be effective in other languages, but by and large, I’m a Python guy.
The problem
Not too long ago I counted it up…over the past seven years I’ve worked at three companies all which had these things in common:
- Python web stacks
- Django
- Spaghetti code which is (extremely) hard to reason about and evolve
Where I landed many months ago was being unhappy and unsatisfied with Django and all of the wrong decisions which are easy to make when using it to build a web application. I don’t think this is necessarily a problem with Python or Django themselves, however I do feel that both Python and Django enable developers to make poor decisions when architecting a web application. Maybe to frame this in a more positive light, it’s hard to make good decisions when writing a large Django application. If anything, Django is an enabler (and guilt by association, Python).
Node and JavaScript are probably even worse and more enabling at bad patterns. Rails and Ruby, yes…although with Rails there are such strong conventions around the “Rails way” that at least people don’t have to look very far to figure out the “right” way of doing something (don’t take this as an endorsement…I believe this creates a community which freaks out if they don’t have an existing pattern to copy/paste).
Django makes it very easy to couple
different parts of your application together, in spite of your best intentions. I would venture
to say that most of this comes down to the ORM. Want to import some models from a completely
different part of your application and start firing off queries? No problem. Want to write a
triple nested loop over a queryset and fire off 1M+ DB statements for related records? Sure!
(Note, I have seen
this done and spent weeks fixing it). Need some data in your template? Just shove an all()
queryset in your template and iterate to your hearts content. This works great when you have 10
rows in your table, not so much when you have 1M rows.
Where I’m going is that I said to myself
There must be a better way
The (possible) solution
Slowly, I’ve been digging into Elixir. Why? I wanted to learn and use a functional language to see if it would solve some of the problems I’ve hit with imperative languages like Python. Here are a few other things which helped point me in the Elixir direction:
- Erlang VM (BEAM)
- Ability for some massive concurrency
- Built-in messaging (hello microservices)
- Possibility of hot-loading new code
- Picking up lots of steam mostly from the Rails community. But the bottom line is that the “there must be a better way” theme is shared from different communities
- Ringing endorsements from other web influencers
Of course, I could be wrong…Elixir could not be the solution I’m looking for. However, I do
know that I want need a new way of building web applications and microservices. Here, I’d like to
do a series of posts about exploring and learning Elixir from the perspective of a Python
developer.
Elixir for Pythonistas
I would rather not do a series on Elixir syntax, but it’s inevitable that I’ll need to cover some things. There are plenty of resources online about the Elixir language itself…the official docs are quite good. I’ll recommend the following if you’d like to start from zero:
- http://elixir-lang.org/crash-course.html
- My Elixir Fundamentals talk from the Boulder Elixir Meetup
- Programming Elixir by Dave Thomas
Very quickly I’d like to get rolling into the distributed nature of Elixir/Erlang, which I currently don’t know many details about. Let’s start with some basics.
NOTE: I will say things like, “unlike Python…” due to the fact that I’m writing from the Pythonistas perspective. In reality, these comparisons should be made with procedural or OO languages. Here, I’ll just use Python to represent that class of languages unless I’m discussing something truly unique to Python.
Immutability and Variables
Unlike Python, variables are immutable. For example:
>>> d = {'name': 'bz', 'height': 67}
>>> some_function(d)
Now, what is the value of d
without knowing the details of some_function
? It’s impossible to
answer this. The reason is that you’re passing the d
dictionary by reference, which means
some_function
can mutate any mutable object it’s given (lists, sets, class, instances, etc.)
What about Elixir:
iex> d = %{name: "brian", height: 67}
%{height: 67, name: "brian"}
iex> some_function.(d)
%{height: 67, name: "Fred"}
You’ll notice that the Elixir shell spits out values while it’s evaluating commands. Here, we can
see that some_function
is replacing the name
key with "Fred"
. But, look at d
after all of
this.
iex> d
%{height: 67, name: "brian"}
That’s right…our original map (dict
in Python terms) is unchanged. That’s pretty great. All of
a sudden it become much easier to reason about what your program is doing since we’re dealing
with data rather than behavior.
So, if we really did want to update our map, how would we handle this? We’ll simply reassign the
d
variable to the results returned from some_function
:
iex> d = some_function.(d) │:yes
%{height: 67, name: "Fred"}
iex> d
%{height: 67, name: "Fred"}
Let’s just try to manhandle this thing:
iex> Map.put(d, :name, "sam")
%{height: 67, name: "sam"}
iex> d
%{height: 67, name: "Fred"}
Doh! You cannot mutate an existing object. You will always be creating new objects. What you do with those is up to you.
I like this very much. Tracing code is now a matter of looking at what is occurring to the data, rather than trying to track down what code is changing this class instance, dict, etc from under me. There are other implications and advantages to immutable data types I won’t cover here.
Pattern matching
You’ll hear the term “pattern matching” a lot with Elixir (and I’d guess, with Erlang). This will likely be the biggest shift in thinking when coming from Python to Elixir, but I think it’s easy to understand as you work with it.
Above, we seemingly assigned a map to a variable d
. Don’t be fooled here…what we did was
pattern match the left side of the equality operator with the right side. What does that mean
exactly?
Elixir will take an expression and attempt to match whatever is on the left side of the equals sign
with that is on the right side, in this case, what is happening is that Elixir is matching the
variable d
with the map on the right:
iex> d = %{name: "brian", height: 67}
There is one item on the left, d
and one on the right, %{name: "brian", height: 67}
…so d
ends up being pointed at this map.
Let’s look an Elixir tuple:
iex> tup = {1, 2, 3}
{1, 2, 3}
Makes sense. But we can also do this:
iex> {a, b, c} = {1, 2, 3}
{1, 2, 3}
iex> a
1
iex> b
2
iex> c
3
What is happening here is that Elixir attempt to match the left and right side. Because we have the same number of arguments, the right side values are assigned to the left side variables. This is “unpacking” in Python…we can do the same thing so you may not be very impressed (yet):
>>> tup = (1, 2, 3)
>>> (a, b, c) = tup
Going back to our Map / dict, how would you extract the value of a dictionary key and assign it into a variable?
>>> d
{'name': 'bz', 'height': 67}
>>> myheight = d.get('height')
>>> myheight
67
With Elixir, we can extract a value by matching a key on the left with the right:
iex> %{height: myheight} = d
%{height: 67, name: "Fred"}
iex> myheight
67
Wow…so we’re saying, “Elixir, please match a Map with a key of “height” on the left with whatever
is on the right. If that matches, assign the variable myheight
to whatever the value is on the
right side”
That may seem trivial now, but it’s the underpinning of Elixir and helps preventing code like this:
def view_function(request, user, reports=None):
reports = reports or {}
for key, f in reports.items():
perm_key = 'user.can_view_%s_report' % key
if key == 'unsigned' and not user.sig_on:
continue
if key in ('foo', 'br') and not user.new_user:
continue
if key in ('payments', 'all', 'client') and not user.track:
continue
if key == 'authorizations' and not user.cms_user:
continue
if key == 'totalcostof' and not user.is_foobar:
continue
if key == 'premiums' and not \
(user.some_attribute and request.user.admin and request.user.admin.is_manager):
continue
In Elixir, all of these conditionals could be handled with pattern matching, resulting in multiple functions that handle some specific part of our domain logic. The code above becomes very very hard to reason about, test and debug. Sure, this can be refactored, but to my previous points because it’s possible to write code like this, it’s inevitable that people will.
Conclusion
That’s it for now. I hope to continue on this path of exploring Elixir and writing about the highlights I find interesting.