smueller

smueller

With the announcement of 1.19 rc0 and the path to user-supplied type annotations, I want to make the case for inline types—both for function parameters and local variables—rather than the currently implied direction of out-of-band annotation signatures using a $ prefix. The examples shared so far suggest something like:

$ integer() -> integer()
def inc(i), do: i + 1

This pattern – separating types from function heads – is unusual and, in my opinion, a step backwards compared to almost every modern language that supports types.

Why Inline Type Annotations Matter

Clarity and Locality

Types belong next to the values they describe. Splitting types from the function definition forces developers to mentally reconcile two separate lines:

# Inline (clear and self-contained)
def inc(i: integer): integer do
  i + 1
end

# Out-of-band (disconnected)
$ integer() -> integer()
def inc(i), do: i + 1

Inline syntax offers immediacy—everything needed to understand the function is in one place.

Developer Familiarity

Languages like TypeScript, Swift, Kotlin, Rust, C#, and even Python (via PEP 484) all use inline typing for a reason. It’s readable, discoverable, and familiar to modern developers. A $ line above each function feels more like a callback to the days of @spec, and misses the opportunity to modernize.

Better Tooling & UX

Inline types play better with IDEs and editors. They improve autocomplete, refactoring support, inlay hints, and inline docs. Detached signatures complicate tooling and reduce feedback during development.


Variable Type Annotations Are Just as Important

It’s not just function signatures. We need a way to annotate local variables inline:

x: integer = some_potentially_dynamic_value()

case y: String.t() <- some_api_call() do
  ...
end

Other languages with gradual typing (Python, TypeScript, Julia) allow inline variable annotations. Without this, dynamic code within functions remains opaque to tools and reviewers, limiting the power of gradual typing.

Obviously retrofitting types on elixir is complex, and everyone appreciates the care being taken by José and the core team. But inline types—both for function signatures and variables—are essential if Elixir wants to deliver a type system that feels natural, modern, and ergonomic.

Would love to hear thoughts from others, and to also hear from the core team why it appears that $ annotations have so much momentum already!

Showing Posts 1 to 10

plcholder

plcholder

Interesting, I have no authority on type theory, I had a similar thought also. I do wonder how would inline work with say a complex pattern match it may look a bit too noisy versus the out-of-band look, you make a good point with dx is it possible both can be implemented and one is just syntax sugar of the other etc?

felix-starman

felix-starman

There’s an extremely long thread about this from when this was first discussed, and the main reason is for backwards compatibility and the ability to undo it if the community decides they don’t like it.

Doing it inline would basically require it to be a 2.0 bump and make it difficult to “try it out” or reverse course.

With the current approach, once type annotations are fully supported, you could implement your own inline approach and if people liked it, adopt it, and maybe the language could evolve that way anyway, but there’d be more flexibility to try different things out along the way

cmo

cmo

What they said :up_arrow:

We do not need a way to annotate local variables. You might want one though. We already have when is_binary(y) and the compiler will hopefully know what type the functions return.

Do you want to be able to type a value to integer when the type system has determined it to be dynamic?

smueller

smueller OP

I haven’t seen that thread, but the logic seems completely unintuitive. Inline types aren’t some new breakout syntax that’s never been tried before – we’re talking about 50 years of programming languages new and old that have consistently proven (and chosen) the best thing. Why sacrifice so many benefits for the optionality of backing out due to community backlash? That’s just mitigating the wrong risks, while being indecisive at the same time.

If it’s significantly harder to implement the more proven and established way (inline typing), then the team should just say that. There would likely be more support for the annotated $ approach.

smueller

smueller OP

Interesting choice of terminology to quibble – why not go further and ask whether we need any type system at all?

See the other recent thread about perspective on Rust / Kotlin / Scala developers and whether they would be compelled to switch. https://forum.elixirforum.com/t/do-you-think-the-new-type-system-would-be-enough-to-attract-folks-that-are-also-interested-in-ocaml-haskell-and-scala

The direction that’s being advocating in this post is focused on building great language ergonomics and compiler trust, and the downstream effect is ultimately more elixir adoption, instead of continuing to be confined as a niche language

IloSophiep

IloSophiep

Let me start by saying i also thought that people are free to try out macros to inline type definitions if they want to experiment with that. You can always go from one style to the other with Elixir? No reason to think it’s set in stone i think?

But i work with the language Elm for fun every now and then which uses separate types, but at work i use C#, Typescript and sometimes even Pascal / Delphi. And i feel like the differences are not as crazy as you point them out to be? In Elm you have types for “variables” (just because it’s basically the same as a function) and often you only need to read the type of a function to get the gist of it (which is very elixir-like i think):

some_value : Int
some_value =
  42

stringify : Int -> String
stringify value =
  String.fromInt value

generate : ShoppingCart -> Account -> Invoice
generate cart account =
  ...

I don’t really see how the “clarity” of one syntax is objectively better. I would agree that there is probably more “familiarity” with the inline-style, but there a quite a few languages with seperated types that are closer to Elixir than e.g. Python is, i think? And on top of that the pattern matching in function heads and variable assignments are not something that’s very common in any of the languages you listed as “let’s look at them”, so there is more stuff going on in Elixir function heads already, i think? “Locality” and “tooling” i can only judge from using Elm and i think the Elm compiler is really good and i would rather spend my time 2h refactoring with the Elm tooling than 10min refactoring with C# tooling. I don’t see the reason why a different notation of types can make the tooling so much worse?

Sorc96

Sorc96

While there is personal preference to separate/inline type annotations, I don’t think they make a difference for the tooling. There’s no reason why IDEs should have a harder time with separate signatures.

Personally, I like inline type signatures, but having them together with non-trivial pattern matching could be very difficult to understand. Even in TypeScript, function heads can get pretty confusing, especially when some parameters are themselves functions.

I’m not sure how much value there is to explicitly typing local variables. Your examples are already handled by guards in pattern matching. And the compiler would need to generate those guards anyway, since the type check has to happen at runtime.

Finally, Elixir not having a static type system has always seemed like a red herring to me when it comes to reasons for not switching. Many of the most innovative and influential software developers have consistently gravitated towards dynamically typed languages, like various forms of LISP, Smalltalk, Erlang or even Ruby.

I know that the church of the static type has gained a lot of zealous followers on the internet, but there is always a trade off, no matter how much people like to pretend otherwise. In the end, somebody will necessarily be disappointed by some aspect of the type system, whether it will be the syntax, its capabilities or even its existence. There’s just no way to please everybody.

And then, even when the perfect type system has been implemented for Elixir, people will still refuse to switch. Because it was never really about types, but about people using what they know and not understanding that there is something actually different from the other technologies that all do the same things with different syntax.

dimitarvp

dimitarvp

I have met some of these people. Dynamic languages for them are a way to start doing stuff immediately. They view statically typed PLs as a hassle that’s always getting in the way. They want to get on with it.

I sympathize with that, a lot. But I feel that this mindset being brought to the work place is a very thin line to tread: one between “let’s deliver this in a reasonable timeframe” and a good old impatience. The former I respect. The latter: I don’t. Sprinting past proper tech due diligence is one of the best ways to accumulate tech debt.

Paying tech debt is not fun. It’s often extremely difficult, too, and I have witnessed it killing commercial projects.

All that being said, Elixir strikes a very good balance. But as your codebase and complexity grows, lack of static typing becomes more and more source of development slowdowns. Even that can be mitigated somewhat… but it does require paying the tech debt periodically. Which almost nobody wants to do.

plcholder

plcholder

Well said, static types save cost in the long run, they can help set the programmers intentions with the right invariant’s. I love writing elixir, but i always have that weird feeling that I wrote something wrong somewhere, that will comeback to bite me maybe not now, maybe in a few weeks, years etc ofc BEAM idiom of let it crash, does mitigate a bit of the worries but it is still a far cry from something like rust were you can guarantee things hold in most cases & you don’t need to write insane conditionals just to make sure some input is the right type

lud

lud

It’s a matter of taste maybe. In Haskell you have the annotations on top of the functions and I like it.

Elixir supports multiple clauses, so from that function:

def hostname(%URI{host: host}), do: host
def hostname(url) when is_binary(url), do: hostname(URI.parse(url))
def hostname(nil), do: nil

You could get something like this:

def hostname(%URI{host: host} :: URI.t()) :: String.t() | nil, do: host
def hostname(url :: binary()) when is_binary(url) :: String.t() | nil, do: hostname(URI.parse(url))
def hostname(nil :: nil) :: nil, do: nil

And I think it’s pretty unreadable.

Or maybe the type system will not collect all clauses (I’m sure it will, but it may not collect all clauses declarations), so you may have to do something like this:

def hostname(%URI{host: host} :: URI.t() | String.t() | nil) :: String.t() | nil, do: host
def hostname(url :: URI.t() | String.t() | nil) when is_binary(url) :: String.t() | nil, do: hostname(URI.parse(url))
def hostname(nil :: URI.t() | String.t() | nil) :: String.t() | nil, do: nil

Let’s simplify a bit:

$ has_hostname :: URI.t() | String.t() | nil
$ maybe_hostname :: String.t() | nil
def hostname(%URI{host: host} :: has_hostname) :: maybe_hostname, do: host
def hostname(url :: has_hostname) when is_binary(url) :: maybe_hostname, do: hostname(URI.parse(url))
def hostname(nil :: has_hostname) :: maybe_hostname, do: nil

But really my favorite is to separate the thing with a function head:

def hostname(uri :: URI.t() | String.t() | nil) :: String.t() | nil

def hostname(%URI{host: host}), do: host
def hostname(url) when is_binary(url), do: hostname(URI.parse(url))
def hostname(nil), do: nil

Note that I chose to use the function head syntax and the :: operator because if we support types only in function heads for now, and if it gets adopted and there is no going back, then we can allow it later in actual function clauses, especially when there is only one clause.

With $ + arrows well it’s a different syntax so it may be harder to inline those in functions, but I guess it’s feasible.

My point is that a type system needs to see functions as a whole ('cause you cannot chose a specific function clause to call from the outside – by other matters than passing the right arguments of course).

Where Next? Top

Trending in Discussions Top

AstonJ
As the title says, please share what you’ve been up to with Elixir. Whether that’s been learning it, looking into it, making stuff with i...
2977 91898 914
New
AstonJ
The obligatory hello world thread! Who are you and where are you from? :stuck_out_tongue:
4616 55835 594
New
byu
@chrismccord : I just saw the Extract AGENTS.md from Phoenix.new into phx.new generator commit to the phoenix project. My initial shotgu...
New
arcanemachine
I was working on an Ecto migration and I needed a timestamp. So, for the nth time, I looked up the different data types for timestamps, a...
New
alexslade
Fly’s CEO posted this recently - Turn And Face The Strange · The Fly Blog It says that Fly is going all-in on sprites, which is a worry ...
New
Herve37
We’re evaluating API mocking tools for OpenAPI-based projects and would love to hear what other teams are using. We’re particularly inte...
New
matt-savvy
Is there a word for the ~> symbol used in Version strings? Do you also just call it a Squiggle Arrow™ ?!
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New

Latest on Elixir Forum

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews