palatable / lambda

Functional patterns for Java
https://palatable.github.io/lambda/
MIT License
860 stars 85 forks source link

New utility methods on MonadReader and simple implementation of Reader. #118

Closed danijelz closed 2 years ago

danijelz commented 2 years ago

It is sometimes usefull to inspect the 'environment' while mapping the current value. Something similar is implemented by State#mapState(Fn1) but the implementation is somehow cumbersome when only inspection without modification of the 'environment' is needed.

7h3kk1d commented 2 years ago

Hi @danijelz can you give some context as to what use cases you're trying to solve for? Some of these can already be accomplished with flatMap for example in the ReaderT case.

danijelz commented 2 years ago

Hi @danijelz can you give some context as to what use cases you're trying to solve for? Some of these can already be accomplished with flatMap for example in the ReaderT case.

The problem with current solutions is that they don't provide insight into the environment passed to the MonadReader without unnecessary code cluttering. For instance:

public class UseCase {
  public ReaderT<Environment, Maybe<?>, String> flatMappingOverEnvironmentAndCurrentValue() {
    return readerT(this::someEnvironmentDependentCalculation)
            // fmap does not allow insight into environment so we have to clutter the code
            .flatMap(str -> readerT(env -> maybe(otherEnvironmentDependentCalculation(env, str))));
  }

  public ReaderT<Environment, Maybe<?>, String> rmappingOverEnvironmentAndCurrentValue() {
    return readerT(this::someEnvironmentDependentCalculation)
            .rmap(this::otherEnvironmentDependentCalculation);
  }

  private Maybe<String> someEnvironmentDependentCalculation(Environment env) {
    // magic
  }

  private String otherEnvironmentDependentCalculation(Environment env, String str) {
    // some more magic
  }

  public static class Environment {
    // some state
  }
}

I must admit that I have not found any useful case for the rflatMap so so it would be better off not including it. I also noticed that after calling rmap it is necessary to coerce the result. This could be avoided by overrding the rmap method on implementations.

As for the Reader monad, it's a bit more optimal and cleaner version of ReaderT <R, Identity <?>, A>, which is interchangeable with State <R, A> but slightly more explicit as it communicates that the environment is constant since it doesn't implement MonadWriter.

I am proposing to clear the code by removing rflatMap and overwriting rmap on implementations to avoid calling coerce. If such a solution suits you and you are willing to merge it, I will be happy to do it.

7h3kk1d commented 2 years ago

Thanks for the response. I think the intent when using readerT is to define the ongoing computations that need access to the environment in terms of ReaderT as well so we don't have to add more interface methods. That's equivalent to your example but might be clearer if you name the second ReaderT like so:

public class UseCase {
  public ReaderT<Environment, Maybe<?>, String> flatMappingOverEnvironmentAndCurrentValue() {
    return someEnvironmentDependentCalculation()
            .flatMap(this::otherEnvironmentDependentCalculation);
  }

  private ReaderT<Environment, Maybe<?>, String> someEnvironmentDependentCalculation() {
    throw new NotImplementedException(); // magic
  }

  private ReaderT<Environment, Maybe<?>, String> otherEnvironmentDependentCalculation(String str) {
    return readerT(env -> {
      throw new NotImplementedException(); // some more magic
    });
  }

  public static class Environment {
    // some state
  }
}

Since ReaderT is Cartesian you can derive rmap at the call site as follows:

    @Override
    public <B> ReaderT<R, M, B> rmap(Fn2<? super R, ? super A, ? extends B> fn) {
        return carry().fmap(into(fn));
    }

In general though I usually just use ReaderT::flatMap and chain together the ReaderTs to get access to the environment. I would like your thoughts in if that seems satisfactory.

Reader seems mostly redundant since we have Fn1 which implements Cartesian and also implements Monad which like you've said keeps the environment constant. Let me know if there's some other aspect of Reader that you want semantically.

On a related note I'm wondering if MonadReader<R, R, MR> ask() would be useful in order to get the environment on it's own? That may make it easier to do similar operations with the environment across all the MonadReader instances

danijelz commented 2 years ago

In general though I usually just use ReaderT::flatMap and chain together the ReaderTs to get access to the environment. I would like your thoughts in if that seems satisfactory.

I still tink this clutters the code. When using existing methods, probably from some library, there is some additional gmnastics that has to be done. The main idea here was to add functionality which would enable us to write code that reads imperatively but solves problems functionaly. Let's say we have some environment that is configured impurely, maybe at the initialization of app or right before running the test. If environment contains some information that is needed couple of flatMaps below, the code will be much less readable:

public class UseCase {
    public ReaderT<Environment, Maybe<?>, String> recipeForRequestHandling() {
        return canComputeSomeValueUsingEnvironment()
                // couple of fmaps and flatMaps here ... 
                .flatMap(str -> wrapSomeUtilityStuff(str));
    }

    public ReaderT<Environment, Maybe<?>, String> recipeForRequestHandlingWithRmap() {
        return canComputeSomeValueUsingEnvironment()
                // couple of fmaps and flatMaps here ... 
                .rmap((env, str) -> someUtilityStuff(env.config, str));
    }

    private ReaderT<Environment, Maybe<?>, String> canComputeSomeValueUsingEnvironment() {
        throw new NotImplementedException(); // magic
    }

    private ReaderT<Environment, Maybe<?>, String> wrapSomeUtilityStuff(String input) {
        return readerT(r -> just(someUtilityStuff(r.config, input)));
    }

   // some magic in super cool library
    public String someUtilityStuff(String config, String input) {
        throw new NotImplementedException(); 
    }

    private static class Environment {
        public String config;
    }
}

Reader seems mostly redundant since we have Fn1 which implements Cartesian and also implements Monad which like you've said keeps the environment constant. Let me know if there's some other aspect of Reader that you want semantically.

I totaly agree with this, Reader is unnecessary.

On a related note I'm wondering if MonadReader<R, R, MR> ask() would be useful in order to get the environment on it's own? That may make it easier to do similar operations with the environment across all the MonadReader instances

I don't see how this would be useful as instance method. Kleisli from Scala Cats has this as static method with same name and fuctionality, similar to State#get which is super usefull.

7h3kk1d commented 2 years ago

I don't see how this would be useful as instance method. Kleisli from Scala Cats has this as static method with same name and fuctionality, similar to State#get which is super usefull.

That makes sense. I don't think there's a way to force that on all the classes that extend MonadReader unfortunately but we could add it as a static method on ReaderT.

The main idea here was to add functionality which would enable us to write code that reads imperatively but solves problems functionaly

I would keep in mind the Monadic composition is what keeps the effects managed. So in the rmap case there's no monadic effect the function that would be passed in would need to be a pure function and shouldn't be used to represent an impure operation similar to flatMap. We don't want to do too much "magic" that makes the effects implicit rather than explicit because then it becomes hared for users to identify the pure operations from the impure. e.g.

  public ReaderT<Environment, IO<?>, String> example() {
      return canComputeSomeValueUsingEnvironment()
              .<String>rmap((env, str)-> writeToDatabase()); // Don't want to do this because it looks pure and isn't lifted into the effect
  }

  private String writeToDatabase() {
      throw new NotImplementedException();
  }

There's probably a case to be made to add something equivalent onto Cartesian that would take a pure function but State/StateT aren't ProFunctors

danijelz commented 2 years ago

it becomes hared for users to identify the pure operations from the impure.

Haven't think of that and I totaly agree with it.

we could add it as a static method on ReaderT

That would be quite useful.

7h3kk1d commented 2 years ago

I'll add the ReaderT#ask static method.

Thanks for reaching out and sorry for the long delay in response. Let me know if there's anything else we can help with or if there are any other pain points.