If you could change one thing in Elixir language, what you would change?

Ok, so my task was to check are changes of arbitrary nested changeset empty in the meaning of:

  1. if all simple fields of this and all nested inside changes key are nil or empty string
  2. list considered to be empty when it’s empty or consists of “empty” by same rules values
  3. map is empty when it has no elements or all elements inside are empty (later this rule appeared to be not necessary because there were only assoc_many and embed_many in my changesets but it can be added)

So if we find any non empty value there is no need to continue this recursion and we need just break.

My way of doing this was:

  def empty?(val) when is_binary(val), do: val == ""
  def empty?(nil), do: true
  def empty?(%Changeset{changes: changes} = ch) when changes == %{}, do: true

  def empty?(%Changeset{changes: changes} = ch) do
    try do
      Enum.reduce(changes, true, fn {k, v}, _acc ->
        case v do
          v when v == %{} ->
            true

          [] ->
            true

          %{changes: changes} = v ->
            unless empty?(v), do: throw(:break), else: true

          v when is_list(v) ->
            unless empty?(v), do: throw(:break), else: true

          _ ->
            throw(:break)
        end
      end)
    catch
      :break -> false
    end
  end

  def empty?([]), do: true

  def empty?(list) when is_list(list) do
    try do
      Enum.reduce(list, true, fn el, _acc ->
        unless empty?(el), do: throw(:break), else: true
      end)
    catch
      :break -> false
    end
  end

What would be the best consice way of doing this?

I’m sorry if my comments are off the topic.

Thanks, that’s a beauty!
I just didn’t touch Elixir for quite a while so starting to forget it…

Though in the last one if I make a
bar = %{buz: 1}
and try to get absent key value like
foo = bar && bar.buzzz
it gives me an error. But this one works:
foo = bar && bar[:buzzz]

Well that’s not a Loop, that is a Reduction, those are not the same things. A Reduction just calls a function once for each element passing along an accumulator. Internally it could be done via a loop or recursion or even flat-macro calling with no loops/recursion at all if the bounds were known, the user of a reduction shouldn’t care how it is implemented as all they are is reducing.

Your requirements mean that you are not just reducing. Sure you could set a flag on the accumulator to no-op the rest but your function is still being called for them, which if it is ‘sufficiently large’ of a list then that could be slow. What you want is actually a reduce_while, which gives an early-out flag for a reduce, or to just do your own recursion. But using a reduce based on those requirements is the wrong construct.

Use . for a struct, or [...] for a map. It wasn’t stated which was which but based on that a dot was used in your example then I used a dot as well. :slight_smile:

1 Like

Inability to compare dates directly is killing me.
date1 > date2 # will return some result but not an expected one

This one looks horrifying and hard to comprehend:
DateTime.compare(DateTime.utc_now(), date2) == :gt
Well, it requires some deep underlying changes to make it work so it’s unlikely to be changed any time soon

1 Like

Yeah using :gt to compare is quite tricky. But I have good news! In the next version of Elixir there are new facilities for comparing structs. You will be able to simply do Enum.sort(dates, Date) (or Enum.sort(dates, {:desc, Date}) if you want to control the order).

docs: https://hexdocs.pm/elixir/master/Enum.html#sort/2-sorting-structs
PR: https://github.com/elixir-lang/elixir/pull/9432

2 Likes

It’s funny how Elixir is finally starting to use module’s as First Class Modules ala OCaml, Elixir should steal more of those patterns and features (if only they didn’t help force out tuple calls, then you could get OCaml Functors in Elixir too… blah…). ^.^

3 Likes

Can you explain “First Class Modules?”

First Class Modules are like First Class Functions. :slight_smile:

Basically, you know how you can pack a function up into a variable, ferry it around, and call it ‘later’? Like:

iex(1)> addone = fn a -> a + 1 end 
#Function<7.91303403/1 in :erl_eval.expr/5>
iex(2)> addone.(1)
2

This is a First Class Function.

A First Class Module is, also likewise, a module that you can pack up into a variable, pass it around, and call it again:

iex(3)> defmodule Ops do
...(3)>   def add1(a), do: a + 1
...(3)>   def double(a), do: a * 2
...(3)> end
{:module, Ops,
 <<70, 79, 82, 49, 0, 0, 4, 188, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 133,
   0, 0, 0, 16, 10, 69, 108, 105, 120, 105, 114, 46, 79, 112, 115, 8, 95, 95,
   105, 110, 102, 111, 95, 95, 7, 99, 111, ...>>, {:double, 1}}
iex(4)> blah = Ops
Ops
iex(5)> blah.add1(1)
2
iex(6)> blah.double(2)
4

And for completion, an OCaml Functor is like a Closure, but for Modules. Basically you know how Elixir can ‘close’ around it’s environment to carry it around, like this:

iex(7)> outside = "Hello "
"Hello "
iex(8)> f = fn(name) -> output + name end
** (CompileError) iex:8: undefined function output/0
    (stdlib) lists.erl:1354: :lists.mapfoldl/3
    (elixir) src/elixir_fn.erl:14: anonymous fn/4 in :elixir_fn.expand/3
    (stdlib) lists.erl:1354: :lists.mapfoldl/3
iex(8)> f = fn(name) -> outside <> name end
#Function<7.91303403/1 in :erl_eval.expr/5>
iex(9)> f.("zachgardwood")                 
"Hello zachgardwood"

You see how it can ‘close’ around the environment to carry ‘new’ data that may not even exist until runtime? An OCaml Functor let’s you do the same thing with Modules. In Elixir it would have been like:

iex(1)> defmodule CondMap do
...(1)>   def create(cond), do: {__MODULE__, cond, %{}}
...(1)>   def get(key, {__MODULE__, _cond, map}), do: map[key]
...(1)>   def set(key, value, {__MODULE__, cond, map}) do
...(1)>     cond.(key, value) || throw "Invalid key and value: #{inspect {key, value}}"
...(1)>     {__MODULE__, cond, Map.put(map, key, value)}
...(1)>   end
...(1)> end
{:module, CondMap,
 <<70, 79, 82, 49, 0, 0, 7, 148, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 215,
   0, 0, 0, 25, 14, 69, 108, 105, 120, 105, 114, 46, 67, 111, 110, 100, 77, 97,
   112, 8, 95, 95, 105, 110, 102, 111, 95, ...>>, {:set, 3}}
iex(2)> string_map = CondMap.create(fn(_key, value) -> is_binary(value) end)
{CondMap, #Function<13.91303403/2 in :erl_eval.expr/5>, %{}}
iex(3)> string_map = string_map.set(:a, "hi")
{CondMap, #Function<13.91303403/2 in :erl_eval.expr/5>, %{a: "hi"}}
iex(4)> string_map = string_map.set(:b, :an_atom)
** (throw) "Invalid key and value: {:b, :an_atom}"
    iex:5: CondMap.set/3
iex(4)> string_map.get(:a)
"hi"

However, they encouraged the OTP group to remove tuple call support, which is what enabled Functors on the BEAM, so now if you try to do that on modern OTP versions you just get:

iex(3)> string_map.set(:a, "hi")
** (ArgumentError) you attempted to apply a function on {CondMap, #Function<13.91303403/2 in :erl_eval.expr/5>, %{a: "hi"}}. Modules (the first argument of apply) must always be an atom
    :erlang.apply({CondMap, #Function<13.91303403/2 in :erl_eval.expr/5>, %{a: "hi"}}, :get, [:a])

So yeah, a useful feature was broken… :cry:

Being able to ‘close’ around a Module just like you can a Function to make a Closure is so very useful. Yet they decided to keep Closures, yet get rid of Functors… :frowning:

4 Likes

I would love to have elixir without feature of rebinding variable names, this will eliminate the need of prefix the variable ^.

I see often times, people miss to use ^ when they want to do strict pattern matching without rebinding. I would prefer to compromise the ease of rebinding with forcing the programmer to bind to another variable when they need to change the value.

It would be great to have this as a selectable option when are booting repl or compiling the code.

3 Likes

defguards with pattern matching support

A way to customize, extend or replace iex.

E.g., the Python world has the alternate iPython REPL. Ruby has pry, which is an alternate to the built-in irb.

But iex is only a very thin wrapper around an Erlang tool, which wasn’t designed for much customization. Changes mean PRs to Erlang, which I’ve found difficult to get support for.

I’d like these customization points because I see many places that the iex experience can be improved, putting it on par with other contemporary REPLs. E.g.,

  • ctrl-d to exit
  • ctrl-l to clear screen
  • Pager for help and completion output
  • Completions sorted vertically in columns (ls style), instead of horizontally (DOS dir style).

Compare iex sorting to a couple other REPLs:

The lack of a pager in iex means the user has to scroll around to find the start of the content. Here’s ipython vs. iex showing String help.

tldr; I’d like the ability to make these improvements.

5 Likes

IEx is regular application and it explictly do not use Erlang shell. So nothing prevents you from writing your own Elixir shell with all the features you want.

3 Likes

Yeah I’m still not a fan of name rebinding, or at least rebinding without explicit syntax for it, it shouldn’t be the default path…

I initially had a proposal for that and a mock-up of how it would work, sadly it didn’t get any real interest…

2 Likes

Thanks - that’s interesting. Maybe I got confused from previous conversations about needing to make changes on the Erlang side. (?)

1 Like

If you could change one thing in Elixir language, what you would change?

Get rid of alignment in the formatter, in favour of indentation-only (I can’t even stand using the formatter anymore because of this).

1 Like

Well make the change in freedom_formatter. ^.^

1 Like

I usually make my rebindable variables end in ! And I have credo check that for me.

2 Likes

One thing more that would like to change is that - You should not successfully pattern match empty map with any other maps.

%{} = %{abc: "something"} # This should fail, but it is not.
# %{abc: "something"}

a = %{abc: "something"}
case a do
%{} -> "this" # This should not be matched, but this one is matched.
%{abc: _} -> "that"
# "this"
1 Like

If Elixir had static typing it would be the “perfect” language for me.

6 Likes

Ha! Was just going to write that! :smiley:

My main grieve is Elixirs refactorability. It is not bad but it also does not spark joy :slight_smile:

2 Likes