cdegroot

cdegroot

Should we adopt Dave's way of building applications as a series of components? (Dave's talk has now been added!)

Another point that Dave drives home in his course is that applications really are components, and you should treat them as such. Not as “full fledged applications”. We’ve been doing that umbrella-style before, but umbrellas are confusing and I always felt that even mix phx.new --no-ecto --no-html --no-brunch api generated a ton of fluff. In the same vein that files/modules are free and you can use them to separate api/otp glue/implementation, applications are free and you can - and I think should - use them to separate concerns. The reason therefore that I’m putting this here is that I feel it’s basically the same principle at work.

We’re setting up a new service, that will be responsible for various internal permissions related stuff. The “hard” part so far was whittling down the Phoenix generated code to something clean and manageable:

..../api$ find test lib config
test
test/support
test/support/conn_case.ex
test/test_helper.exs
lib
lib/api.ex
lib/api
lib/api/endpoint.ex
lib/api/router.ex
lib/api/user_policy_controller.ex
lib/api/application.ex
config
config/config.exs
config/deploy.exs
config/dev.exs
config/test.exs

no channels, no contexts, no mixing up of UI and business logic in a single OTP application. The sole controller:

defmodule Api.UserPolicyController do
  @moduledoc """
  This module contains the interface between the API and
  the business logic in the user_policies application..

  """
  use Api, :controller

  def get_for_user(conn, user_id) do
    {:ok, policy_document} = UserPolicies.get_policy_for(user_id)
    json conn, policy_document
  end
end

which calls out to a sibling app (using the “poncho” organization style):

.../user_policies$ find test lib config
test
test/test_helper.exs
lib
lib/user_policy.ex
lib/user_policies.ex
config
config/config.exs

I like this a lot better than the Phoenix default “we are Rails and toss everything together” mess. These are skeleton both currently applications, but it is immediately clear where things go when we start fleshing them out, there’s very nice separation of concerns, a random bypasser can pick an interest and only open that code, and so on. Frankly, with the biggest cost being a top level Makefile to drive CI builds (I’m settling on a pretty standard skeleton for that), I think it’s worth pursuing. At least experimenting with, to see how it feels. My gut says it feels much better, and after ~40 years of coding, I have learned to rely on my gut. YGMV, of course.

(and I’ll shuddup now with my heresies :wink: )


Edit: See post 20 and below for Dave’s talk on this topic and the discussion following the video.

546 20021 107

Most Liked

joeerl

joeerl

Creator of Erlang - Fondly Remembered

Excuse the longish post - I too watched Dave’s talk and it brings up some important points.

In this posting I want to talk about composabilty.

To me the unit of composabilty should be the process and NOT functions - to be clearer, of course functions should be composable but this problem is nicely solved ( F1 |> F2 |> F3 |> … :slight_smile:

I think the gold standard for composabily were unix pipes, and the beautiful way they could be composed in the shell.

a | b | c | d ...

The principle design idea was “the output of my program should be the input of your program”

This allows a b and c to all be written in different languages - but this has a few nasty problems:

  1. text flow across the boundaries
    so there is a lot of extra parsing and serialising involved
  2. if something in the middle fails (say c) there is no nice way to close the pipeline down

One excellent feature is that (say) b does not know who sends it inputs and does not know to whom the outputs should be sent.

Now consider Erlang - one of the above problems gets solved - text does not flow across the boundaries but Erlang messages. X ! M sends a message, receive M → … end receives a message so no parsing and serialising is involved and it’s very efficient.

Processes do not know where they get messages from (=good) but have to know where they send messages to (=bad).

A better way would be to use ports, call them in1, in2, in3 for inputs and out1, out2, out3 for outputs and control1, control1 for controls

We can now make a component - assume a process x that has an input in1 which doubles its input and sends the result to out1 - this is easy to write in erlang

   loop() ->
      receive 
          {in1, X} ->
              send(out1, 2*X),
              loop()
      end

Clever people can write this in Elixir as well :slight_smile:

All the component knows how to do is turn numbers on the in1 port into output on out1 but it does not know where in1 and out1 are.

Now we have to wire things up.

The pipe syntax X | Y | Z means “wire up the output of X to the input of Y” (and so on)

The important point is that a) components do not know where they get their inputs from and do not know where they send their outputs to and b) “wiring up” is NOT a part of the component.

Elixir has a great method for wiring up functions X |> Y |> Z but the X,Y’s and Z’s are functions
NOT processes.

We can imagine components to be processes with inputs (in1, in2, in3, …) outputs (out1, out2, …) control ports (control1, control2, …) and error ports (error1, error2,…) - what are the error ports?

Error ports are for (guess what) errors - sending an atom to the in1 port of my doubling machine would result in an error message being sent to error1 (or something).

All of this can be nicely specified with some type system -

Machine M1 is

 in1 x N::integer -> out1 ! 2*N :: integer

etc. :slight_smile:

With this kind of structure software starts looking very much like hardware and we can make nice graphic tools to show how the components are wired up. The reason we do not program like this in sequential languages is because all the components MUST run in parallel (which is what chips do)

There is actually nothing new in the above - these ideas were first written down by John Paul Morrison in the early 1970’s (see Flow-based programming - Wikipedia) –

This (flow based programming) is one of those ideas we could (and should) revisit and cast into a modern form.

All of this means a bit of a re-think since most frameworks are structured on top of essentially sequential platforms.

Really we should be thinking in terms of “black boxes that send and receive messages” and “how to wire up the black boxes” and NOT functions with inputs and outputs, the latter problem is solved.

Think - “messages between components” and “what messages do I need in my protocol” NOT “input and output types” and “what functions and modules do I need”

(I called this Concurrency Oriented Programming a while back - but the term did not seem to latch on :slight_smile:

As Alan Kay said “the big idea is messaging”

Cheers

/Joe

josevalim

josevalim

Creator of Elixir

Sorry for being pedantic but, since you are asking someone else to build on their arguments, I would like to point out that you have not explained why any of the points above are true. I.e. it is not clear why the approach you mentioned above is easier to test, change, or reason about.

Just to make an absurd point, maybe the contract you define between those components pass all of the arguments between functions via the process dictionary. Maybe you do this:

# in the controller
Process.put(:product, product)
NewProductHandler.call()

# in the handler
def call() do
  product = Process.get(:product)

Now your code is broken between many files but it is still coupled and hard to test!

Every time somebody brings the argument that a module should only have a single function in their domain, I propose this: what if we remove Enum.map/2, Enum.reduce/3, etc and instead we introduce Enum.Mapper.call/2, Enum.Reducer.call/3 and so on?

To clarify: I am not saying breaking your software into multiple files is bad a thing but programmers clearly leverage different mechanisms to group functionality together. We need to identify when those mechanisms are measurably better (i.e. you can prove it leads to less coupling and better tests) and we need to identify when we feel the code is better.

Theoretically speaking, there is no difference between how easy to test or change is a big module MyApp with a 100 functions call_a, call_b, etc compared to 100 modules with a single function MyApp.A.call, MyApp.B.call, etc. There are, however, differences on how a developer understands and communicates with another developer in this codebase, which is what causes some people to feel one approach is superior than the other, but I can’t prove one is better than the other.

It is very important to make a distinction between proofs and feelings because they lead to very different discussions.

josevalim

josevalim

Creator of Elixir

I think it is important to clarify that, and I think Dave would agree, is that GenServer is not wrong. Rather we are wrong in using GenServer to build higher level components, often with domain/business logic, and we need higher level abstractions to solve those cases. When building low-level components, the distinction between client, server, and the low-level callbacks are important, but in your application domain you want to get rid of the bureaucracy and put the focus on the domain code. There are also other libraries attempting to tackle this particular issue.

I agree with Dave in many of the problems he raised: yes, we need to improve configuration (there was a whole other thread about it in the forum). Yes, it would be helpful to know if an application is stateful or is a singleton (and as I said in this thread, I don’t think looking at the supervision tree gives the complete answer). As Dave said, there is a lot to explore. :slight_smile:

Where Next?

Popular in Discussions Top

PragTob
Hello everyone, I know we had quite some threads (read through lots of them) about background job processing but it remains a hotly deba...
New
andre1sk
A big advantage to Elixir is all the distributed goodness but for many applications running on multiple nodes having integrated Etcd, Zoo...
New
Nvim
Elixir appears to be a superior language to Python. I don’t see any advantage of Python over Elixir. Are there any?
New
gausby
I asked this very same question on twitter and got some interesting feedback, but I thought it would be a good question to ask here as we...
1207 39467 209
New
ricklove
I was just introduced to Elixir and Phoenix. I was told about the 2 million websocket test that was done 2 years ago. From my research, t...
New
ejpcmac
I have discovered Nix last month and I am currently on my way to migrating to it—both on macOS at home and the full NixOS distrubution at...
New
marciol
Please, let me know if this kind of discussion already took place in another topic . Hi all, how do you consider if is better to build ...
New
klo
Got a question about when to concat vs. prepending items to list then reversing to achieve appending. So i know lists boil down to [1 | ...
New
joeerl
I’m playing with Elixir - It’s fun. I think @rvirding does give Elixir courses these days. Re: files and database - when I given Erlang ...
New
AstonJ
Seen any cool LiveView demos, sample apps or examples? Please post them here! :003:
New

Other popular topics Top

Nvim
Anybody knows a comprehensive comparison of Django and Phoenix, thanks for the help. Where are they similar? Where do they differ the m...
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 31013 112
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36352 110
New
boundedvariable
I am going through the kafka architecture. All the features what the kafka is providing are already in Erlang. I would like hear your opi...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
jaysoifer
Is there a way to rollback a specific migration and only that one (“skipping” all the other ones)? Would mix ecto.rollback -v 200809061...
New

We're in Beta

About us Mission Statement