9 ms·
Thoughts on Elixir, Phoenix and LiveView after 18 months of commercial use
- slouchpie 3y ago"if...I want a function which gets me the stats on active accounts ... should that function live in the Users, Companies or Accounts context?" Answer: It should live in a new context. I would call it `Stats`.
- FinalDestiny 3y agoAs a new Phoenix convert I agree with a lot of these points from my experience too. The functional components make more sense in the majority of cases (and I think the docs point this out) and having the duplicate auth logic with on_mount feels less than ideal. Hopefully the team will see this and make it even better!
- bobwaycott 3y agoI find a lot of engineers I work with don’t understand on_mount. You shouldn’t have duplicate logic in on_mount—if you do, that code should be in a module. You only have the logic once, but you may call the function twice (though you don’t have to). All your auth logic (and any other bits you need to set while starting up request state) should be a function in a module somewhere that returns expected values. Then, you just care to set these values to Conn and Socket assigns appropriately, keeping in mind that Conns and Sockets aren’t the same—when you have one, you don’t have the other. Thus, on_mount to the rescue: 1. Use Plug pipeline(s) to call your module function to compute and add bits to Conn.assigns (and you can add those values to the session to avoid computing again, if you’d like). 2. Use on_mount to assign_new: 2a. assign_new pulls from Conn.assigns by key for the dead render while you have a Conn. 2b. The fallback function gets that value again when you no longer have a Conn (recompute or pull from session if already there). Your actual logic should only exist in one place. And there are easy strategies you can use to avoid expensive computations if necessary. [Edit]: Not suggesting you don’t understand it, just to be clear. I’ve just experienced a lot of coworkers fighting against, abusing, and misunderstanding Conns, Sockets, and assigns.
- AlchemistCamp 3y agoExactly. Since mix phx.gen.auth handles the authN side, it’s really just authZ that will be idiosyncratic to your application logic. In every authorization system I’ve written in a Phoenix app (which 8 now), I’ve taken this approach above. Make a pipeline of rules that take a conn or socket and authorize or not based on what’s in the assigns. Better yet, at the very top of the pipleline, wrap the conn or socket with an auth struct that holds metadata like authorization status and any redirection to be done. Unwrap that struct into the conn or the socket at the end of the pipeline, where the status is resolved. This way, every request can be default unauthorized and you can apply complex, composable rules for granting permission to take various actions on resources.
- lc-0815 3y agocan you provide sample code of one of your auth you have created, I would like to learn how to code the pipelines as your mention thanks a bunch
- conradfr 3y agoIn other words phx.gen.auth generates a lot of code that may or may not in part suits your authentification needs and Phoenix gives you nothing for Authorization ;) (I am a bit bitter after implementing authentication with Firebase and authorization in a Phoenix app? Maybe ahah).
- AlchemistCamp 3y agoThis a bit too negative of an interpretation. In 100% of the apps where I've used mix phx.gen.auth, the code it's generated has been suitable. In some cases, I've used it in conjunction with a library like Ueberauth for social logins, but it's been strictly superior to older workflows using 3rd party services or frameworks that take over the whole user table. Reaching for something like Firebase or especially Auth0 has added effort in the long run in each project where I've inherited that decision. The typical end-state seems to be a soup of logic split between the 3rd party provider and inside the application. It's more difficult to reason about and more expensive to audit. Nothing is going to do your authZ for you, unless it was made with your business logic in mind. Different apps are going to have radically different needs and there isn't a single best solution for all of them.
- denvaar 3y ago> Grouped aliases/imports/requires are a mis-feature in my view. They save a few characters when typing but they complicate searching for module uses and refactoring. Totally agree with this. I prefer to not use aliases most of the time, or to explicitly list out each module path separately if they are too long to fit inline. All preferences though. > Similarly, I don’t like comprehensions as everything they do can be achieved with functions, and they are not composable. I don't use comprehensions too often, but I would miss them if they vanished. Some things are just plain easier with them. They are also valuable in unit tests to show better error messages[1]: # Don't do... assert Enum.all?(posts, fn post -> %Post{} == post end) # Do... for post <- posts, do: assert %Post{} == post EDIT: Although I suppose you could just use Enum.each as well. [1] https://keathley.io/blog/good-and-bad-elixir.html https://keathley.io/blog/good-and-bad-elixir.html
- conradfr 3y agoInitially I didn't like grouping but lately I started to group aliases by their content/function. Like schemas together etc. But that's really nitpicking. Remembering the difference between require, use and import, now that is a struggle.
- atonse 3y agoI've been coding elixir for 7 years and still don't quite understand why I have to keep typing "require Logger" to just use the logger, and why it isn't just required in the framework across the board. Something to do with require being a macro and being lexically scoped (but you could probably just require it in the usings for Phoenix right?)
- sbaildon 3y ago>> Grouped aliases/imports/requires are a mis-feature in my view. They save a few characters when typing but they complicate searching for module uses and refactoring. > Totally agree with this. I prefer to not use aliases most of the time, or to explicitly list out each module path separately if they are too long to fit inline. All preferences though. recode[0] has a formatter plugin that reformats grouped alias/import/requires [0] https://github.com/hrzndhrn/recode https://github.com/hrzndhrn/recode
- paradox460 3y agoI'd encourage the author, and anyone else using Phoenix and liveview, to check out Surface https://surface-ui.org/documentation https://surface-ui.org/documentation It basically brings some conveniences from Vue/react to live view components, and is wonderful
- SupremumLimit 3y ago[I am the author of the post.] I have! Didn’t quite see enough value and for me something like Petal makes more sense.
- Dowwie 3y agoCan it handle events driven within table components managed through tabbed navigation?
- paradox460 3y agoYup, there's even examples of such in the docs
- Dowwie 3y agothis? https://surface-ui.org/samplecomponents/Tabs https://surface-ui.org/samplecomponents/Tabs I'm still skeptical until I see the backend to this. This exact scenario is causing me grief with vanilla phoenix liveview and components.
- sodapopcan 3y agoVery solid article with many points I agree with. Some notes: Keywords are not simply "another pragmatic solution that won over finding a more elegant way to support named arguments" and they are inherently different than maps and serve different purposes. Maps are unordered hashes, keyword lists are constant time, ordered list that can have duplicate keys. For example, in the language core, duplicate keys are used: import Foo, only: [bar: 1, bar: 2] Ecto's query syntax makes use of both duplicate keys and ordering: from q in query, join: t in assoc(q, :table), on: q.table_id = t.id, join: o in assoc(t, :other), on: t.other_id = o.id Everyone complains about the f.() syntax. It's necessary as explained in the guides in the Anonymous Functions section [0]. There is also a nice little parallel to Erlang in that a regular function call looks like `f()` whereas an anonymous function call looks like `F()`. Contexts can be confusing, unfortunately. They're based off of bounded contexts from Domain Driven Design. I wouldn't say there is one right way to use Phoenix contexts, though reading up on DDD's bounded contexts really helps. I already had DDD experience before I learned Elixir, though, so it's easy for me to say and maybe more of a pain for others who don't want to do that. Comprehensions can be much more concise in certain situations. You can certainly get away with avoiding them, but they can really come in handy sometimes. Not gonna give examples as this is getting too long. A short example would be Cartesian products. Finally, I don't quite understand the gripe with `on_mount` or even `live_session` but that's been addressed in another comment. [0] https://elixir-lang.org/getting-started/basic-types.html https://elixir-lang.org/getting-started/basic-types.html
- deleted 3y ago[deleted]
- jkhdigital 3y agoAnother note: The “inconsistent” naming is actually a way to distinguish between guard and non-guard functions: is_map can be used in guards while empty? cannot.
- SupremumLimit 3y ago[I am the author of the post.] I know that keyword lists are different from maps, but I think their superficial similarity to maps isn’t ideal, and I don’t think that they’re the best possible solution (eg consider pattern matching on keyword lists which is order dependent, it’s not intuitive in the case of named args). They’re confusing to newcomers because of these things.
- Alacart 3y agoA possible counter point to the dislike of comprehension, depending on why you don't like them: https://www.mitchellhanberg.com/the-comprehensive-guide-to-elixirs-for-comprehension https://www.mitchellhanberg.com/the-comprehensive-guide-to-e... I discovered a lot of things in this post that made me see them as much more valuable than I'd originally thought.
- mcintyre1994 3y agoBitstring parsing examples using Elixir comprehensions always seem so much nicer than anything I’ve seen anywhere else. I need to play with Nerves to have an excuse to learn them properly!
- davydog187 3y agoI recently wrote a binary protocol in C and Elixir. The Elixir implementation was so much simpler and 1/3 of the code, and has better error messages
- mcintyre1994 3y agoThat’s not open source by any chance is it? I’d be really curious to see the comparison!
- davydog187 3y agoIts for our proprietary product, but maybe I’ll consider writing a blog post on how they compare
- pdntspa 3y agoWhy do so many feel so strongly against state? In my 10+ year programming career not once have I ever wished something was functional. State simply is, dealing with it is a foundational concept in writing code. "It's harder to test" -- so mock it in the tests. "It's harder to reason about" -- you can see the references in your code and you can punch it into the debugger. If the state is implicit it is mentioned in the documentation that you've read right?? "It's hard to tell where changes come from" -- again, stepping through with a debugger makes this somewhat trivial. Find the line that turns state bad, then drill into whatever that line calls, repeat. I'm not saying we should be adding external state willy-nilly. But it is hardly evil -- the dumb hacks that have appeared when a class or instance var would have done the trick boggle the mind. Can someone please prove to me how FP isn't just another cargo cult?
- pjerem 3y agoWhat do you mean by state ? Nobody is against "state", like, for sure, you need your data anyway. What I see that people are calling "state" is just deriving your final state from your original data, like in FP but saving your derived data into memory with a different lifetime than your og data. FP style don’t promote anything complicated other than just don’t save your derived data, just recalculate it each time. Doesn’t mean that you can’t have performance : in fact most of the time, the memory is the bottleneck anyway. Storing your state makes you responsible for synchronization which is where your bugs are going to hide.
- pdntspa 3y ago> What do you mean by state ? External state. Class vars, instance vars, even (gasp) globals. OP goes on some slight tangents/comments regarding FP and state management which inspired my comment.
- youraverageuser 3y agoProbably because it is very easy to mess up when using state incorrectly. It has been years since the last time my program run without bugs for the first time. If you're a genius, then sure, whatever tool you may be using, you'll do fine. But I noticed that many mainstream programming languages introduce concepts that are easy to misuse, even the same global variables. I recently wrote a small python script in 40-50, having concise and separate functions, etc. And I still was stuck on a bug that turned out be a consequence of shared state. The real issue was far from the location I was observing the bug. I think those kind of bugs is quite common. What it boils down to in the end, is your priorities. If you want a program that works 90% of the time and you don't care about the last 10%, then sure, stating your way through is quite natural. If you're sick of the small bugs that is the consequence of the language semantics and not the logic or design, then you'll start nailing down the factors that contribute to the erroneous nature, with state being just one part of it. Based on my experience in python and trying to teach other python developers some fp (spoiler, it didn't work)
- cschmatzler 3y ago> or inconsistent naming (is_map vs empty?). fyi, this isn’t inconsistent. https://hexdocs.pm/elixir/1.15/naming-conventions.html https://hexdocs.pm/elixir/1.15/naming-conventions.html
- Munksgaard 3y agoWhile true, I think the larger problem is that it is not intuitive (at least to me) when I can expect a function to be of the `is_` vs `?` form. It's a leaky abstraction.
- bitwalker 3y agoThe only time `is_` is used, is with functions permitted in guards. These are functions defined in Erlang, and only a small handful exist, and you learn them very early on. With the advent of `defguard`, it is conventional to use `is_` with custom guards as well, but that’s the intuition - guards vs general predicates.
- Munksgaard 3y agoSure thing. My point is that, if I want to check whether an argument is a keyword list, I have to do extra mental work to guess whether the correct function to use is `is_keyword` or `keyword?`. There also doesn't seem to be a consistent rule I can apply to figure out whether it's one or the other. Conversely, I also get tripped up every time I want to add a guard to a function like, "is the thing I want to test written as a macro or not?". I understand the reasoning for the distinction and its roots in Erlang, it's just not very elegant to work with.
- bobwaycott 3y agoIf you’re having to do extra mental work to guess, it means you don’t have enough familiarity with the language, its type checks/guards, and the standard library. That’s not the language’s fault. If you want to check the type of a thing, you always want the matching `is_<type>`. It can be used anywhere in your code, including guards. There’s no guesswork involved here. That is the consistent rule. When you see a function with a `?`, look at the typespec and the function name—give it the argument(s) it expects and it will answer the question on the tin with a boolean. Again, there’s no guesswork. These functions can be used anywhere except guards—that is the consistent rule for boolean functions. > Is the thing I want to test written as a macro or not? I have never had to ask myself this question, and I struggle to parse it. Is the “thing” you want to test referring to the value or to the test you wish to perform on that value? Since you’re asking about macros, I’m assuming you mean the guard test. For that, just learn what guards are available[0] (you can also write your own :D ). [0]: https://hexdocs.pm/elixir/1.15.4/Kernel.html#guards https://hexdocs.pm/elixir/1.15.4/Kernel.html#guards
- travisgriggs 3y agoGood article. My experience using it to do an API endpoint system to a proprietary MQTT system has been really enjoyable. I really like the Elixir community. The slack channel is really helpful. The documentation is pretty good. And I like the fact that I could solve my problems with out needing a huge tone of micro packages that I have to constantly keep up with. I haven't done much LiveView/Phoenix with it, but my biggest "wish this were otherwise" is the chasm between Elixir and Erlang. There's a ton of basic stuff that when you ask, you get told to jump into this other domain with this other set of documentation. It's like being in Wales or something, and sometimes English works, and othertimes, you get told you have to switch to Welsh to enjoy the local experience (not meant as a diss to those that are trying to retain various indigenous languages against English throughout the Isles).
- POiNTx 3y agoI tend to ignore the whole context thing in Phoenix, it confused me in the beginning when I was learning about Elixir and Phoenix. It's a design pattern instead of a core functionality of the framework. Just think about what are good interfaces and abstractions. Seperate your core functionality from your "view" functionality as Phoenix sets it up for you anyway (App = core and AppWeb is your external view layer, being a webpage, json api, liveview, etc). You might end up with something that looks like a context and that's ok, but it shouldn't be what you start from.
- atonse 3y agoI mainly think of contexts as what the "business layer" used to be in the Java world. There's the code that maps to your databases (Ecto structs, like "User" that maps to a users table). Then there's the code that explains higher _business_ concepts, (like "register user" - which might insert a user, send an email, etc). In ruby, for example, a lot of that stuff was just combined in the same active_record classes. So you could say user.register.send_email. But now you'd do Users.register(user) and Users.send_email(). Just a different name for a pretty standard concept of separating your business level code from your objects as stored in the database.
- dns_snek 3y ago> Live components are best avoided if possible, in my view. The documentation is quite bullish on them, presenting them as just another thing alongside functional components, but I think they should be more of a last resort, with functional components used whenever possible. That's an interesting view. I tend to use Live Components whenever I'm rendering a list, because if you don't, you'll quickly notice how the entire list gets re-rendered and sent over the wire whenever one of the elements on the list changes. Live Components act as a sort of a barrier for this, they have a long-lived persistent ID that uniquely identifies the component, so when you're rendering a list of them, only the modified component gets sent over the wire. The effect this has on bandwidth usage can be dramatic, depending on the number of items in your list.
- LukaD 3y ago> That's an interesting view. I tend to use Live Components whenever I'm rendering a list, because if you don't, you'll quickly notice how the entire list gets re-rendered and sent over the wire whenever one of the elements on the list changes. That's what streams are for https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#stream/4 https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#s...
- dns_snek 3y agoThat's new! I'll go have a look, thanks.
- josevalim 3y agoHi @SupremumLimit, that was a great article, thanks for sharing. Here are some tiny remarks, in case they help: * About dot when calling anonymous functions, I have just published an article I have been sitting on. I hope it clarifies a bit why it exists and its benefits for code readability: https://dashbit.co/blog/why-the-dot https://dashbit.co/blog/why-the-dot * Regarding keywords, I actually don't consider them to be a replacement for named arguments. However, given they are called "keyword lists", it is an easy association to make. I will make sure we focus more on them as optional arguments. * There is a distinction between `is_map` and `empty?` as it denotes which are allowed in guards and which ones are not. With a potential type system incoming, we may be able to enforce this too, making it easier for library authors to follow these guidelines. * Your approach to using contexts is absolutely fine. Phoenix typically does not care how you answer the context question, as long as you answer it. The goal is to avoid the case where you build an application for 18 months without considering how it should be organized. * You are 100% correct on functional components vs live components. I will make it clearer in the docs the former should be preferred.
- nivertech 3y ago1. Elixir Keywords are Erlang's proplists plus syntactic sugar (making them easier to write and read), which in turn I guess were inspired by proplists in Lisp, but there they're lists of cons-cells, not a list of 2-tuples. Also Erlang's proplists have a special case for Boolean flags, e.g. instead of [{key, true}] one can just write [key]. 2. I think Phoenix Contexts are inspired by the DDD Bounded Contexts - a strategic pattern, which seems not be relevant for a typical web app built with Phoenix. But maybe it serves as a forcing function for the developer to think about the domain and how it may evolve in the future. 3. I surprised the author didn't complained about overuse of sigils in the language ;)
- SupremumLimit 3y agoThanks for the post about the dot, interesting stuff and the TL;DR is excellent.
- bongobingo1 3y ago> With the context above, I had to answer the following question when designing *Elixiur*: should anonymous functions have a dot when invoked or not?
- jongjong 3y agoElixir gets way too much coverage on HN considering that so few people use it. Unfortunately Elixir doesn't solve any problem which isn't already solved by more popular languages such as Golang, JavaScript/Node.js, Java, C# or Python and the result is neither more efficient nor more maintainable... But its lack of adoption is a problem as it limits your ability to access many powerful libraries and tools that are available when using other more popular languages. Programming languages aren't so different from spoken languages... Linguists can nit pick various pros and cons of different languages but ultimately it's not an author's language which determines whether or not their novel will earn them a Pulitzer prize in literature... Yet there is a good reason why no serious author will write a novel in Klingon (and it has nothing to do with the characteristics of the language itself). Same goes for programming languages and programmers.
- tacker2000 3y agoI fully agree. And since you are talking about popularity of languages in Web backend dev, don’t forget about PHP, which is still the silent beast in this area.
- dns_snek 3y agoHow much hands-on experience do you have with Elixir and Phoenix? At the end of the day, every programming language can be used to solve problems, but you need hands-on experience to be able to judge the ease of writing, reading and maintaining a codebase in that ecosystem.
- emerongi 3y ago> nor more maintainable My humble opinion is that it is more maintainable. Additionally: more fun to use, has less warts than the other languages mentioned and provides really powerful tools out of the box in an easy-to-use manner. I'd be interested in a study observing work stress levels of employees using various languages - my assumption is that Elixir users have less stress.
- hmmokidk 3y agoI love how all of the example langs you used make me want to claw my eyes out (except for maybe Go which I don’t know much about).
- losvedir 3y agoI thought this was a great article! I've used Elixir professionally for several years and it all rings true to me. The bit about Keyword lists I think doesn't mention the biggest aspect of it as I think about it: erlang compatibility. I don't think they make a ton of sense in a vacuum, but a list of 2-tuples where the first element is an atom has been the convention for function arguments in Erlang for a long time, as I understand it. And so Elixir kind of adopted it as well, since it inter-operates with Erlang so much and so well. I think a good "map" implementation wasn't even available in the early days of Erlang? So, basically I just kind of "go with the flow" and use keyword lists for the final `opts \\ []` parameter of a function, but basically nowhere else. I also agree with `for` comprehensions. I use it when meta-programming function definitions, but hardly anywhere else. Also, I forgot that `unless` even exists! I don't think I've seen it in any code since my ruby days 7 years ago.
- fuzztester 3y ago>Also, I forgot that `unless` even exists! I don't think I've seen it in any code since my ruby days 7 years ago. Pascal has both a while loop and a repeat ... until loop (and a for loop too, with to and downto variants, IIRC. And cannot remember for sure, but I think Unix shell has an unless statement, although you can use if with not too.
- rkangel 3y agoI agree on the origin of keyword lists but they do have a useufl property - the same key can appear multiple times. That's why they're right for varargs - because nothing stops the user using the same argument multiple times and sometimes (rarely) that makes for a good API.
- malkosta 3y agoGreat article, thanks for sharing. I’ve been using elixir for the past 5 years, and I’ve had pretty much the same questions along the way…some of them I was able to figure out by myself, and others I only learn by reading comments of posts like yours. Thanks a lot!
- moomoo11 3y agoThe inconsistency of elixir is what turned me away from it. The author points that out as something to live with, that’s fine. But it’s annoying for me. It reminds me of my time dealing with ruby and rails, and I absolutely hated the billions of ways to do one thing with ”cute” helpers. These days I just pick Go or TS for backend. Go for most things but especially anything performance critical or real time, and TS for anything else that just needs to get done really fast and is just IO bound. Go has only most just one or max 2-3 ways to do something. TS takes the best parts of js and makes them shine brighter. I also don’t see the value of live view. Is it really that superior to just putting together an API or ws connection, and having web/mobile clients consume it? It just sounds like over engineering to be cool to send actual view code instead of a bit of json that updates the UI in place. Would love to discuss further and see if I’m wrong.
- deleted 3y ago[deleted]
- atonse 3y ago> Live components are best avoided if possible, in my view. The documentation is quite bullish on them, presenting them as just another thing alongside functional components, but I think they should be more of a last resort, with functional components used whenever possible. This is because live components immediately raise questions about what is the source of truth for the state (view or component), they are harder to test given that they are stateful, and they have some warts like inconsistency in how messages have to be passed depending on whether the live component is a child of a view or another live component (although I know this is on the roadmap to be fixed). This has been the single biggest issue we continue to face in organizing our components. The way to communicate between live views, live components, etc, still feels inconsistent, incomplete, and leaks abstractions everywhere. In some places, between handle_event (which is sort of a live_view thing), handle_info (which is a genserver thing), send (elixir/erlang thing) and send_update (which is kind of a live view thing, but requires a DOM ID, and you handle it via "update", not via handle_*), we end up having tight couplings in code, not fully understanding when to use which one, or having to pass a bunch of extra attributes that only send_update would want (module, DOM ID, etc). Given that PubSub is baked in, it seems like it could be a really powerful abstraction for all the message passing between components, with elegant namespacing between components, their parents, etc. You would just handle_event() to a pubsub topic and stop caring about who in the child hierarchy threw it. Somehow coming up with an elegant event abstraction that combines send_update, send, etc would take live_view to the next level as far as maintaining larger groups of components and their interactions.