adomokos / light-service

Series of Actions with an emphasis on simplicity.
MIT License
837 stars 67 forks source link

Add reduce_case #232

Closed kavinphan closed 2 years ago

kavinphan commented 2 years ago

Was looking for a when-else reducer but couldn't find one, so I made this.

reduce_when takes in:

Example

reduce_when(
  :incr_num,
  {
    :one => [TestDoubles::AddsOneAction],
    :two => [TestDoubles::AddsTwoAction],
    :three => [TestDoubles::AddsThreeAction]
  },
  :els => [TestDoubles::FailureAction]
)
adomokos commented 2 years ago

@kphan32, thank you for the PR! Can you please provide a bit of context on where you think this reduce_when would be handy? I'd like to understand the use case a bit better.

kavinphan commented 2 years ago

@adomokos I was working on ingesting data from multiple sources that needed to be marshaled into a common data source. A majority required the same transformation, while others required special treatment. This resulted in the following similar to the original example:

reduce_when(
  :data_source,
  {
    :source_one => [Resources::TransformFromSourceOne],
    :source_two => [Resources::TransformFromSourceTwo],
    :source_three => [Resources::TransformFromSourceThree]
    # [...]
  },
  :els => [Resources::TransformGeneric]
)
adomokos commented 2 years ago

Sweet, I like it, will merge it. I am not sure about reduce_when name. I was expecting the first argument to be a predicate, then a truthy and falsy case, it took me a while to figure out that the first argument is the key, and the other is the dictionary of key and actions, and then the third argument is the case not found.

It seems you are trying to find a "processor" based on the key you provided. Could we find a better name for this? Do you have other options?

adomokos commented 2 years ago

Also, you might want to rebase from main, I bumped the Ruby versions last night.

kavinphan commented 2 years ago

WDYT of having the second parameter be named as is? That way it could be read as reduce_when :key is ___ els ___?

kavinphan commented 2 years ago

I took inspiration mainly from using Kotlin previously, which had a when expression.

adomokos commented 2 years ago

This helps. I like Kotlin's when, it's a decent-enough pattern matching attempt from JetBrains.

adomokos commented 2 years ago

WDYT of having the second parameter be named as is? That way it could be read as reduce_when :key is ___ els ___?

This could work. Can you show me what you mean by a working prototype?

alessandro-fazzi commented 2 years ago

Lovely :) 🙏🏻

Just my 2 cents about what I'd mainly expect from the interface:

reduce_case(
  :key,
  when: {
    one: [TestDoubles::AddsOneAction],
    two: [TestDoubles::AddsTwoAction]
  },
  els: [TestDoubles::FailureAction]
)

because it would really quack like https://ruby-doc.org/core-3.1.1/doc/syntax/control_expressions_rdoc.html#label-case+Expression. Given the ruby expression is called case, I'd opt to name the reducer with the same word.

But I'll be obviuosly grateful for any decision <3

adomokos commented 2 years ago

I do like what @pioneerskies suggested here:

reduce_case(
  :key,
  when: {
    one: [TestDoubles::AddsOneAction],
    two: [TestDoubles::AddsTwoAction]
  },
  els: [TestDoubles::FailureAction]
)

I also like the idea of making the matching more flexible (goes against strongly typed languages, but we are talking Ruby here).

What do you think about making those changes @kphan32?

kavinphan commented 2 years ago

@adomokos Those sound good to me, will do :+1:

kavinphan commented 2 years ago

Though, when is a keyword so it'll end up being wen. Not sure how I feel about that, WDYT?

adomokos commented 2 years ago

Though, when is a keyword so it'll end up being wen. Not sure how I feel about that, WDYT?

Yeah, wen is awkward.

What do you think about this?

reduce_case(
  :key,
  :cases => {
     one: [TestDoubles::AddsOneAction],
     two: [TestDoubles::AddsTwoAction]
  },
  :els => [TestDoubles::FailureAction]
)

Let me know if @pioneerskies (or anybody else) has better ideas.

alessandro-fazzi commented 2 years ago

On the fly the only alternative ringing in my brain is a - possibly awkward - whens.

alessandro-fazzi commented 2 years ago

This is what I'd do in order to achive the most expressive interface bypassing the problem of having keywords

module LightService
  module Organizer
    class ReduceWhen
      extend ScopedReducable

      class Arguments
        attr_reader :value, :when, :else

        def initialize(**args)
          validate_arguments(**args)
          @value = args[:value]
          @when = args[:when]
          @else = args[:else]
        end

        private

        def validate_arguments(**args)
          raise(
            ArgumentError,
            "Expected keyword arguments: [:value, :when, :else]. Given: #{args.keys}"
          ) unless args.keys.intersection(mandatory_arguments).count == mandatory_arguments.count
        end

        def mandatory_arguments
          %i[value when else]
        end
      end

      def self.run(organizer, **args)
        arguments = Arguments.new(**args)

        lambda do |ctx|
          return ctx if ctx.stop_processing?

          matched_case = arguments.when.keys.find { |k| k.eql?(ctx[arguments.value]) }
          steps = arguments.when[matched_case] || arguments.else

          ctx = scoped_reduce(organizer, ctx, steps)

          ctx
        end
      end
    end
  end
end

This way the example of use would be

reduce_case(
  value: :foo,
  when: {
    'Foo' => [MyAction],
    bar: [MyOtherAction]
  },
  else: [AnotherAction]
)

Don't know if it fits, if it's worth the complexity, etc.

Hope it someway helps :)

kavinphan commented 2 years ago

@pioneerskies definitely a fan of this, and I don't mind the complexity.

adomokos commented 2 years ago

This is looking good, I'll merge it. Can you, @kphan32 add README docs to it?

kavinphan commented 2 years ago

@adomokos Updated :+1:

adomokos commented 2 years ago

Thank you @kphan32 for this great addition to LS! 💯