Reducing the maximum latency of a bound buffer in Erlang
theerlangelist.com
theerlangelist.com
It covers numerous things: from the basics, all the way to OTP, meta-programming, and even certain popular libraries such as Plug and Ecto.
The thing that seemed magical about Erlang was how efficient it's syntax managed to be without being ambiguous. In a weekend I picked up the syntax, and in days I was being productive with it.
I've heard Elixir described as "Rubyfied-Erlang" which would explain why I don't particularly love it, since I don't love Ruby syntax (and I secretly harbor a tiny bit of resentment for Elixir for taking off and essentially cementing Erlang's place outside the spotlight), but it's not bad either and doesn't add that much syntax.
And it's not like the syntax of C-like languages hasn't evolved towards something that looks "different" anyways. Lambdas + Rx/Promises + Streams practically looks like it's own language in most C-like languages...
Syntax aside, the final implementation of this feels like, okay, well, you have access to an in-memory store that's outside of the GC and which works well for your use case here. I'd actually be more interested in further optimizations of the first approach, in-GC, which seem like they'd be more generalizable to other tasks.
The server implementation [0], for example, is 25 lines and handles a lot behind the scenes. Those 25 lines are all you need to define a process that can leverage all of the supervision and hot code reloading benefits offered by OTP. There's a lot going on, but it's mostly just pattern matching tuples and maps, and it only takes a few hours of messing around with Erlang or Elixir before that kind of thing stops looking so scary.
[0] https://github.com/sasa1977/erlangelist/blob/master/examples...
%__MODULE__{buffer | size: buffer.size + 1, queue: :queue.in(item, buffer.queue)}
So that involves a bunch of operators that are very specialized to Elixir -- the %, the | are both very unclear in my opinion. And the __MODULE__ construction is clumsy.Or worse:
{:ok, {
:ets.lookup_element(buffer.ets, pull_index, 2),
%__MODULE__{buffer |
size: buffer.size - 1,
pull_index: rem(pull_index + 1, buffer.max_size)
}
}}
Same problem as above, plus the nested set structures that Erlang loves, but which rapidly become super difficult to decipher. Oh, and also the overloading of atoms to work like pseudo-objects.% and | are going to be unclear if you're an outsider looking in on the language, but that is true for any sort of specialization. I'd be interested to know what languages you find to be readable, because I can all but guarantee that there's aspects of that language that I'd find arcane -- not because they are, but because I don't know the language.
I'm not terribly interested in discussing whether a language is readable or not, because a lot of that is going to come down to subjective opinion, but at the end of the day, I find that Elixir lets me quickly write maintainable code.
I'm not entirely sure what you mean by "overloading of atoms to work like pseudo-objects", do you mind elaborating?
It's perfectly idiomatic and straightforward to read if you've written enough erlang or elixir to recognize the syntactic elements without the translation hiccup. At this point I find Elixir code a great deal easier to read than a lot of ruby, php, or C++ code I've had to work with in the past. While it may not really be your cup of tea, you can't fairly accuse it of something that has more to go with the reader than the writer.
My problems with __MODULE__ are, first, it's just super clumsy to have heavily used language constructs with lots of underscores around them, and I don't really understand why anyone ties themselves to that particular semantic. I hate it in Python, too. Almost any other way of distinguishing it from other language elements would be less jarring to read.
But second, %__MODULE__{ buffer | size: buffer.size + 1, queue: :queue.push(buffer.queue, whatever) } does some kind of magic that I'd have to look up the details of, but basically copies buffer but overrides some of its elements, right? That's why you don't have to set max_size explicitly? It's a hack that lets you have actually immutable objects but deal with a logically mutable buffer. It's a weird combination of verbose (to copy buffer, I have to write %__MODULE{ buffer, where in another language it might be like buffer.copy()), and compact to the point of overly dense information (I'm putting a lot of expressions on one line), and it's implicit, too -- what parts of buffer are being copied over unchanged? Sorry, better go and look somewhere else. I think this is legitimately hard to get right -- the immutability constraint means that a lot of the standard idioms of programming are a poor fit -- but ultimately this is Elixir's job.
The dot operator can be used on an atom type to invoke a function from the corresponding module. A capital Foo.Bar is called an alias in Elixir, and it's the same as an atom :"Elixir.Foo.Bar".
The usage of __MODULE__ is optional. You can also write %Buffer.Ets{...} if you prefer.
There's no hidden magic with %__MODULE__{buffer | ...}. It's an "update" syntax which allows you to take an existing "struct" (essentially a bunch of well-known named fields), and create a transformed version of it with some fields changed. The code %__MODULE__{buffer | size: buffer.size + 1} is therefore the same as %Buffer.Ets{buffer | size: buffer.size + 1}, and it evaluates to a transformed buffer with the size field set to the size of the original buffer incremented by 1.
The original buffer is left intact (since you can't do in-place data mutation in Elixir). However, the transformed version is not a full deep copy of the original. It's going to share as much data as possible with the original version.
Hope that helps :-)
element = :ets.lookup_element(buffer.ets, pull_index, 2)
pi = rem(pull_index + 1, buffer.max_size)
buffer = %Buffer.Ets{buffer | size: buffer.size - 1, pull_index: pi}
{:ok, {element, buffer}}
Would you care to elaborate which parts you consider to be "gobbledegook"? The code supplied is just a few lines of code.
I don't know how familiar are you with the language, but the magical aspect of it will quickly disappear once your learn a few OTP patterns.
I personally started initially with Erlang, but when I discovered Elixir, I switched, as I found it way more pleasurable.
Python and Elixir are actually the only two languages that make me happy when writing code, and I've tried quite a few of them, from C all the way to Haskell.
I thought the same thing once, until I actually sat down and tried to learn Erlang. It turns out that a lot of the decisions that I originally thought were asinine, actually make a lot of sense.
Line terminators for example (, ; .) tend to receive a lot of flak, but they really aren't as crazy as they first seem. Commas come after normal lines of code (you can read them as and), semicolons come at the end of clauses in if or case statements (and you can read them as or), and periods mark the end of a function. It's a little more verbose than something like Javascript or Ruby, but it really isn't as confusing as critics tend to claim!
In the end, even as somebody who cares about the look of code a lot, it doesn't matter to me. The BEAM and OTP are elegant enough to compensate for any syntax.
thanks