Posted by cloud8421 1 day ago
But yes types are necessary for enterprise adoption. Even more important for agentic adoption.
I don't know the current state of Gleam OTP, but last I checked it wasn't great.
If you don't care about either of those things and only about types, use Gleam. But then why not just use Rust?
> I don't know the current state of Gleam OTP, but last I checked it wasn't great.
Gleam uses regular OTP, it doesn't have a distinct OTP framework separate from other BEAM languages.
This is the same as in Elixir, where macro-enabled APIs are offered, and they just wrap the regular Erlang APIs.
The BEAM?
I wrote both Elixir and Erlang code. Erlang is just useless to me as a programming language; it has many great ideas though. I love the idea of being able to think in terms of immortal, re-usable, safe objects (Erlang does not call these objects, but to me this is OOP by Alan Kay's definition. I don't use e. g. the java definition for OOP.)
Elixir built on that and made Erlang code optional, meaning people could write more pleasent code. And here it succeeded. I am not sure why Elixir succumbed to type madness now, but the comment that "writing Elixir is like writing Erlang", is just simply not true.
Elixir is significantly better than Erlang with regard to writing code. José Valim got inspiration for Elixir from ruby, to some extent.
https://gleam.run/frequently-asked-questions/#Elixir Here’s a non-exhaustive list of differences:
Elixir is gradually typed, while Gleam is fully statically typed.
Elixir's type system does not have generics, while Gleam's type system does.
Elixir has a powerful macro system, Gleam has no metaprogramming features.
Elixir’s compiler is written in Erlang and Elixir, Gleam’s is written in Rust.
Gleam has a more traditional C family style syntax.
Elixir has a namespace for module functions and another for variables, Gleam has one unified namespace (so there’s no special fun.() syntax).
Gleam standard library is distributed as Hex packages, which makes interoperability with other BEAM languages easier.
Elixir is a larger language, featuring numerous language features not present in Gleam.
Elixir has an official test framework with excellent support for concurrency, partitioning, parameterized tests, integrated error reports, and more. Gleam has no official test framework, but there are multiple community-maintained frameworks.
Both languages compile to Erlang but Elixir compiles to Erlang abstract format, while Gleam compiles to Erlang source. Gleam can also compile to JavaScript.
Elixir has superior BEAM runtime integration, featuring accurate stack traces and full support for tools such as code coverage, profiling, and more. Gleam’s support is much weaker due to going via Erlang source, resulting in less accurate line numbers with these tools.
Elixir and Gleam both use Erlang's OTP framework. Both have additional modules for working with OTP, which provide APIs more in the style of each respective language. Both common use Erlang's OTP APIs directly, but Elixir can do so more conveniently and concisely due to having a less-strict type system.
Elixir currently has superior deployment tooling, including support for OTP releases and OTP umbrella applications.
Gleam’s editor tooling is superior due to having a more mature official language server, but Elixir has recently announced an official language server project which is in active development.
Elixir is more mature than Gleam and has a much larger ecosystem.
Gleam and Elixir compile at similar speeds due to using the Erlang compiler as their compiler backend. Elixir's macros are evaluated at compile time, so a program that uses macros will take longer to compile the larger the amount of work performed in macros. Gleam has no language features that result in slower compilation.
https://gleam.run/cheatsheets/gleam-for-elixir-users/
This has to much content to reproduce.Gleam for example has issues with verbosity of decoding/encoding json whereas in Rust you derive serde and in Elixir it's just a function call away.
Elixir has a more mature ecosystem. While you can for example use Phoenix with Gleam (or some other Gleam framework) the experience just isn't the same.
The big draw with Gleam over Elixir is the typing (where Elixir is now closing the gap) and being able to compile to JavaScript (which is also what Hologram is doing for Elixir).
I prefer Gleam's typing system and the Rust-like syntax, but for now I feel Elixir is the better choice for all my web dev projects.
Apparently it is not that difficult to add different compiler backends. There was a presentation [0] recently about adding wasm support as a compiler target. The implementation was quite far along, including support for the wasm component model.
Is it just being used as a marketing term?
Long answer, well, there are blog posts[0], the Design Principles of the Elixir Type System paper[1] and related presentations[2, 3, 4] that talk about it at length. Giuseppe Castagna’s site has many more related papers: https://www.irif.fr/~gc/topics.en.html
[0]: https://elixir-lang.org/blog/2022/10/05/my-future-with-elixi...
[1]: https://www.irif.fr/~gc/papers/elixir-type-design.pdf
[2]: https://www.youtube.com/watch?v=gJJH7a2J9O8
[1] https://www.irif.fr/_media/users/gduboc/elixir-types.pdf
in the agent of agents this will probably give us a big boost though so thankyou Elixir team
I've never followed Elixir particularly closely, but what I saw in some Erlang discussions was different. Discourse there was that you need to gracefully handle failure anyhow, so type errors can (should?) just get handled by the failure recovery machinery you're supposed to have anyhow. I disagree with that point of view, but it's much more defensible than "$LANGUAGE is magic".
He gives a lot more nuanced take than 'types are useless', which is more like 'types are less useful than people think in the context of Elixir development'. (Which makes sense because he's in the middle of implementing a type system for Elixir.)
With no insights at all into Elixir this sounds like a reasoned and defensible, if not outright correct, position.
The proposition I'm working with is "types are more useful than people think in managing a horde of degenerate short-cut taking co-workers whose failures I will be blamed for openly and quietly regardless of actual fault". Gradual typing is an interesting and appealing compromise, I'm gonna have to give Elixir a serious try.
Compile-time checks don't obviate the need for runtime error handling, and I love the robustness of Erlang's runtime error handling. However, that doesn't change the fact that we should be catching and handling errors as early as possible, and there's a whole bunch of logic errors that you can easily catch at compile time.
Since any node in a cluster can be updated at any time and Elixir/Erlang code on the BEAM is designed make it easy to pass function calls to other nodes you don’t have any way of guaranteeing the Type contract between nodes. Types create a sort of false confidence in those situations where pattern matching handles everything very cleanly.
Example: You may not need to match on a full type, just a specific element name in a hash.
When people say Elixir doesn’t need types it’s not claiming that types are without value. It’s a claim that the mechanisms that already exist are enough without the added complexity.
I appreciate the gradual approach so that we can lean on both.
I've seen internet commenters say China is overstating its economic numbers to look more intimidating, and that China is understating its economic numbers to receive more favourable WTO trading terms, but somehow these two camps never called each other out, which makes me think they're the same people believing that China is both overstating and understating.
The thing you DO hear a lot, though, is that you don't need to worry about bugs nearly as much as you do in other languages. But that's not because Elixir is "magic", rather, it comes from Elixir's runtime (Erlang/BEAM) providing best-in-class fault tolerance primitives like lightweight process isolation and supervision trees.
In practice that means the blast radius of bugs is generally tiny and any resulting crashed processes are often recoverable. The phrase you often hear is "let it crash", since the effort that goes into exhaustive defensive programming is usually more costly than the bugs you'd be trying to prevent.
Maybe the things that made this transition feasible are the "magic" that used to make people say "Elixir doesn't really need types". Maybe what they meant was something like "Elixir is an orderly language in a bunch of ways that makes the lack of static typing less painful to me than usual".
And I guess we'll see how much people get out of this when they add type annotations later. Maybe the value add will be big after all, and then they'll really be proven wrong. But I can sort of imagine how the apparent contradiction fits together.
BASIC, Smalltalk vs Strongtalk, Common Lisp, Dylan
It is the eternal September.
So it is possible new theory was actually needed to preserve everything that was judged more valuable than types.
In any case, most of these questions are starting to become less relevant as we switch to having robots doing the programming instead.
Now the question is how to typecheck natural languages.
It’s possible that position was correct before set-theoretic type theory was developed.
and to that point around typing feels like the same wish-washy hand waving from the community that is very off putting
BEAM has genuine use cases but its not as wide as its made to believe. There are very good places where that is a perfect fit but it simply cannot upend Typescript.
Elixir feels very similar to how Clojure started getting traction and then ultimately forgotten apart from its die hard fans, I'm not saying Elixir will go the same way but seems very hard for something new and bold to replace what is popular and boring.
I do want Elixir to succeed (also Clojure as well and I advocated for it for a bit) but the low number of jobs still puts it in similar proximity to Clojure but BEAM I think would still provide uplift where Clojure simply could not
I maintain more than 20 packages and, except for the major ones, like Phoenix and Ecto, they haven't been updated in more than a year and yes, they are all fine.
The language has been extremely stable. There has been almost no breaking changes in over a decade. Case in point: we introduced a whole gradual type system without making any changes to the language surface! The language is still on v1.x!
You think all software breaks every 6 months, what happened Im curious
Or even that, the very same ecosystem congratulates themselves on the typing system but still relies on linters because the language and runtime themselves allow whole categories of dumb ideas to be written?
Unfortunate, since it's one of the flagship Elixir packages, but I think the upgrades are worth the trouble. Better to improve something than to leave it broken solely for the sake of legacy compatibility IMO.
Then eventually they add static types. Happened to Python, JavaScript, Ruby... I'm sure there are more.
Statically typed languages put the onus on the caller to transform the data into the shape(s) required.
Dynamically typed languages put the onus on the called to handle anything.
That is, in a dynamically typed environment your function has to defensively code for every possible type it could be handed.
It's not about that at all. Static types give you errors reliably at compile time instead of randomly at runtime, better documentation of what the code expects (people writing dynamically typed languages eventually resort to type comments), working IDE support, reliable refactoring and better code, all of which results in faster development.
The cost is a more complex language, occasionally difficult-to-write types, and very occasionally impossible-to-write types. But those are very very minor in comparison to the pros.
This has never been an issue in Elixir, because instead of a comment, you'd just improve the pattern matching in the function definition.
def blah(%{students: [%{firstname: firstname, lastname: lastname}|[]], count: cnt}) when is_int(cnt):
fullname = firstname <> lastname
Is a valid function declaration, which specifies that blah takes a dictionary that contains at least 2 keys, :count, who's value is an integer, and :students, who's value is a length-1 list who's first element is a dictionary that contains the keys :firstname and :lastnameReally? All the Elixir fans were saying that?
I love everything about Elixir, but Elixir constantly makes me doubt myself like no other language. My brain isnt made for functional stuff, but this makes me want to try again.
Sucks that it's not really a beginner friendly ecosystem and usually, when having questions answered, people assume you already know a lot about the language.
don't let the title fool you - the first half of the book is just elixir
over the past 8 years this is the book i've used to ramp back up on elixir and it works like a charm every time - i've never finished it
for me, a mark of a good programming book in this tutorial-project style is that I have started it half a dozen times and never finished it because at some point before the end I've been equipped w/ the tools to go off and do my own thing
The upgraded versions are mostly the same, but the differences in Phoenix 1.7 are enough to break the tutorials enough to confuse a newbie. Now, in the post-LLM age, that's not nearly as bad. But it was a real pain when I was learning.
There's a guide in the LiveView docs that walks you through the security model. To be clear, you need to always assume that the user can send you anything. That's a fact of any networked system: Clients need to be assumed to be completely under the control of an evil user, because at the end of the day it is impossible to know whether you're talking to the client you wrote, or some evil program written by an adversary. Any function that acts as a handler for an event/message can be called by the user, at any time. You have to use session/socket state to handle authorization.
The latter doesn't seem to be the case, and if it is would be alarming. The former is absolutely the intended behavior. The client can send events to the server, that's how the whole thing works. If certain events shouldn't be available at certain times, you need to check that server side, and that's going to be true in any http handler.
Exactly this, didnt know how to phrase it as it was a while ago where i had this issue.
And thats absolutely not true for any HTTP handler as there's no way for people to easily break out of the intended behavior.
This may sound crazy but when any interpreter boots up, but I feel it especially with BEAM, that needs to be your "let there be Light" moment. That's your world, that state is yours and only your will decides what changes.
So yes you can call all functions in your module, that's indeed how it works. But that's your module and that function mutates your world.
Just like you filter what people tell you based on your knowledge, you do the same here.
Most of my methods start with guard clauses.
`return if condition_not_met`
Don't touch my state if I don't agree with what you want me to do.
In Ruby it's essential cause that's how we get RuntimeErrors all over the place. In Elixir it's way easier to do, with pattern matching. And easier since state is what enters the function and will be what leaves.
If you keep this in mind you should inherently write safe code, because in protecting your domain through guards you basically close the door for exploitation by unknown means.
I'll give you one example I just thought of. Where I work we run Rails since the time before time, and as such had a lot of technical debt.
Around Rails 5 or 6 what we call `ActionController::Parameters` had a breaking change. Basically this module processes parameters received from HTTP requests.
Beforehand it just wrapped all it got and handed it over to us. But now it expected us to tell it what to expect. And if didn't find what it expected it blew up with a bang!
Horrible for our hundreds of controllers with `controllers * 4` html templates where all the form keys were hidden.
We either had to add the conventiely available `permit!` call, or find the form keys for all the forms, and add `permit(:name, :address,...)`. A shitload of work before AI.
I ended up monkey patching Rails to generate the lists for us instead of crashing. And for the point of this entire story...
The defaults of most frameworks are very safe, but they require the most verbosity so the framework knows what to expect and to guard it. But there always exists easier and faster ways to the same goal, but it's generally a trade. You get ease, you sacrifice some security.
Don't get in that habit and you'll be fine. And spend a lot of time thinking what could go wrong and guard against them.
Sometimes posts don't get traction due to ambiguity, and some smelled like "do my homework" so people ignored them.
But every post with a genuine curiosity in it gets answered, as far as I can tell.
Elixirs community is great. Its just hard to learn because it's not yet widely adopted, there are no (non senior) roles for it and it's a lot of work understanding all the BEAM concepts. A thing just being interesting isn't enough motivation for me to learn, I need a bigger goal but with Elixir there do not seem to be any.
My last experience with it was building something with Phoenix Liveview until I noticed how easily you can hijack the websocket and just spam random commands to your server or temper with payloads (with regular webapps ive built i never had this issue). Which made me quit that project.
One thing that really helped me pick it up was saying YOLO and rewriting one part of the business stack from Ruby on Rails to Elixir. It taught me quickly and well.
The official guides are also great and IMO you can get through them all without a rush in two weekends. But again, if you don't want to then don't.
You can also try asking right here in this HN thread. Maybe I or others would be willing to give you a more detailed response.
Every new paradigm is confusing if you don't put in the work to learn it. That's just how the mind works.
What's important is what you get after you don't give up on it long enough. And that, on BEAM, is a hilariously OP superpower of effortlessly[1] parallelizing and distributing workflows. Then there are Elixir macros and the OTP supervision model. The addition of gradual typing is huge, and when the annotation syntax lands, I will definitely switch to Elixir for everything on the backend.
In any case, the only thing I can tell you is that learning Elixir is worth enduring the confusion. From personal experience, it's just a matter of learning it bit by bit over time - there's a finite set of "confusing" ideas in the OTP/Elixir/BEAM mix, and learning about some of them every other day works wonders over a few months.
[1] An exaggeration - I know! But it does make it much easier to implement parallel and distributed workflows. Recently, most of the important languages finally started getting their m-n concurrency models (from Java to Python), so the BEAM is not as much ahead on SMP, but for distribution (you can send closures to execute on different machines transparently!) it is still in a league of its own.
Check this out: https://www.theerlangelist.com/article/spawn_or_not
Written by one of the very best Elixir mentors. I believe it will dispel most (hopefully all) of your doubts and clear things up.
I'm not sure what a ghost process is? I guess something that's living beyond its usefulness / isn't supervised, etc? ... I don't speak Elixir, but you can do the equivalent of this Erlang to see everything on the node:
rp([{X, erlang:process_info(X)} || X <- erlang:processes()]).
Then you'll know what's going on. Caveat: if you have a lot of processes, that's going to use a bunch of memory; for production you probably don't want to use erlang:process_info/2 with specific items instead of the default items. And you might don't want to output something for all the processes if you have a lot of "normal" processes that won't need to be listed.> "what if I spawn too many processes",
The default limit is 1,048,576, if you want to have more, you can add +P X to the erl command line with a bigger limit? Have your monitoring alert you when you're at ~ 80% of the limit.
> "what if this architecture is bad compared to...",
This probably addresses the real question of your too many process question. If your architecture is bad or if you spawn more processes than a good architecture would, your performance will be bad. If your architecture is really bad, you'll have a hard time solving the problems you're trying to solve. Future you will look upon your system and despair; you may also despair in the present...
Eh, you're going to make bad architecture. BEAM won't solve all your problems. But, if you've got problems it can solve, IMHO, it can be a very nice way to solve them.
> "when to kill processes",
Kill processes (or let them crash) when they misbehave. Kill them (or let them exit normally) when they've done their work and they don't have anything else to do or wait for. When you spawn a process, you'll often have a pretty good idea of the conditions that would lead to its death... Ex: if you spawn a process to handle a connection, it should probably die around the time that the connection ends. If you spawn a process to handle a request, it should probably die when the request is handled. If you spawn a process to listen for connections, it probably should die when you don't want to listen anymore. Etc.
> "whats the correct restart strategy for this"
Well... it depends. Almost never the default strategy. The default strategy is a big foot gun; at least it is for Erlang, maybe they changed it in Elixir. I need zero hands to count the number of times I actually wanted BEAM to stop because some supervised process failed 3 times in a small time frame; but it's happened to me a lot more times than that. For per connection or per request things, the appropriate strategy is not to restart at all; for other things, try to restart a few times quickly then maybe every minute or so is probably sufficient. You'll want some sort of alerting. And if the restart strategy isn't right, you can always console in and poke it.
That being said, I am not forced to use liveview, its just that most ressources nowadays use it.
You can always ask follow up questions for clarification, people there are generally really friendly.
But yea I know about Gleam and I did build some fourier transform stuff with Rust a while back. I like Gleam generally. I am just much much slower with FP and think its extremely unintuituve compared to, say, Go for example.
I experienced this really painfully when I was in college and took a kind of "survey of programming paradigms" course and tried Haskell for the first time. I'd been programming for years by then, and I couldn't believe how helpless I was at trying to complete things that had long felt "basic" to me.
But I don't think it's about the brain not being suited, I think it's that contrast of your experience level in imperative languages vs. the fact that when working in a pure functional style, you start out as a newbie again.
I think you'll gradually improve. I think the thing that finally made functional programming feel comfy for me was realizing how much I love composing code that basically feels like more generously spaced Bash "one-liners". The data starts out in one shape, so you run a command to dump it. Then you think of a step that gets it closer to what you want, you pipe it to that next command, and you take another look. And you keep going and at the end what you're looking at is typically pretty close to a series of transformations of data that you never mutate!
Part of what makes this feel comfy in the shell is that you build up that vocabulary of commands just by puttering around your file system every day. Over the years my library of familiar "functions" in a Unix-like environment has grown quite large. In a pure functional programming environment, you have to do the same thing but it takes a little more effort to learn the vocabulary. Your most frequently used "commands" will be functions like map, fold, and zip instead of grep, cat, or sort. But the core of it is really the same, and what I love about building pipelines applies equally to both: you can build it piece by piece, and for each puzzle you're on, you can forget about the previous steps and just think about the next transformation of the data that's in front of you. There is something refreshingly, relaxingly low-context about that.
Anyway I hope you give it a try and enjoy it. When we can learn to enjoy being bad at something, that's how we finally get good at it.
When I was in university, the introductory class was about Java, and an advanced class in the next semester was about Haskell. There were many imperative/functional newbies in both classes, but the Haskell class still progressed much more slowly. Haskell is simply much harder to grasp, independently of experience.
You can also see this in the fact that even mathematicians use Python rather than Haskell for simulations. Despite the fact that there is no population that is better suited for Haskell than mathematicians.
Even cookbooks are always written in an imperative style, never in a functional one. Why is that? Human brains find imperative algorithms simply more intuitive, and this is not explained by not being used to functional ones.
Religious texts, philosophy, ethics, and even self-improvement books often don't provide a procedure to follow. They teach things like how to handle conflict, how to act fairly, how to navigate difficult situations, or how to reason about competing values.
People then take those ideas and apply them across many different situations in their daily lives. In a sense, they build a toolbox of reusable mental functions rather than memorizing a single algorithm.
That's also why many people finish a self-improvement book feeling like they didn't get much out of it. They were expecting a recipe. Instead, they absorbed a collection of abstractions that only reveal their value when applied later in real situations.
The fact that cookbooks are imperative mainly shows that procedural tasks are naturally expressed procedurally. It's not obvious that this generalizes to human reasoning as a whole.
Once you taste Elixir/Erlang, there is no going back to the madness.
Jank wants to be this, right? IIRC its author and chief maintainer was a game dev before he dedicated himself to the language.
Maybe porting your engine would be a great way to prove out Jank 1.0 when it arrives ;)
Sounds like there is some foundational knowledge of Elixir that you miss and everything seems more confusing than it should be. To me writing a 'server' in Elixir is orders of magnitude easier than doing it in Python, Rust or C++.
As someone else suggested, bring your concerns to the Elixir Forum and surely someone will clarify them for you
OMG, why? Why would you ever have so many processes? All of them at the same time? Are you going to animate a 3D scene and run a process for each vertex, or something?
No, I mean, if you're WhatsApp - across all nodes - then somehow maybe yes? At scale. But in normal code, slicing workloads too thinly is counterproductive, and having even tens of thousands of processes is a sign that you're slicing it way too thin. Message passing between processes is cheap, but not free. Schedulers do a good job, but rarely have more than 16 cores to work with. And so on.
You can have that many processes if you want, to be sure. But if you're struggling with it, why would you want it?
Reading your comments in this thread, I have a feeling you just didn't spend enough time reflecting on how you want to use Elixir. In effect, you also failed to consider how exactly you should learn it. For example: Elixir is a perfectly capable procedural language. Start by writing CLI tools, without spawning any processes at all. Then try to parallelize their processing. If the tool accepts a list of files as arguments, use a `Task` to compute return values for each file. Tasks are processes, but with a particular contract that simplifies their usage. Later, you can experiment with error handling and supervision by putting the tasks under a supervisor. And so on. You go from the familiar to the less familiar, with a useful, working tool every step of the way.
I mean, we had one process per client connection (which is 100% the way to go) and depending on the era, hundreds of thousands or millions of connections per chat node. I don't think we ever really summed the number of processes over a cluster.
Other than client processes, there weren't that many processes per node; like you say, it doesn't make sense to spread too thin.
There's a lot of client connections and so a lot of client processes, but it ends up being pretty simple to work with them. They all do the same thing... wait for a message, process the message, wait some more. Some of the messages are tricky to process (like the user just logged in again over here, so please transfer the state)
That's a bit of a misrepresentation. Error handling on the BEAM has a few more layers than in other environments; specifically, the supervision tree can be used to "let things fail". That's not the layer where you should log or handle failures - that's a safety net that ensures your whole system won't go down if your error handling in a single process doesn't work.
For error handling, there are roughly these layers:
- functions can return {:ok, value} or {:error, error}
- functions can raise errors (similar to exceptions) that can be caught
- processes can be monitored from the outside, you get notified when they die
- processes can be linked and exits can be trapped, also notifying you on failure
- supervisors can handle process deaths in a configurable manner
- higher-level behaviours often expose their own error handling callbacks
So there's a bit more to error handling on the BEAM, and I get that becoming familiar with all of them and using them properly can be a challenge. The defaults skew towards high-availability, which is not always what you want in development - sometimes, failing fast and completely (up to stopping the app or the BEAM as a whole) is more convenient. You can have that; you just need to ask for it specifically in your code.That's a choice, but it's not idiomatic.
You're expected to write things like...
ok = thing_that_might_not_work().
(Well, that's what it looks like in Erlang anyway). If there's an error, it doesn't match, so it crashes. You don't have to check for success, but it's easy to, and 'let it crash' is the mantra, so yeah. Then you watch for crashes, and fix them with hot loading, and pretty soon you have a reliable system.Let it crash ends up not quite working, so you end up catching a lot of errors, but you should be logging them, not swallowing them...
EDIT: I see my cohort has already given you this suggestion :P
``` socket "/ws/:user_id", MyApp.UserSocket, websocket: [path: "/project/:project_id"]
```
Elixir gives you too much freedom on how to write something on a syntax level which really annoyed me.
I pretty frequently find myself needing to open up the source to understand what's actually going on, the docs aren't bad but it often feels like they assume a lot of existing familiarity with phoenix.
In this example, `socket` is a compile time macro and it's being called with
path = "/ws/:user_id"
module = MyApp.UserSocket
args = [
websocket: [
path: "/project/:project_id"
]
]
and what is does is register that data with the `phoenix_sockets` attribute inside the module you called `socket` from. At compile time that gets turned into a lookup inside your module, and presumable then the UserSocket module is invoked when a websocket request hits the specified path.Would you find it more clear if socket was called like this?
socket("/ws/:user_id", MyApp.UserSocket, [websocket: [path: "/project/:project_id"]])
Or, alternatively, would it help if the endpoint was more specifically defined like defmodule MyApp.Endpoint do
use Phoenix.Endpoint,
otp_app: :my_app,
web_sockets: [
socket("/ws/:user_id", MyApp.UserSocket, [websocket: [path: "/project/:project_id"]])
]
endComing from other languages, I find that
example("with", 3, extra: "arguments", as: "a", keyword: "list")
being equivalent to example("with", 3, [extra: "arguments", as: "a", keyword: "list"])
and example "with", 3, extra: "arguments", as: "a", keyword: "list"
always takes some extra mental effort to get through, especially when there's no parenthesis. But I appreciate not having to write all the extra brackets and parens when I get going, so I think it's a fair tradeoff.Personally, I like the flexibility, but yes there are a lot of rules to keep in mind.
example("with", 3, [{:extra, "arguments"}, {:as, "a"}, {:keyword, "list"}])
iex> [{:extra, "arguments"}, {:as, "a"}, {:keyword, "list"}] = [extra: "arguments", as: "a", keyword: "list"]
[extra: "arguments", as: "a", keyword: "list"]This is true perhaps compared to python or go, but not compared to Java, JS/TS, or some others.
> socket "/ws/:user_id", MyApp.UserSocket, websocket: [path: "/project/:project_id"]
Socket is a behavior, which is like a trait or interface. MyAppWeb.UserSocket implements the behavior. It's basically a convenience over having to write a bunch of repetitive WS or long poll handling every time you want a socket like thing. Its pretty well documented https://phoenix.hexdocs.pm/Phoenix.Socket.html.