News for hackers. Stuff that matters.
Re:Elixir web development 101: collaborative todolist with r... by pmontra on Friday October 27, 2017 @09:47AM (#15568177)
I'm in the fourth month of a Phoenix project, which shields me from almost all of OTP, and I agree with your point. Maybe even "Erlang with a Ruby syntax" would be too much. Similarities with Ruby don't go any further than "def" "do" "end" and many library modules and functions deliberately named to mimic corresponding modules and methods in Ruby. Everything else is different, both syntax and semantic.

After 10 years of Rails, all things considered Elixir and Phoenix are pretty good.

The best feature of the language so far is pattern matching. It can be used in the arguments of function definitions. It makes all the "if"s go away. It seems strange after 30 years of programming, but a program without conditionals is easier to read.

Not having to use an external background job framework is great news too. Just spawn your processes to send a mail, process something asynchronously and store the result in the database. No Sidekiq (or Celery if you're into Python.)

On the awful side, Ecto is too low level and it feels like a self inflicted pain. I understand the reasons behind its design decisions, but the typical web application or mobile backend don't need all of that. Actually the team working with me on this project wrote a lot of code to build queries and wrap them into another layer to return {:ok, result} / {:error, reason} tuples. We're calling them from controllers more or less as Model.get_something(args). I checked it now and we have zero occurrences of " from " in controller code. All the queries are confined inside a model. If we were using Rails ActiveRecord would have written all that code for us. Instead we had to use a more verbose query syntax, write our functions and tests. This is a net loss of productivity.

There are some modules that can be mounted on the top of Ecto to make it look like ActiveRecord or other ORMs for other languages. I'm looking forward to using Ecto.Rut in my next project. I hope I'll never have to use Ecto directly.

https://github.com/sheharyarn/ecto_rut

We almost didn't use the supervisor layer of the language (somewhat like systemd/init for who's not familiar with it.) I used it in smaller projects and I feel like it's a little too complicated. Same for defining a GenServer, more complex and verbose than exposing the same functionality with methods from a object. I know we have to split the code between what runs in the client process and what runs on the server process, but I feel there should be a vanilla GenServer that handles the most straightfoward case inside the behavior, which is no code in the client with the exception of passing the arguments to the server. Maybe use the name, sigils or module attributes to declare if there will be a synchronous return value.

Anyway, Elixir is still young and there is time to smooth the rough edges. So far it's an acceptable contender and definitely better than the popular scripting languages when concurrency is important.

Re:Elixir web development 101: collaborative todolist with r... by karmajunkie on Friday October 27, 2017 @10:39AM (#15568760)
Don't take this the wrong way, but it sounds like you're approaching this from the perspective that Rails' way is the "correct" way when in fact almost everything about Rails' doctrine will cause you problems over the life of a project.

> We're calling them from controllers more or less as Model.get_something(args). I checked it now and we have zero occurrences of " from " in controller code. All the queries are confined inside a model.

Yeah, that's almost a best practice, except you're regarding the "model" (the terminology is 'schema' in Ecto) as primary instead of building it as a separate context or application (depending on your preferred approach to modularity within Elixir/Phoenix). For that matter, I fail to see the advantages of `Model.get` over `Model |> Repo.get(id)`.

> If we were using Rails ActiveRecord would have written all that code for us.

There's not a substantial difference between the kind of AR usage that's on the happy path and the equivalent query in Ecto.

> I know we have to split the code between what runs in the client process and what runs on the server process, but I feel there should be a vanilla GenServer that handles the most straightfoward case inside the behavior, which is no code in the client with the exception of passing the arguments to the server

You mean like an Agent[1]?

[1] https://hexdocs.pm/elixir/Agent.html

Re:Elixir web development 101: collaborative todolist with r... by pmontra on Friday October 27, 2017 @12:19PM (#15569815)
First of all thanks for the detailed answer. It contains many interesting points.

I don't think that AR is correct, I think that AR is more convenient than Ecto in many cases we run into in web development. With AR I don't have to write my own queries and encapsulate them into functions (but it's kind of what we do with scopes) and it has a more compact syntax. Ecto is more flexible, but that flexibility is not needed in most cases. Anyway when we must really be flexible we write queries in SQL and manually unmarshal resultsets. ORMs/Data Mappers are for simple and medium use cases.

I still didn't run into problems with the AR approach. Maybe it's because all of my projects were medium or small sized. I found AR to be perfect for them to the point that I want to disguise Ecto as AR using Ecto.Rut.

> you're regarding the "model" (the terminology is 'schema' in Ecto) as primary instead of building it as a separate context or application

This sounds interesting but I fail to understand what you mean. Would you mind explaining or posting a link? Thanks.

> I fail to see the advantages of `Model.get` over `Model |> Repo.get(id)`

It's shorter but not by much. One reason is that Repo doesn't mean much to me, so it could be hidden. What I care about is Model. But getting values out of the db is not such a pain. Inserting and updating is, because they are more verbose. I quote Ecto.Rut for the insert

    Post.insert(title: "Awesome Post", slug: "awesome-post", category_id: 3)
    # instead of:
    # changeset = Post.changeset(%Post{}, %{title: "Awesome Post", slug: "awesome-post", category_id: 3})
    # YourApp.Repo.insert(changeset)
If all of those extra characters are for extra flexibility (maybe for using more repositories in future?) then it smells of premature optimization. I'll happily do without it.

Regardless of this discussion, IMHO a thing that Ecto should fix is requiring developers to write both the migration and the schema. It's either the AR way, migration first and auto generated model, or the Python way, model first and auto generated migration. We got some bugs because we didn't write the same things inside the migration and the schema. Mistakes happens and the tools we use should help us not the make them. Ecto is not DRY but it should.

Btw, the compactness argument applies more or less to all of Ruby vs Elixir because of the object.oriented.notation.is.shorter than the Module1.functional |> Module2.way |> Module3.of |> Module4.composing. Aliases help to some degree but they add clutter to the top of the file. It's a little nuisance but pattern matching more than evens it.

I have to look more into Agent (thanks) but it seems exactly the opposite of what I want: write in the module only the code that should run in the server. Probably what I'm looking for is a macro that writes a vanilla GenServer for me hiding all the functions run on the client.

Re:Elixir web development 101: collaborative todolist with r... by out_of_protocol on Friday October 27, 2017 @01:33PM (#15570496)
You could do this as well:

  Repo.insert! %Post{title: "Awesome Post", slug: "awesome-post", category_id: 3}
If you don't need flexibility. Well, usually that's bad idea - you'd want to carefully handle what's can be mass-assigned and what should be carefully set by hand. In case of ecto, you'd do:

  %Post{user_id: current_user.id}
  |> Post.changeset(attributes)
  |> Repo.insert
In case of rails/AR you'll use strong params (or alternatives)

  Post.create(post_params.merge(user_id: current_user.id))

  def post_params
    params.require(:post).permit(:title, :slug, : category_id)
  end
Re:Elixir web development 101: collaborative todolist with r... by passer-by-123 on Friday October 27, 2017 @01:05PM (#15570226)
> If all of those extra characters are for extra flexibility (maybe for using more repositories in future?) then it smells of premature optimization. I'll happily do without it.

I wouldn't say it is to use more repositories in the future (I also hate future-proofing code) but rather to make it explicit what is happening on the database side. It aligns well with other ideas in Ecto, such as letting the database do uniqueness checks.

Most of the problematic Rails projects I worked with were because of this coupling between business logic and database that ActiveRecord encourages. But this is nothing new, it is one of the top 3 complaints about Rails.

Re:Elixir web development 101: collaborative todolist with r... by angersock on Friday October 27, 2017 @12:37PM (#15569963)
Yours is a toy example though, yeah? You skip the n+1 problems Ecto makes really obvious and which AR encourages.
Re:Elixir web development 101: collaborative todolist with r... by karmajunkie on Friday October 27, 2017 @06:52PM (#15572885)
I think I came off snarkier than I intended to above, thank you for reading that gracefully.

> I still didn't run into problems with the AR approach. Maybe it's because all of my projects were medium or small sized. I found AR to be perfect for them to the point that I want to disguise Ecto as AR using Ecto.Rut.

Perhaps its a stylistic thing, but recent versions of Ecto feel quite natural to me. I frequently use Ecto without a database by using it for embedded schemas (which I don't embed)—they're basically just structs that I can use with changesets a little more cleanly.

As someone else noted, you don't have to use changesets for the happy path interactions like simple inserts. It really just comes down to whether you can live with the Repo as the first thing you type instead of the schema.

> This sounds interesting but I fail to understand what you mean. Would you mind explaining or posting a link? Thanks.

Rails teaches us to look at the model as the primary point of business logic. In my view this is putting the cart before the horse. By putting the Model (big M, not little m) we limit our thinking around abstractions to what we can represent structurally in the database. A well-designed, thought-out application model (little m, not big M) encompasses much more than database tables. With AR, even if you try to create behaviors on fat models, ultimately the only vocabulary you have to work with are those nouns the database allows you.

Ecto does something subtle but important: it demotes your data schema to just that—data. Behavior is modelled in the messages you pass between processes, which is why you see so many people in the elixir community jumping into architectural techniques like eventsourcing. While things like Ecto.Rut add some conveniences, they also encourage promotion of the data to the central artifact of the system. After building Rails applications from small to very large over 10 years, I can say for my part I want to stay as far away from that as possible. Its convenient until its not, and when you can start to feel the pain of it, its very difficult to unwind its effects throughout your system.

re: agents, maybe what you're looking for is ExActor: https://github.com/sasa1977/exactor

Worth noting, especially if you're in the first 12-18 months of using Elixir: OTP is incredible, but it takes time to wrap your head around all its pieces. I usually recommend new users coming from an MVC framework just try to muddle along using Phoenix as they would Rails until they start getting comfortable with things like supervision trees and GenServers... and then the fun really starts. All the rest of it, like Ecto's hands-off approach to the database, really starts to make sense around that time.

Re:Elixir web development 101: collaborative todolist with r... by pmontra on Sunday October 29, 2017 @06:32AM (#15578799)
ExActor is more or less what I was looking for. It deserves its 491 stars (492 now.) A big thanks for that!

After so many years of software development I don't like unnecessary complexity, that's why I'm keen to shave off features from Ecto and GenServer (and a lot of other tools, not only in Elixir) and settle for a subset with an easier API.

That said, you have a point when you write that AR's approach is "convenient until its not, and when you can start to feel the pain of it, its very difficult to unwind its effects". The project I'm working on is an MVP. Who knows where my customer is going to be in 12 month. What I know for sure is that using Ecto cost them some extra time to deliver because of all that boilerplate we had to write. Would I add an extra layer between the db and the logic in a Rails application if its requirements imply complicated logic and interactions? Probably yes. We can put any kind of objects in the models (or lib) directory of Rails, not only ones derived from AR.

Re:Elixir web development 101: collaborative todolist with r... by karmajunkie on Monday October 30, 2017 @10:39AM (#15585257)
I think I'm still left with the impression that you may be making things a little harder on yourself than they need to be, or perhaps better stated, than they need to be now... Ecto's API has gotten streamlined over the last few versions, such that in most common operations, I can't see much more than a cursory cosmetic difference in the APIs, like starting the call chain from the Repo instead of the Schema. What I will readily concede is more complicated is the support for things that are widely considered to be antipatterns, like STI and polymorphic associations.

> After so many years of software development I don't like unnecessary complexity, that's why I'm keen to shave off features from Ecto and GenServer (and a lot of other tools, not only in Elixir) and settle for a subset with an easier API.

This is what I'm missing—there's essential and accidental complexity, and when I look at GenServer, I see an API that's been shaved down by decades of practice in Erlang to its most essential complexity. Even ExActor is just a set of macros for generating those essential parts—it basically just saves on typing, not skipping functionality. Ecto hasn't had the years of legacy that OTP has, so its API has fluxed a bit in the last few years, but its still something I think is cut down to the bare minimum for healthy database interaction.

This kind of discussion is best handled by email, i think, and mine is in my profile—feel free to send a gist of something that illustrates what you're talking about, and maybe I'll have a better understanding of what aspects of the API are headaches. I'm really curious what I'm missing about your perspective here.

Re:Elixir web development 101: collaborative todolist with r... by out_of_protocol on Friday October 27, 2017 @11:21AM (#15569248)
I don't see how Ecto is any worser than AR - imo it is not AND way more flexible. Also, `from ...` form is not the best choice - other form (e.g.

  user = User |> User.actual |> User.by_email(email) |> ....
) allows query decomposition, the same way AR does.

  user = User.actual.by_email(email)...
Re:Elixir web development 101: collaborative todolist with r... by pmontra on Friday October 27, 2017 @12:22PM (#15569845)
It is more flexible, but often that flexibility is not necessary. I replied in more detail to the sibling answer but in the Ecto version of this sample query, don't I have to write the definition of actual and by_email? AR gives me by_email for free. Instead, actual would be a scope and I have to write that code with AR too.
Re:Elixir web development 101: collaborative todolist with r... by out_of_protocol on Friday October 27, 2017 @12:43PM (#15570019)
Exactly the same:

  Repo.get_by(Post, title: "My post")
https://hexdocs.pm/ecto/Ecto.Repo.html#c:get_by/3

(Rails/AR deprecated find_by_xxx methods, promotes find_by() instead - for performance reasons)