Elixir – Why the dot when calling anonymous functions?
dashbit.co
dashbit.co
1. Static Typing is planned and currently the top priority of the team
https://elixir-lang.org/blog/2022/10/05/my-future-with-elixi...
2. There is a type checking tool
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.
Personally I want the EVM to (eventually) get the same level of adoption and mindshare as the JVM.
And currently wider Elixir use/education seems like the path to that.
The externals binding system for JS libs works well for integrating libraries. And react bindings are included in the standard lib, they also work great. The compiler is fast and produces generally quite reasonable JS.
You could argue that it's not "better" than elm in a theoretical sense. But for practical work it's much closer to the mainstream of web dev in mindset and syntax. Easier to learn, much easier to integrate with other frameworks or into existing teams & codebases. And it's less opinionated about rendering: the tooling generally assumes you're using react, but you don't have to and it can emit anything if you do some extra work wiring it up. I have used it with node and even, irresponsibly, deno fresh.
Been using it whenever I can for frontend stuff for a year now and haven't enjoyed a language this much since... well... elixir.
Elm is a heavily stripped down purescript, plus an ambiguously-benevolent dictator for life. Also truly terrible interop. It's a good learning environment but I would strongly recommend against using it for anything big.
Rescript is basically Ocaml with half-js syntax.
Typescript is bizarre. On a scale from 0 (C) to 10 (Haskell), it's a 12, a 6, and a 2 duct taped together. It's got some incredibly powerful features (template literals, conditional types), but it's missing most of the standard "advanced"-but-production-ready type system features (e.g. no HKTs, no type-directed emit) and the foundation is absolutely riddled with soundness holes.
Compile-to-JS languages add a big impedance mismatch when you inevitably need to use JS libs. Typescript's JS + types philosophy helps with this issue (it still sucks though).
I think if you really want implicit conversions, then you want to do what Perl does (never thought I'd say that): different operators for different types.
This is kinda insane. People can give reasons X is better than Y on grounds of various theoretical virtues of X -- that has nothing to do with a preference for any language.
Here's an example virute:
Behaviour should be consistent unless specialised. When specialised it should be obvious which specialisation is chosen.
(Violation: basically all of js' operators).
It's really easy to forget a closure in an event driven function and to produce difficult to debug, error prone code.
A classic example I just screwed up:
export function getSFTPConnection(options) {
let key = readFileSync(options.keyfile_path, 'utf8');
let ssh = new ssh2.Client();
return new Promise(
(resolve, reject) => {
ssh.on('error', reject);
ssh.on('ready', () => {
ssh.sftp((err, sftp) => {
resolve(sftp);
});
});
ssh.on('close', (err) => {
reject(new Error('SSH connection closed: ' + err));
})
ssh.connect(
{
host: options.host,
username: options.username,
privateKey: key,
}
);
}
)
}
The bug here isn't obvious, at least to me - but it caused me much heartache and pain because I was moving too fast and not thinking - but a good example of unexpected behavior by default.The bug is that there's not a closure around the ssh instance, so if you call this function multiple times it'll actually return the same connection instance.
The extra fun part of this bug is that the code worked - but because I was using the same instance, when I'd download multiple files in parallel, it would interleave the data, corrupting the files.
And the thing is, I know better. I've been doing js for decades.
I love js, but stuff like this is pretty frustrating, and can be a nightmare for new devs.
I’m not entirely sure what you mean by this, but it doesn’t sound like a correct diagnosis. Each call to `getSFTPConnection` creates a new `ssh2.Client` instance (unless `ssh2.Client`’s constructor does something really weird), and the promise can only resolve with the value `ssh.sftp` passes.
(The error handling does look broken, though – I would expect the 'error' event to be able to fire at any time, and the `ssh.sftp` callback is missing a check.)
Bad example!
You've either adjusted the code for posting or you misunderstood what the problem was, because no, that's not the case. The ssh variable is local to the getSFTPConnection function and is not reused between multiple calls to the function, and throwing a pointless extra closure in there wouldn't do anything.
- Lacks structural pattern-matching.
- Lacks first-class sum-types (although they can be clumsily encoded as objects with "kind" properties).
- Static type-checking not directly (or at all) contributing to the execution performance of the code. This is particularly egregious with e.g. CPython.
Presence of the first three are the sort of things that people appreciate in Good static languages (as opposed to the status quo static languages you alluded to that Python/Ruby/JS/etc were rebelling against in the 2000s).
The last is the dead giveaway of not utopia but rather a bizarre compromised situation.
The existence of `any`; worse yet, the use of `any` in the standard library.
Mutable arrays are treated as covariant: the classic `cats : Cat[] ; animals: Animal[] = cats; animals.push(dog);` problem.
Methods are both co- and contravariant in their arguments by default, which is comically wrong. Member variables which are functions, meanwhile, are handled correctly.
`readonly` is a lie:
const test: {readonly a: number } = {a: 0}
const test2: {a: number} = test
test2.a = 5
`Record<string, string>` is actually `Record<string, string | undefined>`. There's no way for an interface to specify that it really does return a value at any string index (e.g. for a map with a default value).`{...object1, ...object2}` is typed as the intersection `typeof object1 & typeof object2`, which is not correct.
const a: {a: 5} = {a: 5}
const b: {a: 4} = {a: 4}
const spread = <L, R>(l: L, r: R): L & R => ({...l, ...r})
const impossible: never = spread(a, b)
You have to resort to bizarre conditional type hackery to control when unions distribute and when they don't.You can constrain generics as `<T extends string>foo(t: T) => ...` but not `<string extends T>foo(t: T) => ...`, which makes many functions (e.g. Array.includes) much too strict about what they accept.
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.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/
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.
The Erlang VM absolutely clobbers registers within functions all the time, since there are only 64? of them and they are a 'limited' resource. My point is that the logic to efficiently move data around is more complicated when you don't know the arity ahead of time.
You can't, for example, keep a register file as a linear slice. You have to allocate a whole slate of 64 registers on each call with the last (n) registers blanked.
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...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
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.
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. :)
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.
Enum.map(list, fn do _ -> :ok end)
Is unambiguousHonestly 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
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.
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[0] https://hexdocs.pm/elixir/1.14/Kernel.SpecialForms.html#fn/1
[1] https://hexdocs.pm/elixir/1.14/Kernel.SpecialForms.html#cond...
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?
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.
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.
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").
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.
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
Like:
"hacker news" |> (&Map.put(:site, "ycombinator", &1)).()
It makes no sense to have the dot at the end of an anonymous named function.
An anonymous function "`()`" is stored inside a `variable`, so we call `variable.()`.
It does make some good sense. It's even kinda elegant!
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?
Not to say you shouldn't like barewords! It is nice that Elixir enables that possibility for those who want it!
fn = -> x { puts "hello #{x}" }
fn.('world')
In Ruby, .() is an alias to #call. 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(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.
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).
[1] https://www.cs.utexas.edu/users/EWD/transcriptions/EWD13xx/E...
Elixir 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.
EDIT: Also, when I go to look for this quote I can't find it. Did you somehow end up on last year's announcement of an upcoming type system for Elixir [0]?
[0] https://elixir-lang.org/blog/2022/10/05/my-future-with-elixi...
The type checking beam does can coincide with the checking the source language does but that's not fundamental. Say you compile SML to beam. Would you conclude that the compiler isn't able or allowed to do anything with the types in the source language?
> x64 machine which is mostly untyped
Dynamically typed and untyped are not the same, no?
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.
Remember the context. The thread started with Elang VM making it impossible to make code faster, due to the need to accommodate Erlang's dynamic typing features and do runtime checks for that. Top comment said that dynamic typing per se can't be the obstacle for programs being faster, and gave x86 as an example of a presumably dynamic VM which does not limit the speed of the programs.
So "dynamically typed" from the article here meant that there's some runtime checks you can't unsubscribe from, and due to them there's a cap on how much faster you can make your code. It applies to some extent to x86, there's also checks there like segfault for example, and maybe you could save some space on silicon if you removed them and required the machine code to be verified to never cause segfaults. But I argue that this is too much of a stretch. Runtime checks that x86 performs are negligibly simple, and x86 is not a valid counter-example for the article's point. In my view the article's point is valid.
> 1/ there is no static checker
"return 0" is the static checker for x86 machine code.
But seriously: this is not a necessary condition for something to be statically typed, it can't be.
> 2/ if there was, the interpreter would run the failing code anyway
Failing code does not exist. All code is valid.
Haskell is undoubtedly statically typed. But division by zero will still cause a runtime error.
> 3/ memory can be integers, floats, machine instructions all at the same time
Ok. Disregard what I said about machine words. The only datatype in x86 is byte. Some operations use 2 bytes, some use 4. They may interpret them differently (some may see an int, some float), but it changes nothing, since both int and float operations are operations on bytes.
Whether x64 is typed or untyped feels like the start of an argument about whether xmm registers have the same type as stack memory which is kind of interesting but orthogonal to elixer.
I would agree that static, dynamic and untyped are distinct things. You can compile from a source language with any type system to a target language with any type system. I think, there may be edge cases around excessively constrained languages.
As an extreme example, the target machine says nothing about whether you can constant fold 1+2 into 3 in the source language, but the source language can definitely block or enable that minimal optimisation.
Odd that a language that takes it’s syntax from a language about 50 years old is 10x more syntactically clean than elixir.
Any high-level language that is afraid of map/reduce and other functional constructs is a waste of time to me.
Python has a lot of mindshare because it looks simple, but by God if it isn't an absolute kitchen sink of a language full of weirdnesses and gotchas. And still, completely afraid of functional constructs.
Nice bait though.
You mean anonymous functions surely? The syntax for closures is basically nothing, you just make a function.
Elixir is afraid of most "functional concepts".
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.
What does that even mean?
Sure there are times that a need for performance calls for mutable data. But for me those times are pretty rare, and when they happen it's usually easy enough to quarantine the mutation.
That's exactly how lambdas (and all functions) work in actually functional languages like Haskell. And it's a good thing,lambdas shouldn't allow statements.
> how you can't pipe/chain things
Ironically after switching from Elixir to a language where I can have any operator I wish, I found myself using the equivalent of Elixir's |> only on a very rare occasion. IMO they're vastly overrated.
And it doesn't have to be the pipe operator, just something similar. Like list.map().filter().reduce() instead of reduce(filter(map(list))) which quickly gets unwindly.
It can sometimes be annoying yes, but when you come back to the code 6 months later it's mostly a good thing.
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.
The big thing in Elixir community right now is to write the native/performance code in Rust[1] and interop with Elixir.
Elegant though...