Available for work

I’m now available to take on new iOS and Elixir projects. I have experience working with distributed teams, fast-paced projects, and leading teams.

If you want to ship a 1.0, please do get in touch over at oscar@swanros.com and let’s talk about your needs.

You can learn more about my background and experience downloading my resume over here.

Also, I’d greatly appreciate if you shared this post with anyone you think may be interested in hiring an experienced iOS developer!

Thanks!

Publicado en Meta | Etiquetado , , , , , , , | Comentarios desactivados en Available for work

guards in Elixir 1.6

Elixir 1.6 has been released with a lot of nice additions to the language, but without a doubt, what I’m most pumped about is real guard support in the language.

Before 1.6, you could constraint a function like so:

def change_settings(%User{admin: is_admin}, settings) when is_admin == true do
    # Update the settings is only available for admins
end

This worked just fine, but now with real guard support, we can extract those checks to make reuse and composition easier!

defmodule User do
  defstruct [:karma, :friend_count]
end
defmodule Main do
  defmacro can_update_settings(karma, friend_count) do
    quote do
      unquote(karma)> 100 and unquote(friend_count) > 3
    end
  end
  def change_settings(user = %User{karma: karma, friend_count: count}, settings) when can_update_settings(karma, count) do
    IO.puts "Updating settings..."
    {:ok, user}
  end
  def add_member(user = %User{karma: karma, friend_count: count}, new_user) when can_update_settings(karma, count) do
    IO.puts "Adding member"
    {:ok, user}
  end
  def main do
    %User{karma: 101, friend_count: 4}
    |> change_settings(%{})
  end
end
Main.main
// => "Updating settings..."

You can skip the manual macro creation alltogether if you use defguard and defguardp, though. These are new functions in the Kernel module that just take your constraint and generate a macro suitable for use as a guard from it.

defguard can_update_settings(verified) when verified == true

I specially love the fact that now you can also use multiple guards with functions:

defmodule User do
  defstruct [:karma, :friend_count]
end
defmodule CommunityFactory do
  defguard has_enough_karma(karma) when karma > 100
  defguard has_invited_enough_friends(friend_count) when friend_count > 3
  def create_community(%User{karma: karma, friend_count: count}, com_name)
    when has_enough_karma(karma)
    when has_invited_enough_friends(count) do
    IO.puts "Creating community..."
    {:ok, %{name: com_name}}
  end
  def create_community(_user, com_name) do
    IO.puts "Cannot create #{com_name}. User still does not complete onboarding."
    {:error, nil}
  end
end
defmodule Main do
  def main do
    %User{karma: 101, friend_count: 5}
    |> CommunityFactory.create_community("My Community")
  end
end
Main.main
// => Creating community...

I use guards like crazy in Swift. I think they’re a great tool to structure your code. However, in Swift, guards can be used (and maybe abused) more as a fancy if/else dance, rather than a mean to express condition conformance.

For instance, in Swift, the only requirement for a guard is that it breaks the flow of the function:

guard user.isAdmin else {
    print("User is not admin!")  # Compiling error, does not break function flow
}
// Update settings here

But literally nothing stops you from transferring the control over to another function!

guard user.isAdmin else {
    showErrorMessage(.notAdmin) // Give up control of the execution flow
    return
}
// Update settings here

Although this may seem harmless, it can cost some precious down the road when you’re trying to debug something that just follows a trail of guarded statements that keep on going forever.

With Elixir’s flavor of guards, this is explicitly not possible:

Not all expressions are allowed in guard clauses, but only a handful of them. This is a deliberate choice: only a predefined set of side-effect-free functions are allowed. This way, Elixir (and Erlang) can make sure that nothing bad happens while executing guards and no mutations happen anywhere.

Altough I love Swift, I do think that its guard does not entirely guard you from shooting yourself in the foot.


Small but thoughtful updates like this to the language really make me hopeful for the future of Elixir.

You can read everything about the new Elixir guards on the official documentation page and read the release announcement here.

Publicado en Elixir | Etiquetado , , , , , | Comentarios desactivados en guards in Elixir 1.6

Experimenting with inherited tables in Ecto

I’ve been working on a personal project with some mates for the past few months, and I’ve been mostly responsible for building our backend with Phoenix.

It’s come to a time where we need to implement some sort of "comments & likes" functionality for the app, so I set out to start thinknig how to go about it.

Note: this is just me experimenting, not trying to say that this is the right way to do it. I’m still learning about these things, so if you think I’m missing something, by all means, let me know at oscar@swanros.com

Note 2: the example code for this post is on GitHub.

When I started digging around, most of the answers I found about how to implement such functionality pointed to using table inheritance so that I could have a ActionableEntities table, and EntityComments and EntityLikes tables. Then, Posts, Photos, Events whould inherit from ActionableEntities gaining the ability of being commented or ‘liked’.

"Simple enough," I said to myself. Then I started digging. This is what I found.

Implementing table inheritance with Ecto

I couldn’t find concrete examples of how to do this, but did read the official Ecto documentation, though, and found that you can pass an :options parameter to specify extra attributes that you want your table to have, such as WITH, INHERITS or ON COMMIT. So, the migration looks like this:

def change do
    create table(:actionable_entities) do
        timestamps()
    end
    create table(:entity_comments) do
        add :content, :string
        add :entity_id, references(:actionable_entities)
        timestamps()
    end
    create table(:posts, options: "INHERITS (actionable_entities)") do
        add :content, :string
    end
    # photos and all other tables follow the same structure as the posts one.
end

Now the modules that’re going to be using each of these tables are defined as follows:

defmodule Inh.ActionableEntity do
  use Ecto.Schema
  schema "interactive_entities" do
    has_many :comments, Inh.EntityComments.Comment
    timestamps()
  end
end
defmodule Inh.EntityComments.Comment do
  use Ecto.Schema
  schema "entity_comments" do
    field :content, :string
    belongs_to :entity, Inh.ActionableEntity
    timestamps()
  end
end
defmodule Inh.Posts.Post do
  use Ecto.Schema
  schema "posts" do
    field :content, :string
    timestamps()
  end
end

"This ought to work right here," I thought to myself. And it does for the most part: I can create a post, and get a list of posts. I can even query for a Post‘s comments eventhough there are none on the database.

However, the following error on the database arises when trying to create a comment for a given post:

ERROR: insert or update on table "entity_comments" violates foreign key constraint "entity_comments_actionable_entity_id_fkey" DETAIL: Key (entity_id)=(1) is not present in table "actionable_entities".

The code used for trying to insert a new comment looks something along the lines of

def create_for_post(id, c_params) when is_integer(id) do
    Repo.get(ActionableEntity, id)
    |> Ecto.build_assoc(:comments, c_params)
    |> Repo.insert()
end

First get the post I want to comment, build the corresponding association and then insert it into the database. Here’s the problem, though:

indexes (including unique constraints) and foreign key constraints only apply to single tables, not to their inheritance children.

So, if I add a record to posts or to photos, the information from the inherited table bubbles up to teh parent table. But then, technically, the information is a post, not an actionable_entity. 🤔

Postgres is right to be telling me that there’s no ActionableEntity with the given id.

I could get more of a sense that something is really wrong here by creating a new photos table that too inherits from actionable_entity and adding some records to it:

At this point, there’s no data consistency as the database can’t distinguish between Photos and Posts.

One way to solve this is to create a trigger on the database to check every time we try to inset a new EntityComment for a Post, that the Post indeed exists on the database. To do this, the foreign key constraint needs to be removed from the database. So first, update the entity_comments migration:

create table(:entity_comments) do
  add :content, :string
  add :entity_id, :integer
  timestamps()
end

entity_id is now just a simple :integer, there’s not an explicit reference to the actionable_entities table.

Then, create the trigger:

execute """
  CREATE OR REPLACE FUNCTION internal_post_check() RETURNS TRIGGER AS $$
  BEGIN
    IF NOT EXISTS(SELECT 1 FROM posts WHERE id = new.entity_id) THEN
      RAISE EXCEPTION 'Post does not exist:  %', new.entity_id;
    END IF;
    RETURN new;
  END;
  $$ language plpgsql;
"""
execute """
  CREATE TRIGGER CheckEntityExists BEFORE INSERT OR UPDATE ON entity_comments
  FOR EACH ROW EXECUTE PROCEDURE internal_post_check();
"""

This trigger will run every time a new record wants to be inserted on the entity_comments table and will manually check if a post with the value on entity_id as id exists.

It may seem that the problem is now solved, but then again, what would happen when I have a Post and want to retrieve its comments? If I have Posts and Photos and potentially N number of "interactive entities" on my database, how would I be able to query just for those?

I solved this by adding a new type:String column on the actionable_entity table. Posts would have the value post in that column, photos would have the value photo, and so on for every type of actionable entity I eventually add to the database.

This way, now I can query for the comments of a specific photo with a given id:

def comments_for_post(id) when is_integer(id) do
    q = from entity in ActionableEntity,
        where: entity.id == ^id and entity.type == ^"post",
        left_join: comments in assoc(entity, :comments),
        preload: [comments: comments]
    post = Repo.one(q)
    post.comments
end

Final notes

Although this works it does require to bypass the database’s integrity checks to handle those on my own. This is very error prone and would require me to constantly run tests to verify that I’m not missing addig a new trigger for photos, events, or any other "actionable entity" that I want to add to my system.

Also, if I decide to add another kind of action to these entities, such as "likes" or "claps," I’d have add another set of checks for those too.

This is very soon becoming a maintainability nightmare.

I asked friend whose really experienced with backend development and he just sent me this link to a post called Three Reasons Why You Shouldn’t Use Single Table Inheritance. Read it.

In the end, this was just me trying to implement a solution that I thought would make for a good one, but it seems that the compromises that need to be made here are not worth it.

What I went with was the simplest approach: add to the entity_comments table the columns of the entities that I want to enable comments for:

id | content | post_id | photo_id | event_id | inserted_at | updated_at
-----------------------------------------------------------------------
1    Hey!      1                               -----         -----
2    nice!               1                     -----         -----
3    Good!     4                               -----         -----

As stated in schema.ex#L806:

Unless you have dozens of columns, this is simpler for the developer,
more DB friendly and more efficient in all aspects.

So there’s that! Thanks for reading.

Publicado en Elixir | Etiquetado , , , , | Comentarios desactivados en Experimenting with inherited tables in Ecto

Becoming a Better iOS Developer Through Tooling: My AltConf 2017 talk

The fine folks at Realm have now uploaded the remaining videos from AltConf 2017 — including the presentation that I gave there.

You can watch it a Realm Academy, here.

Presenting in the US for the first time was a really exciting experience for me — and something that I’m going to push myself to do more of. I really loved the rush of being there presenting in a foreign country on a foreign language.

I’m gonna call this one a victory — although I know there’s some stuff in the presentation that could’ve used a little more polishing, I’m quite happy with how everything turned out.

What’s nice about this, is that I can now watch myself with a calm head and take notes of things that I can improve for future conference talks in the US or around the world.

I’d really appreciate any constructive feedback you share with me about this presentation. Hit me up at oscar@swanros.com, or @Swanros on Twitter.

Publicado en Speaking | Etiquetado , , , , | Comentarios desactivados en Becoming a Better iOS Developer Through Tooling: My AltConf 2017 talk

On iOS interview exercises

Today I saw this Tweet:

It spawned a great discussion on Twitter and thought I’d share my perspective on this.

Although this may seem like a dull exercise to ask an interviewee to do, in my experience I’ve found it to provide a great amount of insight into how the one doing it thinks about solving a problem.

By revieweing this exercise’s deliverable, I can know…

  • How they think about data modeling
    • Are the models structs or classes?
  • How they reason about data flow
    • Did they create a manager for the data layer?
    • Whose responsability is to download the data and parse it?
    • How should changes be bubbled up to the UI?
  • How they feel about using dependencies
    • Did they use Alamofire to consume the API?
    • Did they use a library to parse JSON?
  • How they approach API design
    • Are closures used intellengtly, or at all, where they would become handy?
    • What’s the “no-data” state, nil or []?

I could go on.

What’s best, is that many interesting conversations that provide even more useful information can be spun-off from any of those questions above.

In my experience, I’ve found this very exercise to have a different outcome every time I use it to interview someone.

Publicado en Work | Comentarios desactivados en On iOS interview exercises

Apple starts rejecting apps with “hot code push” features

This thread from Apple’s Developer Forums is making some waves on the internets today. Apparently, they’re now rejecting apps that include code that can modify your app after it’s released to the store.

Rollout.io seems to be the SDK that’s setting the alarm off, so far.

If you use Rollout.io, or another “push code after you release to the store” service, it’s time to start thinking about changing your strategy. (clears throat I told you).

From Rollout.io‘s FAQ:

Does Rollout comply to Apple’s Guidelines?

Yes. As per Apple’s official guidelines, Rollout.io does NOT alter binaries. Rollout uses JavascriptCore to add logic to your patches. For more details, check out how Rollout is compliant with App Store guidelines.

I wouldn’t build my business on such a fine line.

And also, please don’t act surprised when Apple start rejecting React Native apps down the road.

The moral of the story: stay away from JavaScript.

Publicado en Software | Etiquetado , , , , , | Comentarios desactivados en Apple starts rejecting apps with “hot code push” features

action_fallback and contexts in Phoenix 1.3 made my controllers tiny!

Yesterday I posted about how Phoenix 1.3 was pure love for API development. In that post, I mentioned that one of my favorites features in this new release is the action_fallback plug.

Today, I want to talk a little bit more about a killer combo I’ve uncovered: action_fallback + contexts.

I’m still amazed at how Phoenix 1.3 enables my controllers to be so tiny.



# Background
In my application, I have a `User` module that’s managed through an `Accounts` context:

def update(conn, %{"id" => id, "user" => user_params}) do
  user = Accounts.get_user(id)
  with {:ok, %User{} = user} <- Accounts.update_user(user, user_params),
  do: render(conn, "show.json", user: user)
end

The default MyApp.Web.FallbackController that’s generated when you first create your app includes, amongst others, a call/2 definition that accepts an %Ecto.Changeset{} instance:

defmodule MyApp.Web.FallbackController do
  use MyApp.Web, :controller
  def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
    conn
    |> put_status(:unprocessable_entity)
    |> render(MyApp.Web.ChangesetView, "error.json", changeset: changeset)
  end
  #
end

This function will be called if, for instance, the update/2 method on UserController failed to pattern match the with statement. Repo.update/1 returns {:error, changeset} in case it fails. Since I didn’t define an else path, {:error, changeset} would be then forwarded to MyApp.Web.FallbackController.



# Custom Errors
I like to define custom errors for the domains for my app. The default behavior on `MyApp.Web.ChangesetView` will output something like this:

{
  errors: [
    "username has already been taken"
  ]
}

I modified MyApp.Web.ChangesetView to expose the error the way I wanted:

defmodule MyApp.Web.ChangesetView do
  use MyApp.Web, :view
  alias MyApp.ResponseWrapper
  def translate_errors(changeset) do
    case hd(changeset.errors) do
      {label, {"has already been taken", _}} ->
        "#{label}_taken"
      {:password, {"should be at most %{count} character(s)", _}} ->
        "password_too_long"
      {:password, {"should be at least %{count} character(s)", _}} ->
        "password_too_short"
      _ ->
        :something_went_wrong
    end
  end
  def render("error.json", %{changeset: changeset}) do
    ResponseWrapper.error translate_errors(changeset)
  end
end

In the translate_errors/1 function, I take the first error off the changeset, and then pattern match its content. I can add more patterns to match later depending on my needs. Now, MyApp.Web.ChangesetView outputs errors like this:

{
  status: "error",
  error: "username_taken"
}
{
  status: "error",
  error: "password_too_short"
}



# Benefits
What’s great about that, is that now I can basically reuse that same code for any other module that needs to be inserted in my database.

This lets me write code like this:

defmodule MyApp.Web.DeviceController do
  use MyApp.Web, :controller
  alias MyApp.{Authentication, Notifications}
  plug :scrub_params, "device" when action in [:create]
  plug Authentication
  action_fallback MyApp.Web.FallbackController
  def create(conn, %{"device" => device_params}) do
    with {:ok, user} <- Authentication.get_current_user(conn),
         {:ok, _dev} <- Notifications.add_device(device_params, user),
         do: :success
  end
end

My create/2 function in DeviceController is just 3 lines long. In those 3 lines, I:

  1. Get the current user.
  2. Insert a new device (with changeset validations) to the database.
  3. Passes :success to my the action_fallback plug.

The “happy path” response from my fallback looks like this:

{
  status: "success",
  data: {
    status: "success"
  }
}

If any of the guards in the with statement fails, guess what: my fallback controller’s got my back 😬.

For instance, say the Notifications.add_device/2 function fails to insert the new device into the database because it didn’t meet one of the changeset validations, such as the unique_constraint for device_token.

Notifications.add_device would then return {:error, changeset}, which would be forwarded to my fallback controller, and the JSON error would look like this:

{
  status: "error",
  error: "device_token_taken"
}

And I got that for free. 👆

Yay contexts! Yay action_fallback! 🎉

Publicado en Elixir | Etiquetado , , , , , , | Comentarios desactivados en action_fallback and contexts in Phoenix 1.3 made my controllers tiny!

Phoenix 1.3 is pure love for API development

Phoenix 1.3-rc.0 was just released, and while it’s still not a final release, it already feels solid as h*ck.

I’ve been doing a personal project on the side for a while now, and it has a web API component, which I wrote in Phoenix 1.2. That was my first time using Phoenix on a serious project that I intend to ship.

With the release of Phoenix 1.3, and after I watched Chris McCord’s talk about the changes it brings to the table, I decided to rewrite my API from scratch with this new version.

And oh boy, is it great.

Here are my first-impressions.

New Folder Structure

It has been said a lot of times that Phoenix is "Rails for Elixir." With 1.3, the core team sure wants that notion to be as dead as the web/ folder and the model concept.

I’m not going to explain everything that Chris talked about on his keynote at Lonestar Elixir (you should seriously go watch it), but I think it’s worth saying that the change in the folder structure of a Phoenix project has bigger implications that just files moving from one place to another.

Now, there are concrete boundaries between your actual Elixir application, and the interface that Phoenix provides for it to communicate with the web.

Responsibility separation by design. I love this.

Thinking Ahead

Long gone are the mix phoenix.* commands. Say hi to mix phx.*. Less things to type. Love it.

Also, the generators will make you think in advance of what you want to do before you actually do it.

For instance, in Phoenix 1.2 you could do something like this:

$ mix phoenix.gen.json User users email:string

which then would generate the right migrations, and the model:

defmodule MyApp.User do
    use MyApp.Web, :model
    schema "users" do
        field :email, :string
    end
end

along with a (web) controller that would handle the CRUD tasks, with the logic for validation within each action. Over time, using this structure, is easy to end up with controller actions over hundred lines long.

Now, with Phoenix 1.3, you need to specify a context for each resource you create (don’t call them "models" anymore 🙄):

$ mix phx.gen.json Accounts User users email:string

In the command above, Accounts is the context module that’s going to be also generated, and is through this context that we’ll interact with our User module:

Accounts.get_user(2) #=> %User{email: oscar@swanros.com}
Accounts.create_user(params) #=> {:ok, new_user}

This is powerful, because now you can define clear boundaries for your application domains.

Say you have the concept of "contacts" in your application. Each user (User) has contacts (also User instances). This can get messy (as I’ve written before). But using this new notion of "contexts", we can have a ContactRelationship context and handle everything in a clean way:

user
|> ContactRelationships.relationship_with(another_user)
|> ContactRelationships.accept
user
|> ContactRelationships.get_contacts
user
|> ContactRelationships.request_contact(another_user)

This scenario would make our folder structure look something like this:

|- lib/
|---- my_app/
|-------- accounts/
|-------- contact_relationships/
|-------- web/

Defining concrete boundaries between responsibilities within your app makes you come up with better code over time.

action_fallback

So far, the additions to the framework are really nice and welcomed. However, I think my favorite one is the new action_fallback plug.

On Phoenix 1.2 and earlier, every controller needed to return a valid conn for every request. Otherwise, an exception would rise.

This is still true, but with the new action_fallback plug we can delegate that task to another controller whose sole purpose is to handle the cases when other controllers couldn’t provide a "successful" response to the request.

Let me explain… before, you wrote code similar to this in every controller action:

def create(conn, %{"user" => user_params}) do
  user = Repo.get_by(User, phone_number: user_params["phone_number"])
  cond do
    user && checkpw(user_params["password"], user.password_hash) ->
      case create_session(user) do
        {:ok, session} ->
          conn
            |> put_status(:created)
            |> render("show.json", %{session: session, user: user})
        _ ->
          conn
            |> put_status(:unauthorized)
            |> render("error.json")
      end
    true ->
      dummy_checkpw()
      conn
        |> put_status(:unauthorized)
        |> render("error.json")
  end
end

Is not that bad, truly. But this is still better with Phoenix 1.3:

action_fallback MyApp.Web.FallbackController
def create(conn, %{"user" => user_params}) do
  user = Accounts.get_user_by_phone(user_params["phone_number"])
  cond do
    user && checkpw(user_params["password"], user.hashed_password) ->
      with {:ok, %Session{} = session} <- Sessions.create_session(user) do
        conn
        |> put_status(:created)
        |> render("auth_success.json", user: user, session: session)
      end
    true ->
      {:error, :wrong_credentials}
  end
end

With Phoenix 1.3 our controller actions can be simplified to just care about the "happy path", and leave the rest to the fallback controller that we define using the action_fallback plug.

In this case, when the cond evaluates to true we return from the action {:error, :wrong_credentials}. Since this is not a valid connection, our fallback controller comes forward:

defmodule MyApp.Web.FallbackController do
  use MyApp.Web, :controller
  def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
    conn
    |> put_status(:unprocessable_entity)
    |> render(MyApp.Web.ChangesetView, "error.json", changeset: changeset)
  end
  def call(conn, {:error, :unauthorized}) do
    conn
    |> put_status(:unauthorized)
    |> render(MyApp.Web.ErrorView, "auth_required.json")
  end
  def call(conn, {:error, :wrong_credentials}) do
    conn
    |> put_status(:unprocessable_entity)
    |> render(MyApp.Web.ErrorView, "wrong_credentials.json")
  end
end

By pattern matching, the fallback controller knows exactly what to send back as a response to the request.

This also means that we can have a centralized list of error codes that our API can expose to our clients, on our ErrorView module.

No more error definitions scattered around our codebase. ❤️

However, why stop with errors? If your API has "default" responses for actions, you can rely on the fallback controller too to display them:

def delete(conn, _params) do
  with {:ok, session} <- Authentication.get_session(conn),
       {:ok, deleted} <- Sessions.delete_session(session) do
    :success
  else _ ->
    {:error, :unauthorized}
  end
end

:success will be passed to the appropriate action on your fallback controller, and there goes your default response. 🎉

Final thoughts

Phoenix 1.3 is a great update.

Please keep in mind that the defaults 1.3 offers could be done with 1.2, too. But the fact that this is now the default, really speaks of how much attention to detail the Phoenix team is putting in.

They don’t want us to be using "The Phoenix Way™", but rather provide sensible defaults that enable us to create our own solutions in the best possible way.

Some points:

  • The new fallback_controller plug is awesome.
  • The fact that you need to think in terms of boundaries for your app, I think will eventually lead to better, more maintainable codebases.
  • Smarter code generation is 👌.
  • For APIs, the centralized list of available errors is really welcomed.

I love how productive I am with Phoenix when building APIs, and I’m really looking forward to how this already-great framework will evolve past 1.X.


edit: I also wrote about how action_fallback and contexts in Phoenix 1.3 made my controllers tiny.

Publicado en Elixir | Etiquetado , , , | Comentarios desactivados en Phoenix 1.3 is pure love for API development

The Algorithms Of Discrimination

Go read this. If you prefer you can listen too.

I posted a blog post a few weeks ago about an experience I had interviewing at a rather large and well known company that had a white board interview where I was being asked to implement a linked list in Java.
I expressed that I felt this was a useless task.
I got a lot of pushback from people on Twitter about this. A lot of older programmers chastised me that you can’t be a great programmer if you don’t understand data structures and algorithms. I tried to make the point that if you are an iOS developer that most of your job is to have knowledge of the iOS frameworks and that the language is secondary. Someone who had never owned a Mac or opened Xcode could theoretically get a job as an iOS developer.

Janie sparkled a conversation about this. I’m on her side (not that anyone’s taking sides, anyway).
I remember when I interviewed at TopTal for an iOS Developer position, and they had me writing algorithms in C. A company that posts about what to ask developers in iOS interviews.

Publicado en Work | Comentarios desactivados en The Algorithms Of Discrimination

Facebook announces Yarn, a new package manager for JavaScript

From Facebook’s announcement:

In the JavaScript community, engineers share hundreds of thousands of pieces of code so we can avoid rewriting basic components, libraries, or frameworks of our own. Each piece of code may in turn depend on other pieces of code, and these dependencies are managed by package managers.

Exactly. That’s the problem.

From NPM’s blog, acknowledging the release of Yarn:

Today, Facebook announced that they have open sourced Yarn, a backwards-compatible client for the npm registry. This joins a list of other third-party registry clients that include ied, pnpm, npm-install and npmd. (Apologies if we missed any.)

Trying to solve the dependency management problem by adding another layer of dependencies to the equation is as stupid as it sounds.

From an outsider that doesn’t do web development for a living, this is crazy. I don’t know how people cope with the web ecosystem. Specially the front-end part.

If you’re a web developer, my condolences.

P.S.: this article really resonated with me, as I’ve been spending some time doing web development for the past few weeks.

Publicado en Software | Comentarios desactivados en Facebook announces Yarn, a new package manager for JavaScript