https://hexdocs.pm/elixir/1.15/patterns-and-guards.html#guar...
Plus the docs lists on the sidebar which custom functions are guards. Examples: https://hexdocs.pm/elixir/1.15/Integer.html#guards
4,367 karma · joined January 12, 2008
https://hexdocs.pm/elixir/1.15/patterns-and-guards.html#guar...
Plus the docs lists on the sidebar which custom functions are guards. Examples: https://hexdocs.pm/elixir/1.15/Integer.html#guards
The arity overloading is related (albeit not required!) because it adds to the expressive power of Lisp 1. As the examples show, you need a single function for defining the initial accumulator and the reduction operation, instead of passing two arguments or a composite type (see this example from Clojure [1]). By allowing a single function to encode more information via multiple arities, you also only need to override a single name (which must adhere to all arities). It is also an important characteristic of Elixir, so the article would be lacking if it was not included in the discussion.
[1]: https://clojure.org/reference/reducers#_using_reducers
> If we think a special syntax is needed to use local variables of function type, why not use the same syntax for local variables of all types? E.g. $localvar, $localfunc() vs globalvar, globalfunc().
Elixir doesn't have global variables, so trying to unify from that angle would not necessarily help (it would only make local variables of all types more verbose).
The Lisp-1 and Lisp-2 discussion is several decades old, with many discussions arguing the benefits of one over the other. The article highlights some of those trade-offs, which should be clear if someone is willing to get past superficial syntax notes.
However, given how intent you are on putting words in my mouth and on distorting contrasting opinions as white lies, let's call it a day.
Much of the software we write needs to deal with the constraints of its environments. It would be unfortunate if we decide to give all of them the uncharitable description of being defined by "dubious design choices for the innards" even when known well-defined patterns surface from designing within constraints.
case some_fun(1) do
end
case some_fun() do
end
case some_fun do
end
We all know `do` binds to case. Therefore, if `fn` used `do`, the `do` would bind to the farthest call to: # this code
Enum.map list, fn do
end
# or this code
map list, fn do
end
would both bind `do` to `map`.Of course we could special case `fn do` but having `do` bind to different places based on `fn` would be extremely confusing.
Erlang does not have low-ceremony ways of calling C from Erlang, as I believe Python has. It could be done but that would be frowned upon anyway, because it would defeat the preemptive and fault-tolerant guarantees of the runtime.
So I guess it depends on what you mean by great. FWIW, Elixir has no problems integrating native code from XLA, LibTorch, Polars, that also powers equivalent libraries in Python.
Follow up question: do you think the above would be helpful as a short-guide on Phoenix?
Anyway, regarding the dot, we could make `fun.(...)` dispatch to the correct arity, but that would make every function dispatch slower. However, even if we assume that's an ok price to pay, it wouldn't take long for people to request function capture without an arity, such as `&Foo.bar` (otherwise it would feel incomplete). And this feature would add further penalties as we further postpone the call.
Both would also reduce the amount of compile-time checks we can emit and hurt integration with the overall Erlang ecosystem. On the large scale of trade-offs, I don't think it is worth it. :)
irb(main):008:0> class Integer
irb(main):009:1> def call(b); self.+(b) end
irb(main):010:1> end
=> :call
irb(main):011:0> 1.(2)
=> 3
:DPerhaps I was unable to get my point across but that's what the blog post is meant to answer. The TL;DR is two fold:
1. In order for the feature to be worthwhile, we should remove the distinction between name-arity pairs altogether from the language (so module functions and variables effectively exist in a single namespace)
2. However, this double namespace is a core feature of the Erlang VM, so it would require radical changes to it (or you would need a statically typed language with FFI bindings to Erlang in order to "work-around" this efficiently)
Overall Elixir is a Lisp-2 language, with two distinct namespaces, and it requires conversion between functions of those namespaces. It does not have currying, it does not support point-free style, but in practice guards and pipelines help alleviate those concerns.
Regarding your gist, I am honestly not sure if 15 minutes is enough to evaluate a programming language, but in case you want to dig deeper, many questions are answered in the official guides. Here are some quick links:
* On maps vs keywords: https://elixir-lang.org/getting-started/keywords-and-maps.ht...
* On do-blocks and syntax: https://elixir-lang.org/getting-started/optional-syntax.html
Assuming you have a `current_user` stored in assigns, I implement basic role access as:
def edit(conn, param) when conn.assigns.current_user.role == :admin do
...
end
If that's too long: defguard is_granted(conn, role) when conn.assigns.current_user.role == role
def edit(conn, param) when is_granted(conn, :admin) do
...
end
It also works on LiveViews with guards on handle_event.---
The other way of doing authorization, which IIRC is similar to voters in Symfony, I typically implement with protocols:
defprotocol Authz do
def can?(resource, action, user)
end
Then in my Post schema: defmodule MyApp.Blog.Post do
schema "posts" do
# ...
end
defimpl Authz do
def can?(post, :view, user) do
post.public or can?(post, :edit, user)
end
def can?(post, :edit, user) do
post.owner_id == user.id
end
end
end
You may encapsulate in a controller helper like this, although you cannot use it in guards: def can?(conn, resource, action) do
Authz.can?(resource, action, conn.assigns.current_user)
end
Now you can call it to check against any resource whatsoever. If you don't implement it for a resource or for an action, it will crash as expected.I find it requires less boilerplate than the Voter approach in Symfony (but it has been quite some time since I last checked it). No additional abstractions either. The only downside is that it doesn't work annotation/@decorator style (but if you really want it, it should be doable).
---
However, my favorite way of doing authz is by scoping the queries. Typically all of my context functions receive either the org, the user, or a "session" data structure with both which I use as the starting point of my queries. Then I complement with Authz.can? style when that's not enough.
Of course, we could then say "let's allow a default value to be given when pattern matching keyword lists". The problem is that no other pattern works like this and it would be inconsistent.
I think optional arguments are fundamentally incompatible with pattern matching (in Elixir). I understand why we would want that but, if we did have it, we would be adding tons of complexity and it would stand out as a sore thumb. I am also wary of going towards the same direction as Ruby and Python which have quite complex argument handling (albeit we have slightly different feature sets).
I agree with you that you have to build a mental model and you have to memorize (or consult) which guards are available, but arguably the `is_foo` vs `foo?` distinction here is a positive one, because it only takes a glance to know if it is a guard or not (and the majority of guards have the `is_*` prefix so you have to memorize fewer).
You are correct URI.char_reserved? could have been a guard - but that should not be a confusion point for a user of the library - it is clear it is not a guard function. The confusion only arises as a contributor once you read the source as to why a certain choice was made (in this particular case, char_reserved? was written before defguard existed, but even if I wrote it today I am not sure I would consider the possibility users would invoke it in guards).
Btw, I know you know this. I just hope this clarifies the whole discussion a bit for others!
It would be best if there was no confusion whatsoever, but given that it is most likely impossible (in any language), it is important to learn from where the confusion comes from, so we can continue improving the docs. It is a positive loop to have, as long as we listen. :)
We are not against state, quite the opposite, FP likes state so much that we put it on a pedestal! We make it explicit, often with functionality that makes it easy to spot, understand, and manipulate.
If you think about your questions, here is how I would answer it:
"It's harder to test" -- so let's make them simple to test without mocking them out (which would effectively remove them from the test).
"It's harder to reason about" -- so let's make it easy to reason about by making it explicit.
"It's hard to tell where changes come from" -- so let's add constructs for manipulating it.
It is not about antagonizing it, it is about making sure it is clear and intentional.
---
PS: the "evil bit" is more often associated to global mutable state, but don't let any FP language fool you, they all have global mutable state with explicit tools for manipulating them. In Elixir, you can even use tools such as LiveDashboard [1] to navigate all global mutable state in your application, making it very easy to introspect.
[1]: https://github.com/phoenixframework/phoenix_live_dashboard/
* 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
* 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.
If this is something you would like to work on, please reach out, I think it could open up interesting possibilities!
- Many of the non-core++ libraries are considered done
- Many popular and upcoming projects have zero macros: Decimal, Jason, Telemetry, Mint, Finch, Req, etc
- All machine learning projects have either zero macros (Axon, EXLA, Bumblebee) or the macro layer is built on top of the functional API (Nx and Explorer)
- Phoenix LiveView, which is the most recent project from the Phoenix team, is very minimal on the use of macros
- Most of the Phoenix sub-projects have zero-macros: phoenix_pubsub, phoenix_html, phoenix_ecto, esbuild/tailwind, etc
I mentioned this in another comment but in my opinion Phoenix is low on the use macros. They are present in the endpoint+router but then you are working with functions. I speculate the reason why they stand out is because they are the entry-point of your code. But even then the socket API in the endpoint is being replaced by a functional API thanks to contributions from Mat Trudel and the new verified routes are way less magic than the previous route helpers.
I have heard this claim so many times but it is easy to prove it wrong given how much we focus on documentation and understandability around Elixir and main libs. I have had clients asking me questions and I would answer it by improving the docs or writing a guide, committing it to the repo, and then asking the client for feedback. Half of the Ecto guides were written like this.
It is not about keeping clients at all. It is about having a great on-boarding (and beyond) experience.
Not sure whose idea it was but I would say this is a very inaccurate take. Of course, we have taken much more from Erlang than given (that's expected from hosted languages), but I personally sit on more than 100 PRs to Erlang/OTP. I made binary matching several times faster by using SIMD. I improved the compiler performance (including Erlang programs) by (conservatively) 10-20%. I contributed compiler optimizations to reduce memory allocation. There is a now faster sets implementation which I contributed. Erlang/OTP 26 ships with faster map lookups for atom keys based on a pull request I submitted. The new logger system in Erlang takes ideas from Elixir logger. The utf-8 string handling in Erlang is based wholesale on Elixir’s (and extended for lists). And this is in no way a comprehensive listing [1].
And this is not counting everyone else in the Elixir community who contributed and those who are now actively working on Erlang tooling and were on-boarded to the ecosystem via Elixir. The Erlang ecosystem now has a package manager [2], used extensively by both languages, rebar3 and Mix, all implemented and powered by Elixir.
And then there are efforts like the Erlang Ecosystem Foundation, which is made of developers and sponsored by companies working with both languages, and it delivers important projects around observability, documentation, and more. For example, you can find several Erlang projects now using ExDoc for documentation, such as Recon [3].
That’s my take. What are your demonstrations that we are pretty useless for the BEAM ecosystem?
1: https://github.com/erlang/otp/pulls?q=is%3Apr+author%3Ajosev... 2: https://hex.pm/ 3: https://hexdocs.pm/recon
This is not true. The endpoint and router are macro based, but once it is routed, it is all functions. Your controller actions are functions, calling the view/template is a function, your channel is functions, the model/context layer are functions.
When it comes to behaviors, we do our best to follow Erlang/OTP conventions, but deviate where it makes sense. For example, channel has “join” instead of “init” because you can refuse a join but you can’t refuse an init. They have different return types and augmenting init could be confusing if later you were to write a plain GenServer.
It is easy to look from outside and say “someone wanted to put their fingerprints on it” but actual discussions were had in pretty much all of these topics. I suggest asking around. If you asked for something and we had no actual arguments, then please point me to such occurrences. I will be eager to clarify (and as a rule we generally do).
new_column = Series.transform(df["column"], fn arg -> ... end)
DF.put(df, "column", new_column)
which _is annoying_ since you are not supposed to use it. The correct way is to extend the Series API, which we will be very happy to!Most of the Elixir Nx efforts are on inference, especially on how you can embed and scale it using concurrent and distributed patterns (see this post/video [1]). It may not be what you are looking for but we have more folks deploying than training models, so maybe they will find incentives to give Elixir a try. :)
[1]: https://news.livebook.dev/distributed2-machine-learning-note...
Feel free to open up an issue! We have been focused more on high-level features (such as integration with S3, Postgres, Snowflake, SQLite, etc) and therefore we are missing many functions that already exist on Polars. Good news is that it is very quick to add them, so just let us know. :)