loading...
Cover image for Coming to Elixir from TypeScript

Coming to Elixir from TypeScript

reichert621 profile image Alex Reichert ・11 min read

I've been working with Elixir for about 2 months so far, and it's been quite fun. Coming from a background in mostly TypeScript/JavaScript and Ruby, I wasn't sure how approachable I would find it.

A lot of articles I've read say that most Ruby developers would feel comfortable getting started with Elixir, but I'm not sure how much I agree with that. Aside from some superficial similarities, Elixir really forces you to think about solving problems in a slightly different way.

Over the course of my career so far, I've dabbled in programming languages unrelated to the jobs I've been paid for, but this was the first time I really learned a language by jumping right in and attempting to build a full-stack application. I'm a little ashamed to say that I've spent relatively little time going through books on Elixir, and have mostly just gone straight to hacking on our product. That being said, a lot of the opinions below come from the perspective of someone who probably hasn't written much high-quality Elixir code in production. 😬

What I like so far

Here are a few of the things that make me excited about working with Elixir. 😊

The community

This is an easy one. One of the first things I did when I started spinning up on Elixir was joining the Elixir Slack group, and it's been one of the most helpful resources for me as a beginner. The community has been nothing but friendly, patient, and supportive. When I was misusing with statements, they showed me how to refactor it. When I was starting to set up authentication, they pointed me to Pow. When I needed to set up workers, they showed me Oban. People have even been nice enough to review some of my shitty code on Github. It's been amazing.

The extensive built-in functionality

It's kind of nice just having so many useful functions built into the language. Want to flatten an array? Boom, List.flatten(). No need to import {flatten} from 'lodash'. Need to group a list of records by a given key? Boom, Enum.group_by(). I could go on and on!

I especially love that lists, maps, and ranges all implement the Enum protocol. For example, if I wanted to map over an object/map in JavaScript and double each value, I'd have to do something like:

const obj = {a: 1, b: 2, c: 3};

const result = Object.keys(obj).reduce((acc, key) => {
  return {...acc, [key]: obj[key] * 2};
}, {});

// {a: 2, b: 4, c: 6}

Whereas in Elixir, I could just do:

map = %{a: 1, b: 2, c: 3}

result = map |> Enum.map(fn {k, v} -> {k, v * 2} end) |> Map.new()

# %{a: 2, b: 4, c: 6}

Edit: Apparently there's an even easier way to handle this using Map.new/2! (Thanks to /u/metis_seeker on Reddit for the tip 😊)

Map.new(map, fn {k, v} -> {k, v * 2} end)

# %{a: 2, b: 4, c: 6}

Lastly, I love that there are methods like String.jaro_distance/2, which calculates the distance/similarity between two strings. I don't currently use it, but I could see how this might be useful for validating email address domains (e.g. foo@gmial.com -> "Did you mean foo@gmail.com?")

Pattern matching

Pattern matching feels like one of more powerful features Elixir offers as language. While it certainly takes some getting used to, I've found that it forces me to write cleaner code. (It's also caused me to write more case statements and much fewer if clauses than I ever have before!)

For example, if I wanted to write a method in Elixir that determines if a user has a given role (e.g. for the sake of restricting access to certain functionality), I might do something like this:

defp has_role?(nil, _roles), do: false

defp has_role?(user, roles) when is_list(roles),
  do: Enum.any?(roles, &has_role?(user, &1))

defp has_role?(%{role: role}, role), do: true

defp has_role?(_user, _role), do: false

(Note the additional use of pattern matching in the 3rd variant of has_role?/2 to check if the user.role in the 1st parameter is the same as the role provided in the 2nd parameter!)

In TypeScript, the (very rough) equivalent of the above might look something like:

const hasRole = (user: User, roleOrRoles: string | Array<string>) => {
  if (!user) {
    return false;
  }

  // This is probably not the most idiomatic TS/JS code :/
  const roles = Array.isArray(roleOrRoles) ? roleOrRoles : [roleOrRoles];

  return roles.some((role) => user.role === role);
};

Still confused? I don't blame you. Here's the Elixir code again, with some annotations:

# If the user is `nil`, return false
defp has_role?(nil, _roles), do: false

# Allow 2nd argument to be list or string; if it is a list, check
# if any of the values match by applying method recursively to each one
defp has_role?(user, roles) when is_list(roles),
  do: Enum.any?(roles, &has_role?(user, &1))

# Use pattern matching to check if the `user.role` matches the `role`
defp has_role?(%{role: role}, role), do: true

# If none of the patterns match above, fall back to return false
defp has_role?(_user, _role), do: false

This approach has taken some getting used to, but it's definitely growing on me. For example, one pattern I've started using to roll out new features (e.g. Slack notifications) is something like this:

def notify(msg), do: notify(msg, slack_enabled?())

# If Slack is not enabled, do nothing
def notify(msg, false), do: {:ok, nil}

# If it _is_ enabled, send the message
def notify(msg, true), do: Slack.post("/chat.postMessage", msg)

Not sure how idiomatic that is, but it's a nice way to avoid if blocks!

Async handling

A lot of JavaScript is conventionally handled asynchronously (non-blocking) by default. This can be a bit tricky for new programmers, but it can be quite powerful once you get the hang of it (e.g. Promise.all is a nice way to execute a bunch of async processes concurrently).

Elixir is handled synchronously (blocking) by default β€” which makes things much easier, in my opinion β€” but Elixir also happens to make it incredibly easy to handle processes asynchronously if you would like to.

As a somewhat naive example, when I was setting up our Messages API, I noticed it slowing down as we added more and more notification side effects (e.g. Slack, Webhooks) whenever a message was created. I loved that I could temporarily fix this issue by simply throwing the logic into an async process with a Task:

Task.start(fn -> Papercups.Webhooks.notify(message))

Now, this is definitely not the most ideal way to handle this. (It would probably make more sense to put it on a queue, e.g. with Oban.) But I loved how easy it was to unblock myself.

If we wanted to implement something similar to JavaScript's Promise.all, Elixir gives us something even better: control over timeouts!

tasks = [
  Task.async(fn -> Process.sleep(1000) end), # Sleep 1s
  Task.async(fn -> Process.sleep(4000) end), # Sleep 4s
  Task.async(fn -> Process.sleep(7000) end)  # Sleep 7s, will timeout
]

tasks
|> Task.yield_many(5000) # Set timeout limit to 5s
|> Enum.map(fn {t, res} -> res || Task.shutdown(t, :brutal_kill) end)

This allows us to shutdown any processes that are taking longer than expected. πŸ”₯

The pipe operator

It's almost as if any blog post introducing Elixir is obligated to mention this, so here we are.

Let's just take an example directly from the Papercups codebase. In one of our modules, we do some email validation by checking the MX records of the given domain. Here's how it looks in Elixir:

defp lookup_all_mx_records(domain_name) do
  domain_name
  |> String.to_charlist()
  |> :inet_res.lookup(:in, :mx, [], max_timeout())
  |> normalize_mx_records_to_string()
end

If I wanted to write this in TypeScript, I would probably do something like:

const lookupAllMxRecords = async (domain: string) => {
  const charlist = domain.split('');
  const records = await InetRes.lookup(charlist, opts);
  const normalized = normalizeMxRecords(records);

  return normalized;
};

There's nothing inherently wrong with that, but pipes save us some unhelpful variable declarations, and produce code that is arguably just as readable!

I think the thing people like most about the pipe operator is that it both looks cool AND improves (or at least doesn't detract from) readability. But mostly it just looks cool. πŸ€“

Since I wasn't able to write anything particulary intelligent about pipes, I'll leave this section with a quote from SaΕ‘a Juric's "Elixir in Action":

The pipeline operator highlights the power of functional programming. You treat functions as data transformations and then combine them in different ways to gain the desired effect.

Immutability

I can't tell you how many times I've been writing JavaScript and forgotten that calling .reverse() or .sort() on an array actually mutates the original value. (This almost screwed me over in my last technical interview, embarrassingly enough.)

For example:

> const arr = [1, 6, 2, 5, 3, 4];
> arr.sort().reverse()
[ 6, 5, 4, 3, 2, 1 ]
> arr
[ 6, 5, 4, 3, 2, 1 ] // arr was mutated πŸ‘Ž

I love that in Elixir, everything is immutable by default. So if I define a list, and want to reverse or sort it, the original list never changes:

iex(12)> arr = [1, 6, 2, 5, 3, 4]
[1, 6, 2, 5, 3, 4]
iex(13)> arr |> Enum.sort() |> Enum.reverse()
[6, 5, 4, 3, 2, 1]
iex(14)> arr
[1, 6, 2, 5, 3, 4] # nothing has changed πŸ‘Œ

Hooray! This makes the code much more predictable.

Dealing with strings

I love that there are so many ways to format and interpolate strings in Elixir. This might be a bit of a niche use case, but the triple-quote """ approach has been super useful for email text templates, since it removes all the preceding whitespace from each line:

def welcome_email_text(name) do
  """
  Hi #{name}!

  Thanks for signing up for Papercups :)

  Best,
  Alex
  """
end

If I wanted to do this in TypeScript, I'd have to do something like:

const welcomeEmailText = (name: string) => {
  return `
Hi ${name}!

Thanks for signing up for Papercups :)

Best,
Alex
  `.trim();
};

Which just looks... awkward.

What I'm... still getting used to

I almost called this section, "What I dislike so far", but I thought that would be a little unfair. Just because I'm not accustomed to certain ways of thinking doesn't mean I have to hate on it.

So without further ado, here are some of the things I'm still getting used to with Elixir. 😬

Error handling

One of the first things I noticed when I started to dip my toes in Elixir was the prevalence of methods returning {:ok, result}/{:error, reason} tuples. I didn't give it much thought at first, and found myself writing a lot of code that looked like:

{:ok, foo} = Foo.retrieve(foo_id)
{:ok, bar} = Bar.retrieve(bar_id)
{:ok, baz} = Baz.retrieve(baz_id)

...and then got hit with a bunch of MatchErrors.

As you might have guessed (if you've written any Elixir), this led me to start getting a little overly enthusastic about the with statement. Which if you haven't written any Elixir, looks something like this:

with {:ok, foo} <- Foo.retrieve(foo_id),
     {:ok, bar} <- Bar.retrieve(bar_id),
     {:ok, baz} <- Baz.retrieve(baz_id) do
  # Do whatever, as long as all 3 methods above execute without error
else
  error -> handle_error(error)
end

There's nothing particularly wrong with that, but I've also found myself writing some methods that basically just extract the result portion of the {:ok, result} tuple, which feels a little silly:

case Foo.retrieve(foo_id) do
  {:ok, foo} -> foo
  error -> error
end

(It's very possible that the above code is an antipattern, and I'm simply not handling things correctly.)

Anyway, on one hand, I feel like this convention of the language is good because it forces programmers to be more cognizant of error handling. But on the other hand, it definitely takes some getting used to.

Implicit returns (and no return keyword)

While pattern matching is great and all, the fact that Elixir does not have the ability to break out of a function early can be a bit frustrating as a beginner.

For example, if I wanted to write a function to compute the total cost of a bill in TypeScript, I might do something like:

const calculateTotalPrice = (bill: Bill) => {
  if (!bill) {
    return 0;
  }

  const {prices = []} = bill;

  // This is a little unnecessary, but illustrates the point of
  // a second reason we may want to return early in a function
  if (prices.length === 0) {
    return 0;
  }

  return prices.reduce((total, price) => total + price, 0);
};

The code above allows me to break early and return 0 under certain circumstances (e.g. when bill is null, or prices is an empty list).

Elixir solves this with pattern matching (as we've discussed in more detail above).

def calculate_total_price(nil), do: 0

def calculate_total_price(%{prices: prices}) when is_list(prices),
  do: Enum.sum(prices)

def calculate_total_price(_bill), do: 0

For someone approaching Elixir as a newbie like myself, this can take some getting used to, because it forces you to take a step back and rethink how you would normally design your functions.

Dialyzer and the development experience

There's not much to say here, other than that Dialyzer can be pretty frustrating to deal with at times. Sometimes it's just slow, and warnings take a few seconds to pop up... this is annoying when I: change some code to fix a warning; the warning goes away for a few seconds; I feel good about myself for having fixed it; and then boom, another warning pops up.

Other times, the warnings and just cryptic or confusing:

Cryptic Dialyzer warning

(I have no idea what this means...)

Debugging macros

When I was starting off with the Pow library to implement auth, I ran into Elixir macros for the first time. I felt like such an idiot trying to figure out where the pow_password_changeset method was defined, until I finally found this piece of code:

@changeset_methods [:user_id_field_changeset, :password_changeset, :current_password_changeset]

# ...

for method <- @changeset_methods do
  pow_method_name = String.to_atom("pow_#{method}")

  quote do
    @spec unquote(pow_method_name)(Ecto.Schema.t() | Changeset.t(), map()) :: Changeset.t()
    def unquote(pow_method_name)(user_or_changeset, attrs) do
      unquote(__MODULE__).Changeset.unquote(method)(user_or_changeset, attrs, @pow_config)
    end
  end
end

It's pretty cool that Elixir supports macros, but the syntax and the idea of dynamically generating methods is not something I've ever had to deal with. But I'm excited to try it out!

Dealing with JSON

Honestly, I feel like this is true for most languages (other than JavaScript/TypeScript). Since most maps in Elixir use atoms for keys, I've found myself accidentally mixing atom/string keys when I'm unknowing working with a map that has been decoded from JSON.

Unclear trajectory of the language

I honestly have no idea whether Elixir is growing, stagnating, or declining in popularity, but so far things seem much more enjoyable and less painful than I expected.

When we first started building Papercups in Elixir, a few people warned us that the lack of libraries and support would make it much harder to move quickly. While it's clear that the amount of open source libraries is much lower compared to languages like JavaScript, Ruby, Python, and Go, so far this hasn't been a huge issue.

As more well-known companies (e.g. WhatsApp, Discord, Brex) begin using Elixir in production, I'm hoping developer adoption continues to grow. I'm optimistic! 😊

That's all for now!

If you're interested in contributing to an open source Elixir project, come check out Papercups on Github!

Posted on by:

reichert621 profile

Alex Reichert

@reichert621

Stripe engineer based in NYC

Discussion

pic
Editor guide
 

Great post! I made the same jump (also to ruby) and I echo a ton of this post.

While I wish elixir had a similar type system to typescript, I still find it catches a ton of my mistakes when it compiles (something sorely missing in ruby).

I love that it’s synchronous by default like you mentioned, whereas javascript is the opposite. It’s easy to get things done, but then throw it in a background task if really needed.

And a huge +1 to being apprehensive over where the language is headed. Sometimes I get super excited and devour tutorials and blogs and books, other times I find myself paranoid I’m throwing away career trajectory. But it really is a fun little language

 

Totally agree! But I wouldn't worry too much about "throwing away career trajectory" -- I think learning a language like Elixir will make you a better programmer regardless of how much you end up using it in your professional life :)

 

I don’t disagree with that, but I do think I’m at a point where getting a little deeper knowledge of a language/framework would serve me pretty well, and ruby/rails seems way more prevalent. But it’s definitely not as fun

 

After working with Elixir for the first time, I started to miss some resources in other languages. Pattern matching is what I miss the most, using it eliminates so much complexity that it's crazy!

By the way, great post, I love reading about Elixir. I wrote a similar one for people who have difficulty understanding some new concepts and features, I hope you like it!

Learning Elixir with PHP help