What would you remove from Elixir?

language-implementation

#61

But you are aware that nil isn’t some magic value in Elixir and Erlang? The same goes for true and false. All of them are just simple atoms, so :nil == nil always. So this is quite impossible to remove it as a whole.


#62

Hah! I’m aware of the short hand, but I’m not a big fan. I find that it makes code less readable, unless its very obvious.


#63

Shouldn’t for just be map?

As for |> perhaps being able to define infix operaters would avoid the need for that macro…

e.g. something like (|>) x f = f x

Which uses a far, far less powerful language construct to achieve the same thing…

Structs should probably just be a first class part of Elixir. I don’t seem why they need to be a macro.

The problem with compile time DSL’s is that most of the time they seem more of a hindrance than a feature.

I like Ecto for example, but my colleagues complain about it or avoid using it completely - mainly I reckon, because of the macro interface.

Functional programming is about simplicity.

Macros seem to obfuscate mostly when obfuscation isn’t perhaps necessary.

Sure functional composition can be overly verbose now and then, but at least it’s explicit…


#64

for is much more than Enum.map. It is list comprehension so you can filter there, do multiple iterations at once, etc. For example this is example with map:

Enum.flat_map(1..10, fn a ->
  Enum.map(1..10, fn b ->
    {a, b, a * b}
  end)
end)

And this is example with for:

for a <- 1..10,
    b <- 1..10,
    do: {a, b, a * b}

Elixir has bound set of possible infix operators. Maybe that could be lifted, but I think it would greatly complicate the parser.

Because BEAM do not have structs at all, only maps (since OTP 18). All structures are in fact just special examples of maps, and macros are needed to handle all Elixir’s magic around them.

Unfortunately there are things you cannot do without macros, or things that looks ugly when macros aren’t available. As impossible things let’s see at the Ecto example: within “pure” Elixir fragment is impossible in current form, you would need to pass an list as a second argument. While we can argue that it could simplify some things it would make one thing impossible - compile time errors for missing arguments.

About ugly things. Do you remember using nested case when there were no with available? It was super ugly. Or how about our nice friend in? def foo(a) when a in 1..10 is quite simple, but how about def bar(a) when a in ~w[a b c d e f]a? You would need to write def bar(a) when a == :a or a == :b or a == :c or a == :d or a == :e or a == :f. Super nice. Also remember that Elixir is built on macros, if is a macro, in is a macro, use is a macro, alias is a macro, def is a macro, defmodule is a macro… almost everything is a macro, so writing code without macros would be terrible experience.


#65

I suppose I would argue that there’s a bunch of macro stuff which is great for language designers but prob not good for library designers.

I and many ppl using Elixir have 2 responses when using macro based libraries:

At first -> cool, this looks appealing

Sometime later ->
this API isn’t v clear to me
this error message is pretty cryptic

I love with etc.

But agree strongly with the beginner’s guide caution about macros: i.e. “use with caution”.

So much so, that they should be seen as bad practice unless you are Mr. Valim (a little extreme, but not far from it… ; ))


#66

That is different thing. In general I would also advise to not use macros unless you know what you are doing and that you know you can handle that. I have written some libraries that heavily utilise macros (ecto_function) but also I have removed a lot of macros from libraries that didn’t needed that (prometheus_ex). So macros are great, but you shouldn’t write one yourself.


#67

So write macros responsibly.

Don’t Write Macros But Do Learn How They Work Jesse Anderson, ElixirConf 2017


#68

No, it’s closer to reduce but more succinct.

I agree, but Elixir prepends arguments instead of post-pends and it doesn’t handle anonymous functions cleanly like that.

I personally don’t know why they are maps, they should be tagged tuples on the beam, that is the usual ‘structural’ type after all… >.>

They should instead be called row-typed records or ‘objects’ (not OOP objects).

They should really look closer at it, that interfaces gains you a lot of safety and features that is not otherwise possible via a lower level SQL interface.

Not always, macro’s allow you to add to the language that which it is not otherwise capable of as efficiently or at all.

No, it already can be that, the issue is that using it would then require doing 42 |> &someFunction(&1, 1, 2). |> is a normal infix operator, you can implement it as a normal function or as an operator, it works fine either way, but by using a macro it allows a more ‘normal’ function call syntax, even though honestly I would rather kind of prefer the currying syntax, but you’d have to hope it gets optimized out, and honestly it probably wouldn’t (though a macro could fix that).

Structs and Records should have just been the same thing, a set of tagged tuples with information compiled into a module for introspection about it.

Eh technically some of those are special forms, although with could have been implemented with a better syntax as a macro in my opinion… >.>

I don’t, the comma-turds (erlang’ism) littering it’s expressions and the formatter making it look horrid are two big issues I have with it, both of which significantly harm readability for me (maybe not others, but for me, the formatter is just so bad on it…). I only use it to not pull in other libraries (that do it better) to reduce the dependencies I use.


#69

everything is just a reduce ; ) but i was messing around with maps with a colleague, and saw the map is a much closer analogy

agreed!

thing is, random_library_authors think they knew some new language feature that will solve our woes - mostly not the case : (

The semantics are good, the syntax is indeed in need of a spring clean : )


#70

Finally I thought of something!

I would remove the current Enum.map and similar functions, and replace them by ones that make more clear that we are destroying the structure and returning a list while mapping. And I would like to introduce a new built-in Protocol that allows you to iterate over things while keeping their structure. (So a transforming map rather than a destructive one)


#71

This might not be the most obvious place, but the reasoning behind that behaviour by the Enumberable protocol is explained in the docs for the Collectable protocol. So using the Enum module does already imply you might loose shape as it’s working with a protocol build on that premise.

https://hexdocs.pm/elixir/Collectable.html#module-why-collectable


#72

Except map cannot filter or transform the return type to a different container type, where for can.

This is actually a great argument for making macro’s Less easy, which there are indeed some great arguments for. However I think tooling could fix it far better while keeping Macro’s easy, however the tooling that would really help would not easily exist without the language being strongly statically typed, which Elixir very much is not right now.

The syntax comes from them being Special Forms instead of just Macro’s. Really most of Elixir could be implement ‘in’ elixir directly as macro’s as long as we have our currently existing case special form or some method to build it. ^.^

So… monad interfaces? :wink:


#73

Ugh yeh, forgot about that sort of thing.

I use for loops once or twice a year in any language. Thankfully…

agreed


#74

I was talking about “user defined” operators, not about different form of parsing |> macro which is plain old macro so you can override is as you please. Currying is hard in BEAM languages (without help of any additional syntax) as there is no way to differentiate between foo/1 and foo/2 in that case, and if there would be currying built in into Elixir that do not need any “special syntax” then adding new method with lesser rarity than all existing ones could be breaking change.

TBH I thought it is implemented as regular macro. I do not know why it is in Kernel.SpecialForms o.O

The only reason why Erlang uses tuples instead of maps is that records are much older than maps (which became a thing in OTP 17). Both solutions have advantages and disadvantages and I think that using maps makes it more uniform (Erlang uses #{} for maps and #name{} for records, which can make it a little confusing) and simpler in implementation way (at least for me). And I think that Elixir way is better in “modern” OTPs.


#75

It is because it uses commas to separate expressions instead of a normal block as would be consistent with the rest of elixir (both with and for are horribly broken in this way I say…)

Or if you have type information. ^.^

Not really, most structs are pretty small, meaning the record is a lot faster to parse and update and you don’t have to worry about it containing ‘extra fields’ like row-typing would handle (bugs waiting to happen!).


#76

Oh, yeah. I have forgot about that. Yeah, I think that using plain block would be better as well and would be more “do-like”. Maybe in Elixir 2.0 as I think it would be good choice. I do not think it would be possible with for though.

Typed Erlang/Elixir is completely different beast, however I think that it would be ambiguous like in this situation:

def foo(a), do: fn b -> a * b end
def foo(a, b), do: a + b

fun = foo(a) # it will be curried `foo/2` or lambda returned by `foo/1`?

And that is why small maps (up to 32 keys) have such semantic under the hood (thanks to @michalmuskala) since OTP 21. The disadvantage of records is that in such case you need to know definitions upfront (that is why Erlang utilises -include macro with .hrl files). With one map per module it can be easier. We could make records in similar way, but:

  • it would require record definition even for reading, you cannot use foo.bar to access field value, we would need different syntax for such actions, like foo%MyRecord.bar (following Erlang) or other sigil-magic
  • with OTP <21 it would collide with tuple_calls feature of BEAM (as Elixir uses the same syntax for accessing fields and calling functions)

So with current syntax “structures defined in form of records” would make a lot of things a lot harder.


Elixir 1.7.4 backwards incompatibility
#77

I have my own project that replaces for with a macro comprehension. ^.^

Mine decorates what you pass in with types (or ‘Access’ if not otherwise known) so it can generate optimal code for the specific types being used, meaning it’s faster than for. It’s in one of my playground library and I should probably pull it out into it’s own project as it is quite functional…

My fairly trivial benchmark:

defmodule Helpers do
  use ExCore.Comprehension

  # map * 2

  def elixir_0(l) do
    for\
      x <- l,
      do: x * 2
  end

  def ex_core_0(l) do
    comp do
      x <- list l
      x * 2
    end
  end

  # Into map value to value*2 after adding 1

  def elixir_1(l) do
    for\
      x <- l,
      y = x + 1,
      into: %{},
      do: {x, y * 2}
  end

  def ex_core_1(l) do
    comp do
      x <- list l
      y = x + 1
      {x, y * 2} -> %{} # line 35
    end
  end
end

inputs = %{
  "List - 10000 - map*2" => {:lists.seq(0, 10000), &Helpers.elixir_0/1, &Helpers.ex_core_0/1},
  "List - 10000 - into map +1 even *2" => {:lists.seq(0, 10000), &Helpers.elixir_1/1, &Helpers.ex_core_1/1},
}


actions = %{
  "Elixir.for"  => fn {input, elx, _core} -> elx.(input) end,
  "ExCore.comp" => fn {input, _elx, core} -> core.(input) end,
}


Benchee.run actions, inputs: inputs, time: 5, warmup: 5, print: %{fast_warning: false}

And the results locally right now:

Operating System: Linux
CPU Information: Blah
Number of Available Cores: 6
Available memory: 16.430148 GB
Elixir 1.6.6
Erlang 21.1.1
Benchmark suite executing with the following configuration:
warmup: 5.00 s
time: 5.00 s
parallel: 1
inputs: List - 10000 - into map +1 even *2, List - 10000 - map*2
Estimated total run time: 40.00 s



Benchmarking with input List - 10000 - into map +1 even *2:
Benchmarking Elixir.for...
Benchmarking ExCore.comp...

Benchmarking with input List - 10000 - map*2:
Benchmarking Elixir.for...
Benchmarking ExCore.comp...

##### With input List - 10000 - into map +1 even *2 #####
Name                  ips        average  deviation         median
ExCore.comp        370.81        2.70 ms     ±2.76%        2.67 ms
Elixir.for         245.68        4.07 ms    ±21.72%        3.90 ms

Comparison: 
ExCore.comp        370.81
Elixir.for         245.68 - 1.51x slower

##### With input List - 10000 - map*2 #####
Name                  ips        average  deviation         median
ExCore.comp        2.50 K      399.55 μs     ±9.28%      405.00 μs
Elixir.for         1.92 K      521.94 μs     ±7.26%      535.00 μs

Comparison: 
ExCore.comp        2.50 K
Elixir.for         1.92 K - 1.31x slower

Interestingly it won’t run on Elixir 1.7.4, get a syntax error, @josevalim did something change in a backwards incompatible way with Elixir 1.7.4? o.O

╰─➤  mix bench comprehension                                                                                                      1 ↵
** (SyntaxError) bench/comprehension_bench.exs:35: unexpected operator ->. If you want to define multiple clauses, the first expression must use ->. Syntax error before: '->'
    (elixir) lib/code.ex:767: Code.require_file/2
    (mix) lib/mix/tasks/run.ex:146: Mix.Tasks.Run.run/5
    (mix) lib/mix/tasks/run.ex:85: Mix.Tasks.Run.run/1
    (elixir) lib/enum.ex:1314: Enum."-map/2-lists^map/1-0-"/2
    (mix) lib/mix/task.ex:355: Mix.Task.run_alias/3
    (mix) lib/mix/task.ex:279: Mix.Task.run/2

I notated line 35 as # line 35 in the above benchmark source. Feel free to clone git clone https://github.com/OvermindDL1/ex_core.git && mix deps.get && mix compile && mix bench comprehension


Elixir 1.7.4 backwards incompatibility
#78

Backwards incompatibility thread opened at: Elixir 1.7.4 backwards incompatibility


#79

Seems awesome. I would only like to know how it behaves when you also want to filter some entries like you can do with for, ex.: for a <- 1..10, mod(a, 2) == 0, do: a * a.


#80

I would be nice to get rid of that feeling I get using other languages and platforms that “this would be better in Elixir.” :joy: