How to adapt to the dynamic typing nature of Elixir?

Most of my career has been based on with mainstream typed languages. I know the concepts of FP and I have used Elixir to some extend on a few personal projects, but nothing big. The biggest struggle that I have is not FP, but the dynamic typing of the language and I don’t know if this is a skill issue, likely, or if I’m just hard wired to think about types.

The moment I start to think about protocols and structs in Elixir, is the moment I get blocked with paralysis analysis and the end solution is always bad because it’s neither typed nor dynamic.

Do you have any tips on how to adapt to the dynamic nature of the language?

Thanks!

A good place to start: put protocols at the very back of your mind and avoid considering them when possible. They are a solid solution for a very specific problem, but most of the time you don’t have that problem in your application code.

That problem is (IMO) mostly about the open-closed principle applied to Elixir modules. If you have a function like this:

defmodule Foo do
  def some_fun(%Thing1{} = x), do: ...
  def some_fun(%OtherThing{} = x), do: ...
  ...etc...
end

Then you can’t define additional heads for Foo.some_fun without modifying the file that defines Foo.

Protocols let you break that definition apart, so that each head is part of a separate defimpl. That also means that libraries can define functions (for instance, Enum) that work with user-defined types which behave differently.

In your own application code, it’s usually clearer to just modify Foo.

8 Likes

Elixir is strongly typed (that is to say things have types). Generally we write code with lots of implicit and explicit type-checks (e.g. pattern matching, guards, etc).

The language is (was?) not statically typed, that is to say there is no automatic type checking of e.g. return values of functions. You can add hints (typespecs) to all of your functions and that gets you most of the value (documentation).

However, the language is converting to static (gradual) typing now anyway. There has been a lot of real progress over the last couple releases and it seems like within a couple more releases Elixir will effectively be a statically-typed language.

Can you elaborate on where you’re having problems? (Other than protocols which were covered well by the above reply.)

6 Likes

Truth is, when I’m coding Elixir, I’m usually thinking about types just like I would if I were coding in a strongly/statically typed environment. Now admittedly I’m not thinking of types as classes, nor in any fancy or sophisticated way: I simply expect certain kinds of values to be certain kinds of things and do so consistently throughout the codebase.

What is different is that I expect minimal help from the compiler in making it so. At boundaries in the application I’m enforcing things with guards as pattern matching, and despite it’s issues I’m using Dialyzer as well. As they become available I’ll incorporate the set-theoretic typing features. None of these will replace the comfort of static typing, but the truth is the measures discussed are sufficient enough that type related errors haven’t been a significant issue.

I do think there is a larger danger in trying to find technical fixes for typing beyond those existing tools intended for the task. It’s a l losing battle that’s likely to result in more issues rather than fewer.

6 Likes

This is indeed a problem that I have because the moment I start to use these guarantees, then I kind of start to shift towards trying to check all inputs, and then everything falls apart.

This is something that I need to research to understand what the type system will be able to do. For example, will it be typed as Java is or more like Haskell/OCaml? These kind of things.

I think it’s more the mindset of just using static typed languages for a long time. I keep asking myself things like “am I handling all the error cases?” or “what are the types of errors that could be returned?”. This becomes a huge problem for me. Maybe I just need to write more tests and simply let it go from these kind of guarantees :man_shrugging:

What you define as a boudary? For example, if we do this per module, it means doing this for the whole code :sweat_smile:

Do you have any good reference to understand what will be possible or not with the set-theoretic typing?


Another thing that I struggle is the concept of nils, damn, how do you handle the case of an entity that can have multiple fields and a group of those fields could be empty or not. It’s a hassle to pattern match/guard every single case.

1 Like

Generally functions will document what they return through typespecs/docs. So if you have a function which returns {:ok, value} or {:error, :not_found} you can pattern match it like so:

{:ok, my_value} = function(:whatever)
# or
case function(:whatever) do
  {:ok, my_value} -> my_value
  {:error, _error} -> nil
end
# and so on...

This is all runtime error handling. If you have Dialyzer set up it will warn you if you’re missing clauses (it’s like a static type system). It’s not great, but I like it in my editor (through ElixirLS). It does often catch things I’ve overlooked.

There are a few cases now where you will receive static type warnings directly from Elixir (mostly struct stuff for now). This functionality will continue to grow over the coming releases.

Can you give a code example?

1 Like

What is meant by boundary can mean different things. In my case, my application is made up of a bunch of little but very focused Elixir library projects. Each of these projects presents an API for its dependents to use. I resolve any typing or other validation issues at that API boundary. The API boundary is also very thin: it validates and passes the call on to internal implementation logic placed in “non-public” modules as needed. I do not fret about types or validation inside the internal logic: I assume that each of these little projects is small enough that keeping track of it’s types in the implementation code is simple so long as the external world only passes valid values. This pattern doesn’t require full-fledged Elixir projects to implement… the idea is the same: be rigorous at the edges of however your code is organized.

1 Like

But at this point you’re paying for the types without having the benefit. No?

For example I’m working on a software right now that the user can get into the application without an email and password by using a secret key that it receives during the account creation, this is for extra privacy. So email is something that can be present or not. But this is one example with just one single field, it could have been 10 fields following a similar pattern.

This makes a lot of sense and it would have the benefit of dynamic and static typing. You make it “static” at the boundaries and dynamic inside, it’s a win-win situation in my opinion.

1 Like

Dialyzer is a type checker that exists. It might not be as rigerous or featureful as people like, but it’s also far from no type checking.

Though I’d also argue that developers being able to read types is useful in itself even if there’s no automation using those typespecs. You don’t need to be hunting down the complete implementation to understand the shape of the data being handled – that probably sounds weird to someone coming from static typing, but that exists in dynamically typed languages – but you can be somewhat certain of the data expected and returned.

3 Likes

Gradual set-theoretic types — Elixir v1.18.4 is probably the best starting place for this question as it tells you what’s there now as well as having links to the document which lays out the vision.

I want to echo what @LostKobrakai said. I define typespecs both for all of my public functions and will typically also have a types.ex file/module which defines the various types of values that the project expects. I do use Dialyzer, so they play a role there, but I would continue to define typespecs even without Dialyzer. Frankly, I find the documentation value more important than the Dialyzer support they provide. And given that dynamic typing does shift some of the burden of more formal type enforcement to the developer, I find the documentation via typespecs becomes more important to quickly resolve any typing related questions. Luckily, much of that documentation typically is incorporated into the tooling in tool-tips, popups, and such.

I would go further and argue that documentation is the primary benefit and actual type-checking, while helpful, is secondary.

1 Like

I think I understand what you’re getting at here. I’m not sure this problem is really Elixir-specific, though, so I’m curious what it is you’re missing in Elixir vs. other languages.

You can do stuff like this:

def greeting(user), do: "Hello, #{name_for(user)}!"

defp name_for(%User{email: nil}), do: "Anonymous"
defp name_for(%User{email: email}) when is_binary(email), do: email

Or for some cases this could be simpler:

def greeting(user), do: "Hello #{user.email || "Anonymous"}!"

If you have complex cases with many optional values it’s up to you to structure your code to enforce whatever constraints you are expecting. Again, this is something which is true in any language. I don’t think a type system buys you that much here.

1 Like

Yes, not at all specific to Elixir, but other languages would have builtin features to handle these situations. For example I could do this Option<String> and it will force be to handle the case of that value being a None.

I still did not face the case where I could have many fields that could be empty or not, I did in Rust, and it was okish to handle given the compiler checks.

I’ll also read the paper.


Thanks everyone for the help!

2 Likes

Yeah, if you want this at compile-time that’s the realm of a static type-checker. Dialyzer will catch some of these, and down the road the built-in type system will catch the rest.