Jose Valim: "Elixir is, officially, a gradually typed language"

Personal preferences aside, the problem I see with types in the function head is you end up with this:

def login(%User{admin: true} = user :: User.t()), do: ...

which is a bit much. Sure you can argue you could write this:

def login(%{admin: true} = user :: User.t()), do: ...

but that breaks the spirit of not changing the nature of the language. Besides, %User{} = user is essentially having the type in the function head and now we’re (maybe) going to have that codified into compile time!

6 Likes

Apologies for the tone. I jumped the gun b/c the message was from a person on your team and I misunderstood it given the question he was replying to.

Now I am happy I got it wrong.

As for making mistakes, that’s not something that scares me but a possibility of eventually falling under the influence of the “strong typers” is.

On the other hand one user on this forum correctly put it that we should have faith in the core team given the track record so far. I agree with him (but fear is a b*tch :slight_smile:

Thanks

5 Likes

This looks great, allows people to keep on using Elixir the way they always have, but hey if you want to take it up a notch you can add typed to tighten the bolts of your app. Very cool.

deft MyStruct.t() -> integer()
def some_function(arg) do
  ...
end

I like deft cause we got def and defp, why not deft for “define types”? :smiley:

7 Likes

I think because the %User{} example will cause a match error if a map is passed then its assertive code, and inference about the type should be possible?

Similar to if you had an is_struct(var, User) guard

Adding the type definition either in the function head or above it, would just be icing on the cake.

By having it be a map in the pattern, but saying in the types that it should be a user.

You get compile time checks, but lose the runtime checks. This would probably only bite you with genservers where the incoming type of a message is Any().

Right, I wasn’t thinking of loosing the runtime check in my example. You’re right, my examples have different behaviour …which actually only solidifies my point!

I’m all for types on their own line.

2 Likes

There’s so much excitement around static typing that I really want to figure out how to use it effectively.

I spent a lot of time putting in specs for dialyzer and found I spent more time debugging specs than I found bugs.

For my codebase, the compiler showed warnings or the app would just crash on that particular codepath, but dialyzer rarely showed anything useful for the amount of time spent with it. My understanding is that static typing should help in finding bugs without having to go to the runtime of that particular codepath.

Perhaps I’m doing something wrong, but the data coming in from liveview handlers from params or socket.assigns doesn’t have a particular shape to it.

For instance, if I have:

def handle_event("square", params, _socket) do
  number = Parser.safe_to_float(params["number"])
  square(number)
end

@spec square(number :: float) :: float
def square(number) do
  number*number
end

Pretty much all the data is coming in from input from params, so it doesn’t seem that dialyzer really picks anything up. Am I doing something wrong? Is static typing not going to work for this? Would adding a spec to safe_to_float make a difference? It’s not a small codebase, so I’m hesitant to spend effort on something that might not make a difference.

github.com/AlDanial/cloc v 1.98  T=0.16 s (1271.0 files/s, 320985.7 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Elixir                         142           4984           9688          26995
EEx                             55            491              0           5758
CSS                              2             84            107           1041
Zig                              4            173            151            933
Markdown                         3            311              9            833
JavaScript                       1             64            113            543
-------------------------------------------------------------------------------
SUM:                           207           6107          10068          36103
-------------------------------------------------------------------------------


I like this as well, deft feels very elixir-ish.

Does anyone know how it would work though, if you had multiple functions with the same name?

1 Like

I don’t see how this would integrate with the current ecosystem and what would be the benefit. If we assume that the new spec will behave like dialyzer, just defining a spec and declaring to explicitly throw error instead of warning on type errors should be more than enough.

1 Like

Apologies for a lack of links, but José et al. have explained that @spec is insufficient to fully express set theoretic types without backwards-incompatible changes/breaking Dialyzer, so a new type declaration will be required.

While I don’t believe there are any “official” plans, I guarantee that when/if the new syntax is introduced, some enterprising folk will build tools that ease the transition (e.g. automatically rewriting @spec to $ or whatever where it’s unambiguous).

2 Likes

This is true, dialyzer can express only union types.

The question is why deft is by any means necessary and how the new spec aka $ will not be enough.

1 Like

I don’t think deft makes sense here since we aren’t defining, we’re specifying. Types are currently defined with @type/@typep/@opaque and I’m not sure that is changing? I think it would be more like defs for defsignature, or something :sweat_smile: I really personally really like $, though.

1 Like

I agree.
Readable and clear.

1 Like

I think deft is a little more specific and unmistakable. Easier to global search, parse at a glance, etc. It could live tight right next to the function definition.

deft User.t() -> list(User.t())
def list_companies_for_user(user) do
   query =
     from c in Company,
     join: cu in assoc(c, :company_users),
     where: cu.user_id == ^user.id,
     select: c
  Repo.all(query)
end

$ can be a part of an ENV var, jquery, javascript, hooks, or just plain old cash reference in a template somewhere. Too messy and overloaded.

1 Like

It’s $ (note the space) and always the start of a line, so I’d say it’s unique enough in terms of grepability. Just limit to Elixir files.

4 Likes

Regarding deft - that name is more appropriate for private functions that need to have visibility to test suites. This is easily accomplished by using macros, and has been used in a number of projects. So it doesn’t sound like a good idea to select that keyword.

Why not using @type or @spec instead of $ for this purpose?

1 Like

I don’t think Elixir should usurp @type or @spec and pull out the rug from devs. I remember reading somewhere that the intent behind this is gradual typing for those that want it. Not “hey you’re on Elixir 1.7, it’s typed now”.

5 Likes

Nice work @josevalim ! I only know typing from Typescript and there I hate it with a vengance, but in Elixir I really like the guards and multiple function heads to catch errors early so I hope this will also help with that. My main beef with Typescript is that for me the spec gets in the way of the creative development process, so I am excited to see how this will work in Elixir. :tada:

4 Likes

It’s interesting how many of us experience the same tooling differently. I love Typescript. Having types speeds up my process a lot more than writing types slows me down; and writing types often helps me think through what I am trying to create - similar to writing a test before a function in TDD. But I can see how that’s not true for everyone and that’s a great thing about this gradual typing approach.

I’ve gotten to play with Elixir and Phoenix for just a couple months now and it is super cool. But trying to figure out what types some function takes, and what it returns, burns quite a lot of extra time and mental energy. For an experienced Elixirist it may be second nature to know if something might return a tuple or a value; and whether it takes a string or an atom; that sort of thing. But for learning/onboarding - I’m pretty stoked about this project.

It’ll save newer Elixir devs like me so much time of constantly trying to figure out what arguments a function is supposed to take, and what it’s supposed to return, when the IDE knows and you can just “hover” or open the parentheses for a new function call - and there are the argument types. And hover over the return variable, and there’s the return type. Dreamy…

2 Likes

I would 100% quit and never look back if they turned Elixir to TypeScript-like typed language. I understand people like it but I fully agree with @barttenbrinke here, for me it’s a nightmare and entirely unnecessary complication.

4 Likes

I for one think this is the most significant development in Elixir since NX. Since Seven Languages in Seven Weeks, I have looked at dozens of languages. To me, the thing that sets Elixir apart is the amount of thought and foresight that’s gone into considering the constraints of Elixir, Erlang, and all of the disparate users on the platform.

The work is amazing, and far surpasses my expectations.

11 Likes