I want compile errors from router.ex, but everything compiles fine. (?)

Following the Adding Pages tutorial, I addded a new route, without creating the controller:

get("/hello", HelloController, :index)

And this compiles w/ no warnings! Even after doing mix clean ; mix compile. However, I can get a compile error if I use an unknown lowercased keyword in router.ex:

snackTime

But the following inside a router.ex scope compiles fine:

SnackTime

Is there some kind of lack of type safety for module names?

Well first there is no static typing in elixir. Second, module names are just atom, no different from any other atom:

iex(4)> Blah == :"Elixir.Blah"
true

So the only way to check it would be to actually call something on it (module_info/0 is a common thing to test). What the get is basically doing is just taking the atom you pass in for the module and the atom you pass in for the function (function names are just atoms too) and basically calling it like apply(moduleAtom, functionAtom, [conn, params]). :slight_smile:

3 Likes

Thanks!

I’m seeing the difference outside of the get(), however. E.g., after the defmodule's end:

NonexistentThing  # No error
nonexistentThing  # error

Are you saying, that NonexistentThing is simply not evaluated? Optimized out before it’s linked?

(Indeed, I see that I get this behavior in a simple mix new project.)

Simply calling NonexistentThing isn’t doing anything. It is just an atom. There is nothing to evaluate. It does not know that you are trying to reference a module. It is just like any other literal. You would not get an error if you replaced it with 4. If instead you tried using NonexistentThing.foo, you should get an error stating that it does not exist.

On the other hand, nonexistentThing is trying to reference a variable that does not exist.

3 Likes

So it could be seen as a language syntax issue. I think it’d be hugely beneficial to get warnings for mis-spelled symbols. I hadn’t realized that an uppercase “bare word” is accepted as a valid expression.

I imagine that this alternate API would provide the kind of compiler checks I’m thinking about:

get("/hello", &HelloController.index/2)

…using capture to pass in an actual function instead of stringly typed values. TBH, I like it - it’s more obvious what get() is doing.

The problem with this syntax is that it would introduce a compile-time dependency on the HelloController module. You don’t want all of your controllers recompiled when you change the router.

3 Likes

The MFA pattern (module,function,arguments) is quite pervasive in BEAM languages. For example compare:
Kernel.spawn/3
Kernel.spawn/1

and as already mentioned
Kernel.apply/3

So while

get("/hello", HelloController, :index)

may look strange coming from other languages, it’s not unusual in OTP to assemble function invocations (or process initializations) as MFA triplets.

Not to be confused with the mfa() type which is actually {module(), atom(), arity()}.

2 Likes

Also, will this not create a lot of functions in the router module?

As far as I remember anonymous functions in the BEAM are just compiled into regular functions with a mangled name in the owning module?

1 Like

Acutally, it’s all too familiar, coming from Ruby and Rails. I was hoping to get a little more help from the compiler when possible.

1 Like

Hear hear! If only Elixir had static typing. ^.^;

1 Like

When I found anew statically typed languages (at least for me, I started with them ~13y ago) like for example Rust or Elm (just examples), I was really, really hard asking myself why would I ever go back to dynamically typed languages. And what really is keeping my with Elixir (aside from community etc.) is how easily you can write truly concurrent and fault tolerant software.

What does really helps me write in Elixir is Dialyzer http://erlang.org/doc/man/dialyzer.html and to some extent Dialyxir https://github.com/jeremyjh/dialyxir Because even when I don’t write @specs it’s often smart enough to show flaws with my code only basing it on context.

Although Dialyzer won’t find that you have non existing module name in your path definition get("/hello", IdontExist, :index) i still think that mentioning about Dialyzer might help you with some problems you may ecounter.

2 Likes

Precisely the same with me, erlang was the only really big dynamically typed language I used before (bits of python at times), elixir mostly replaced erlang due to it’s macros and ecosystem (I still prefer erlang’s syntax overall).

It’s only a positive typer though, so it can catch egregiously wrong uses pretty often, but it defers to assuming that what the user wrote was correct.

And this is one of those cases. It only knows that an atom is being passed in, it doesn’t know that it needs to be a valid module as one example.

1 Like

It’s not only an issue of static typing, though. Late binding means that the module can be introduced at run-time at any point. Even if you did somehow say “Yeah, this is a module and I know that”, it seems to me you’d have the issue of basically saying “No, I’m going to force all modules to be defined at compile time and they should remain the same”, which may or may not be desirable.

The current situation is “I’m gonna call whatever I have and what exists at that moment is what I’m going to get”, which is about as far from forcing all modules to exist and stay static at compile-time as you can get.

2 Likes

Well releases on the BEAM do atomic updates of a set of modules at a time, and different processes inside the BEAM can be running 2 different versions of the code, so as long as messages are block-boxed then it would work fine in 99% of cases (and in the rest then you should know what you are doing). :slight_smile:

1 Like

Is this philosophy reflected in other areas of the Phoenix ecosystem like Ecto? I’m wondering if the framework is a good fit for me and my projects: I want the compiler+linker to do as much work as possible, not less. I’m looking for a framework that checks anything that can be checked, without imposing coding or conceptual overhead.

E.g., Swift Vapor’s type safe routes appeal to me a lot, but the framework isn’t very mature at the moment.

Just to be clear, I can’t speak for any libraries out there. It was more a general comment on the handling of modules passed via variables.

Understood! But since I don’t know the libraries well, maybe you can weigh in: is this pattern of “soft references” common? - modules referred to by name?

It’s built in to the way the BEAM VM works. Anything one wants more than that needs to be done by whatever compiler they use or via other passes either before or after. :slight_smile:

1 Like

[Hi, I’m coming back to this after a while…]

I’m wondering, “Why not?” Wouldn’t we want to achieve correctness first, and then optimize second? I don’t see the problem with recompiles.