12 ms·
Elixir – Why the dot when calling anonymous functions?
- jakear 3y agoWhy 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/57b617de38b1c647ec414049591b3844 https://gist.github.com/JacksonKearl/57b617de38b1c647ec41404...
- nesarkvechnep 3y agoDid you actually read the article?
- jakear 3y agoDid you read the HN guidelines for commenting?
- giraffe_lady 3y agoHopefully when the entire article is an answer to exactly the question you're asking in the comments there is a little leeway.
- jakear 3y agoSee 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.
- giraffe_lady 3y agoThe followup discussion of the author just summarizing the article for you? Ok.
- jakear 3y agoThat'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.
- nesarkvechnep 3y agoThe language is OSS, go fix it. Apparently you have the attitude of someone who knows how.
- jakear 3y agoDang was right, these comments do attract that absolute lowest tier of discussion.
- throwawaymaths 3y agoIf 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.
- josevalim 3y agoI 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.
- jakear 3y agoOkay, 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.
- throwawaymaths 3y agoThat 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.
- deleted 3y ago[deleted]
- ajkjk 3y agoSo 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.
- throwawaymaths 3y agoThat'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)?
- lvass 3y ago>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?
- jakear 3y agoThere 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.
- lvass 3y agoThere'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.
- josevalim 3y ago> 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.html https://elixir-lang.org/getting-started/keywords-and-maps.ht... * On do-blocks and syntax: https://elixir-lang.org/getting-started/optional-syntax.html https://elixir-lang.org/getting-started/optional-syntax.html
- jakear 3y agoRight, 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.
- josevalim 3y agoI 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. :)
- munificent 3y agoWhat if you wanted your local function to shadow some of the arities of the outer function but not others?
- jakear 3y agoThere'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.
- b3orn 3y agoAs 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.
- octacat 3y agoCalling 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").
- jakear 3y agoSlowdown 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.
- octacat 3y agoyea, maybe. Repo is otp/erlang. PRs are welcomed ;)
- jakear 3y agoAn 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.
- octacat 3y agowhy? 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 https://www.erlang.org/eep
- lvass 3y agoI've barely used the dot syntax since Kernel.then was added. It's good that it exists even if you never use it.
- paradox460 3y agoThen is really nice. I always used to write "clean" pipelines in my gen server and such, that ended in an anon function to return the needed tuple for the respective handlers. Inevitably we'd get a squabble in the code review, where some wanted everything put in a variable then the explicit tuple at the end, while others agreed that the dot syntax, while a bit odd and line-noisy, was ultimately better Then solved that. The line noise complaints went away, code readability went up, and everyone was happy
- conradfr 3y agoYes the syntax to pipe the first argument to an anonymous function for another function and use it as second+ argument was akward and hard to remember. Like: "hacker news" |> (&Map.put(:site, "ycombinator", &1)).()
- bnchrch 3y agoFor those of you who are interested by Elixir but find the lack of static typing an issue here are somethings to be aware of: 1. Static Typing is planned and currently the top priority of the team https://elixir-lang.org/blog/2022/10/05/my-future-with-elixir-set-theoretic-types/ https://elixir-lang.org/blog/2022/10/05/my-future-with-elixi... 2. There is a type checking tool https://github.com/jeremyjh/dialyxir https://github.com/jeremyjh/dialyxir 3. You can go a long way with pattern matching and guides in the meantime and have alot more guarantees that a typical dynamic typed language.
- sbjs 3y agoI'm glad we're in the age of static typing. The age of dynamic typing as a response to incorrect static typing was a necessary evil. We had to take a break from broken languages in order to realize what needed to be fixed in those languages. I'm convinced that JavaScript will eventually be one of the best, fastest, and safest languages, as long as we keep this cycle up of abandoning necessary languages and returning to them with very thorough fixes to much better understood and detailed problems with them.
- hosh 3y agoI'm convinced that until Javascript makes it easy to do the right thing by default (instead of its current state of doing many wrong things by default) the Javascript ecosystem will continue to have a lot of low-quality library code intermixed with a few high-quality ones. My neutral regard for JS had rapidly degraded when I had to actually start writing code in it or understanding code others have written.
- sbjs 3y agoWhat ways does do the wrong things by default? I can't think of a single one. You might say, well, == is broken and you have to use ===, but that's not "the wrong thing by default" unless you assume that == is what ought to be the default operator and === ought to be secondary. But that assumption has to come from somewhere, and probably comes from the fact that other languages do it that way. But there's nothing inherently incorrect about === being the default operator and == being the rare secondary one. I think the same principle applies to any example you might be able to give me: if you assume that another language is objectively right, only then can you say that JavaScript is objectively wrong in comparison to that language which apparently descended from the heavens.
- ezekg 3y agoYou can do the same thing in Ruby, although I think it's cursed: fn = -> x { puts "hello #{x}" } fn.('world') In Ruby, .() is an alias to #call.
- josevalim 3y agoAnd because in Ruby it is a shortcut, you can invoke it on any object: 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 :D
- behnamoh 3y agoPeople have argued that Elixir could replace Python, but tbh, Elixir’s syntax is too ugly to replace something as elegant and simple as Python.
- giraffe_lady 3y ago"i'm not falling for that hot take. that's clearly someone with a fetish for getting yelled at. i refuse to participate in that kind of perversion"
- Xeamek 3y agoSimple? Sure. Elegant though...
- deleted 3y ago[deleted]
- matsemann 3y agoI'll bite: Impossible to write pure functional style code in python, hence it loses any elegance contest right out of the gate. ;) I work in a python shop now, and the code I've written the last years must be the ugliest of my career. If it's too complicated to be solved with a list comprehension, it will be so much uglier than lambdas in a language with a form of piping.
- boxed 3y ago> Impossible to write pure functional style code in python What does that even mean?
- behnamoh 3y agoThey probably mean you need to import some modules to do things like partial functions. But it's not like functional programming is not possible in Python. I'd argue that Elixir's syntax is more of hindrance to FP than Haskell's elegant syntax.
- 3y ago
- sigwinch28 3y agoEdit: this was meant to be a reply to https://news.ycombinator.com/item?id=37122798 https://news.ycombinator.com/item?id=37122798 but I messed it up. I don't see a good reason Elixir wouldn't allow it, since Erlang (yes I know it's not Elixir) often compiles anonymous functions ("Funs") into top-level definitions... Here's some IR showing how a top-level fun from this Erlang module... -module(foo). -export([f/1]). f(X) -> G = fun (Y) -> X + Y end, G(X * 4) * G(X * 2). ...gets converted into a top-level function called '-f/1-fun-0-': {module, foo}. %% version = 0 {exports, [{f,1},{module_info,0},{module_info,1}]}. {attributes, []}. {labels, 9}. {function, f, 1, 2}. [SNIPPED FOR HN] {allocate,1,2}. {move,{x,0},{y,0}}. {swap,{x,0},{x,1}}. {call,2,{f,8}}. % '-f/1-fun-0-'/2 {'%',{var_info,{x,0},[{type,number}]}}. {gc_bif,'*',{f,0},1,[{tr,{y,0},number},{integer,2}],{x,1}}. {move,{y,0},{x,2}}. {move,{x,0},{y,0}}. {move,{x,1},{x,0}}. {move,{x,2},{x,1}}. {call,2,{f,8}}. % '-f/1-fun-0-'/2 {'%',{var_info,{x,0},[{type,number}]}}. {gc_bif,'*',{f,0},1,[{tr,{y,0},number},{tr,{x,0},number}],{x,0}}. {deallocate,1}. return. {function, '-f/1-fun-0-', 2, 8}. {label,7}. {line,[{location,"foo.erl",5}]}. {func_info,{atom,foo},{atom,'-f/1-fun-0-'},2}. {label,8}. {gc_bif,'+',{f,0},2,[{x,1},{x,0}],{x,0}}. return.
- throwawaymaths 3y agoIf you stripped the arity how would you know how many register need to be moved into the new call routine's register list, so that reordering, clobbering, etc. at the call site are optimized?
- sigwinch28 3y agoI am a bit rusty on the specifics of the BEAM's registers (the VM that Erlang and Elixir run on) but IIRC the short version is these are registers in a VM and the VM takes care not to let them clobber. Also, in the BEAM, intra-module and inter-module calls operate differently. The Erlang/OTP did a blog post on this a little while ago which I think is great: https://www.erlang.org/blog/a-brief-beam-primer/ https://www.erlang.org/blog/a-brief-beam-primer/ Here is some useful info: > BEAM is a register machine, where all instructions operate on named registers. Each register can contain any Erlang term such as an integer or a tuple, and it helps to think of them as simple variables. The two most important kinds of registers are: > * X: these are used for temporary data and passing data between functions. They don’t require a stack frame and can be freely used in any function, but there are certain limitations which we’ll expand on later. > * Y: these are local to each stack frame and have no special limitations beyond needing a stack frame.
- JonChesterfield 3y agoElixir still runs on the Erlang VM, which is dynamically typed, so we should not expect any meaningful performance gain from typing Elixir code. This doesn't follow at all. Lots of code runs on the x64 machine which is mostly untyped and still gets performance gains from type information in the source language. The whole point of a compiler is to have different behaviour between source and target language. If the source language has a static type system that the compiler uses to control code transforms, you get performance/errors regardless of whether the target has a type system.
- gnull 3y ago> Erlang VM, which is dynamically typed > x64 machine which is mostly untyped Dynamically typed and untyped are not the same, no?
- gnull 3y agoAlso: x86 is very much statically typed. Your types are 8-, 16-, 32- and 64-bit words. Each machine instruction knows the exact types of its operands, so there's no overhead on determining the types of values at runtime (like you'd need to do in case of dynamic typing).
- JonChesterfield 3y agoIt's a tangent but a good one. Machine code, at least x64/aarch64/amdgpu and the like, cannot be considered statically typed. 1/ there is no static checker 2/ if there was, the interpreter would run the failing code anyway 3/ memory can be integers, floats, machine instructions all at the same time 4/ registers overlap, e.g. writing to a 'i32' probably zeros the adjacent 32 bits in the same register 5/ instructions on the same register sometimes refer to floats and sometimes to integers It also isn't dynamically typed, though a kernel might kill the program or hardware crash on some operations. I think it's most usefully considered untyped in that there's no ahead of time verification and there's no runtime checking either.
- 3y ago
- polack 3y agoThe real question is why they do Module.function() and then function.() instead of .function()? It makes no sense to have the dot at the end of an anonymous named function.
- sodapopcan 3y ago`function()` is stored inside a `Module`, so we call `Module.function()`. An anonymous function "`()`" is stored inside a `variable`, so we call `variable.()`. It does make some good sense. It's even kinda elegant!
- darraghenright 3y agoI've sometimes wondered if I am the only person who actually likes the syntax :D There's a reason for it, but additionally, I like the fact that it's explicit — I can look at `some_call.()` and specifically know it's an anonymous function.
- sb8244 3y agoI'm a barewords fan, but even I don't understand the hate towards the dot. Honestly, it feels like low level fruit ripe for baiting engagement. Sure it would be cool if it wasn't there, but does it really materially change anything?
- sodapopcan 3y agoI like barewords in Ruby but they don't hold the same value for me in Elixir. In Ruby I was trying to write code in a way where it didn't matter all that much where stuff was coming from. In Elixir I want to know exactly where stuff is coming from and what it is (like how we generally don't `import Enum` or the like). Not to say you shouldn't like barewords! It is nice that Elixir enables that possibility for those who want it!
- sb8244 3y ago
- callamdelaney 3y agoWelcome to to ruby-esque syntax, which nobody likes. It’s really what the beam needed /s. Odd that a language that takes it’s syntax from a language about 50 years old is 10x more syntactically clean than elixir.
- colonwqbang 3y agoIt seems like different issues are being conflated here. Should function arity overloading be permitted, and when? Should we require a specific syntax for calling locally defined "anonymous" functions, for whatever reason? I don't see what these two things have to do with each other. (Haskell doesn't support function arity overloading, despite what the article suggests. Haskell also doesn't use a special syntax for calling local functions) Other questions to ask: If we do permit overloading for toplevel functions why not do it also for local functions? 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(). It doesn't feel fully thought-out.
- josevalim 3y agoMy goal was to say that Haskell has a single namespace, not that it supports arity overloading, but your interpretation is valid as the text is currently phrased. I will address that (edit: now fixed). However, I don't believe it says Haskell has a special syntax for local calls. 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 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).
- mikercampbell 3y agoI’ve missed Dashbit posts!! Good to see another one!
- User23 3y agoDijkstra used this notation for his formal work later on[1]. He didn't like invisible infix operators, so he introduced "." as the function application operator and made it left associative. So f.x.y is the same as (f.x).y. I find it very elegant now that I've gotten over the usual knee jerk aversion to new syntax. [1] https://www.cs.utexas.edu/users/EWD/transcriptions/EWD13xx/EWD1300.html https://www.cs.utexas.edu/users/EWD/transcriptions/EWD13xx/E...