Skip to content

Filtering out no-ops returned by event handlers #23

@rwillians

Description

@rwillians

Related to #16

In order to keep the code simpler and more readable, in some cases I personally prefer creating no-ops (or is it null-ops? -- in a reducer, it means producing an operation like an update that will do no side effect to the db). This avoids conditional creation of ops and overkill extractions with conditions and/or pattern matching.

The most common case, for me, is having a pattern match that might return null instead of an op. But there has been one case case where I used and updated that changed no fields (issue #16) -- this one was in Hatch, don't remember if there were any in TurtleOS.

Of course we could just wrap the returned list of ops with a function that filters no-ops (like reject null values or even filtering out Derive operations' structs based on some logic), therefore this isn't a blocker. But, I think this is something we could do on Derive's side the keep userland-code cleaner.

The sudo-code for what I'm suggesting would look like:

def foo (a, b, c) do
  apply(reducer_mod, :handle_event, [event])
  |> List.wrap() # in case a single op (not a list) is returned
  |> List.flatten() # in case there are nested lists, useful when you extract
                    # the conditional logic for generating subsets of events.
  |> Enum.reject(&no_op?/1)
  |> invoke_commit(reducer_mod)
end

defp no_op?(null), do: true
defp no_op?(%Update{fields: fields}) when map_size(fields) == 0, do: true
# ...
def no_op?(_), do: false

A use case example:

def handle_event(%Foo{} = event) do
  [
    maybe_delete_bar(event.baz)
  ]
end

defp maybe_delete_bar([]), do: nil
defp maybe_delete_bar([_ | _] = bar) do
  [bla: bar]
  |> Query.bars()
  |> delete()
end

Another one:

def handle_event(%Foo{} = event) do
  [
    maybe_update_bar(event)
  ]
end

defp maybe_update_bar(event) do
  transaction(fn repo ->
    current_bar = load_bar(repo, event.bar_id)
    new_bar = Map.merge(current_bar, %{a: event.bar_a, b: event.bar_b})
    changed_fields = diff(current_bar, new_bar)
    
    update({Bar, event.bar_id}, changed_fields) # there might be no fields changed and we actually have 
                                                # a use case like that. In fact, the query fails at
                                                # runtime if no fields were changed (that's issue 16).
  end)
end

It also makes it easier to do this:

def handle_event(%Foo{} = event) do
  [
    if (not is_nil(event.bar)) do
      insert(%Bar{id: event.bar_id, a: event.a, b: event.b})
    end
    # the `if` block will return null when the condition evaluates to false.
  ]
end

Edit: updated sudo-code for filtering out no-ops, now allows for this use case:

def handle_event(%Foo{} = event) do
  [
    maybe_delete_x(event.foo),
    maybe_create_y(event.bar),
    maybe_create_z(event.baz)
    # Each `maybe_*` function returns a list of 0 or more events.
    # That's why that `List.flatten/1` I added is important.
  ]
end

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions