Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Why not allow function literals that define multiple arity implementations? Something like...

    def sum(list) do
      plus = { 
        fn -> 0 end ; 
        fn x, y -> x + y end  }

      Enum.reduce(list, plus(), fn x, y -> plus(x, y) end)
    end
Also throwback to when I decided to rant about Elixir for some reason 6 years ago, with the final comment:

    Converting a Module Function to a First class function (&Math.square/1): 
      Why do you make me lose the ability to use Elixir's admittedly powerful Pattern matching on arity feature?" 
https://gist.github.com/JacksonKearl/57b617de38b1c647ec41404...


> Why not allow function literals that define multiple arity implementations? Something like...

Perhaps 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


Right, I wasn't putting that it forward as a sign of my deep investment in the topic. More just a laugh at dumb things I got worked up over in college. thanks for the links.

That all said, I don't see why you'd need to remove the double namespace feature in order to have function literals that define multiple interpretations. The value namespace still has just the single value-land binding to the literal, only the `call` procedure (the .( operator, so to speak) needs to be modified to dispatch to the appropriate function-land name as determined by airity and the literal the value was bound to.


I see. :D I am lucky my time in college was just before the internet "became permanent". Double lucky that all pictures and recordings from my cover band disappeared with it!

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. :)


I think the real question we all want to know is why lambdas don't have "do"


Because `do` binds the farthest away. In these examples:

    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.


Does anybody actually call Enum.map without parentheses? That's actively discouraged currently.

    Enum.map(list, fn do _ -> :ok end)
Is unambiguous

Honestly I do appreciate not having to type two characters, but I'm currently onboarding a bunch of n00bs and they're very puzzled by this one inconsistency


Well, I didn't have much luck last time I responded to you, but I'll try again since you haven't gotten a response yet!

Regardless of whether or not it's actively discouraged, it still must be supported since Elixir is so heavily bootstrapped. Since `defmodule`, `def`, etc are all just macros written in Elixir, there are no special rules around which functions/macros are allowed to be called without parens. There was some discussion about that on the forums a while ago (like requiring parens for functions and not for macros) and the answer is that that will never happen.


Well I mean it wouldn't have been a big deal, if the do block binds to the outermost, then in most cases just would get a compiler error since usually a private fn/0 doesn't exist. Still though in the early days elixir more strongly preferred no-parens, so avoiding that is understandable.


True!


Lambad definitions don't take arguments, they jump straight to argument matching.

Take:

    case value do
      true -> "true"
      false -> "false"
      _ -> "uh..."
    end
vs

    fn
      true -> "true"
      false -> "false"
      _ -> "uh..."
    end
`do` is simply syntactic sugar for a keyword list argument containing a `:do` key:

    if(true, [{:do, "hi there"}])
So it wouldn't make sense for `fn` to take a `do`.


    fn do
      true -> "true"
      false -> "false"
      _ -> "uh..."
    end


As much as I despise people responding in pure code, you're right, it's technically `fn/1` [0]. Even though the docs say `fn(clauses)`, `cond/1`, which requires the `do`, also says `cond(clauses)` [1] so looks like I'm wrong.

[0] https://hexdocs.pm/elixir/1.14/Kernel.SpecialForms.html#fn/1

[1] https://hexdocs.pm/elixir/1.14/Kernel.SpecialForms.html#cond...


>Why do you make me lose the ability to use Elixir's admittedly powerful Pattern matching on arity feature?

If every first class fuction had to check for arity at runtime, wouldn't there be a performance cost? Also would reduce drastically the amount of errors caught at compile time. Maybe a different mechanism for dispatching like that (a macro?) could be done, how useful would that be?


There already must be a arity check at some point, no? If the type contained the set of airties the lambda accepts rather than just the singe one, all the same type checking could be done when it's already done.


There's no type checking on function dispatch unless guards are explicitly added. The arity dispatch for functions captured into first class is done at compile time and require explicitness. That said, I think you can create your own dispatch macro that's probably as efficient as possible for doing what you proposed.


As far as I as an Erlang developer can say, I think there's a misunderstanding about arity in Elixir (and probably even more misunderstandings on everything else that's borrowed from Erlang). There is no "Pattern matching on arity feature". Normal functions are defined by a name and arity, `foo/1` and `foo/2` are two different functions and not two clauses of the same `foo` function. Erlang makes this clear when it forces you to export both instead of just exporting `foo`, maybe Elixir just tries to hide this limitation. Funs/lambdas are limited by this too, as another comment already showed, funs are sometimes (often?) compiled to normal functions.


What if you wanted your local function to shadow some of the arities of the outer function but not others?


There's no value/function shadowing in Elixr so it isn't possible regardless. But one could always just call the outer Function from the labmbda's innards.


If you look at how anonymous functions are compiled at a low level you'll understand why that's not possible: a function is literally "the module it's in" + the bytecode "line number" + arity (so that it knows how many register items to instantiate). For inlined anonymous functions, a secret private function gets created.


I consider this to be pretty much an implementation detail though and not necessarily set in stone. The Function data structure is their public interface though and it does define an arity.


Okay, so make the lambda's binding refer to several of those 'structs' and have .( dispatch to the correct one based on the airity at the call site.


That will introduce unnecessary overhead in the basic case. At some level you do want to access the "low level" function (interfacing with Erlang, e.g.) if you really want that sort of dispatch you can write your own polyfunction data type.


So the answer to "why does Elixir require a dot?" is "because of dubious design choices for the innards", rather than any sort of reason that makes sense based on its actual syntax or features.


As other languages that run on existing environments most likely have done. C#, F#, Clojure, TypeScript, Swift, and many others probably had to take similar considerations. Then at a lower level, if you want to maintain ABI compatibility, that may also impose restrictions.

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.


It would be unfortunate if we were unwilling to call design choices dubious and learning from past mistakes instead of rationalizing why they're actually fine. I don't know Elixir but you're telling me in 2023 I'm supposed to learn that a language calls anonymous functions with a different syntax than regular ones and not be annoyed? Like.. that just sucks. If you have to do it that way for unfixable reasons, at least deliver the bad news with an apology instead of a white lie about why it makes perfect sense.


I am completely fine with calling past dubious design choices. This isn't one of them. That was your labeling, provided with no evidence or reasoning, not mine, and that's what I was criticizing.

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.


This reminds me of when Paul Graham asked a question about Python and someone said they weren't going to do their homework for them.


Ah, I didn't mean to say your take was a white lie. The white lie I referred to was the implicit claim in the article, that it had to be this way, instead of that it's correcting something unfortunate.


As a developer who came to Elixir from Clojure I had the same reaction to fun.(), “this sucks”. Then someone explained why. So, okay, there is a good reason but “bleh.” I think that lasted maybe a day, by which time I had accepted it as part of the language. It’s just not a big deal when the language offers so much in return. And I liked Clojure.


That's not a dubious design choice. It's actually amazing. How would you design a lambda that can be pickled and rerun across time (run on a different invocation of the vm), or space (sent across a network and executed)?


Well like... the same as it is except allowing multiple arities in the definition.


Calling function with dynamic number of arguments would be slower, if you pass that as higher-order-function. Because VM would have to figure out at runtime if it needs to execute plus/0 or plus/2 depending on number of passed arguments.

I.e. slowdown without any major benefits (i.e. use case of passing function with known number of arguments is much more used comparing to "unknown number of of arguments").


Slowdown would be imperceptible with modern branch prediction. Lay out the airity implementations in memory as a linked list, instruct users to make the first one the common case, you're golden. An index would also work, not really necessary though.


yea, maybe. Repo is otp/erlang. PRs are welcomed ;)


An unsolicited PR that adds new syntax and changes the bytecode layout would 100% not be welcomed. There are stages to redesigning languages and that is the last one. But instead of having a discussion about it (stage one), people like to pretend like the current implementation is God's gift to mankind and anyone who disagrees should DIY or shut up.


why? would be pretty welcomed as proof-of-concept to start the discussion. Nobody has said it would be merged immidiatly. At least could be used to run load tests. You can always start with EEP if you wanna to discuss or propose new feature - https://www.erlang.org/eep


Did you actually read the article?


Did you read the HN guidelines for commenting?


Hopefully when the entire article is an answer to exactly the question you're asking in the comments there is a little leeway.


See the comments in other threads where an actual topic of contention was presented and accordingly follow up discussions could be had. The reason for the HN Guidelines is to foster good conversation. "Did you read the article" doesn't.


The followup discussion of the author just summarizing the article for you? Ok.


That's all you've been able to comprehend from reading this thread? Surprising.

I'd tell you to get off your high horse, but it seems to be your identity.

To summarize: the only reason presented not to do what I said is dubious claims about perf, which were not mentioned in the article whatsoever.


The language is OSS, go fix it. Apparently you have the attitude of someone who knows how.


Dang was right, these comments do attract that absolute lowest tier of discussion.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: