0

Suppose I have a method that takes the Id of a card issuer (think of loyalty cards for shops), and I want to get the status of the customer who made the most recent transaction. Yes this is a silly scenario, but it's easer than trying to describe the real one, and it's good enough to show the problem. The code shown here constitutes a working example, except for the last bit, which is where my question lies.

Here are two very simple types needed to support the workflow...

record CardIssuer(string Id, string Key);

enum CustomerStatus {
  Active,
  Inactive
}

I have some static methods that return an Either<string, T>, where T is specific to the actual method. The versions shown are stupidly simple for clarity...

static Either<string, CardIssuer> GetCardIssuer(string id) =>
 id == "1" ? new CardIssuer("1", "abc") : new CardIssuer(id, "def");

static Either<string, int> GetLatestTransactionId(CardIssuer issuer) =>
  issuer.Id == "1" ? 42 : 666;

static Either<string, CustomerStatus> GetCustomerStatus(int transactionId) =>
 transactionId == 42 ? CustomerStatus.Active : CustomerStatus.Inactive;

I use these in a method like this...

static async Task<String> Process(string cardIssuerId) =>
  await (
    from issuer in GetCardIssuer(cardIssuerId).ToAsync()
    from id in GetLatestTransactionId(issuer).ToAsync()
    from status in GetCustomerStatus(id).ToAsync()
    select status.ToString()
  )
  .Match(status => {
    return status;
  },
  error => {
    return $"Error: {error}";
  });

All of the above works fine, and is all you need to reproduce the simple scenario.

Suppose I now have a requirement that the result is encrypted, using the issuer-specific encryption key held in the Key property of CardIssuer.

Modifying the Right lambda is easy, I just change the last few lines of the query to look like this...

static async Task<String> Process(string cardIssuerId) =>
  await (
    from issuer in GetCardIssuer(cardIssuerId).ToAsync()
    from id in GetLatestTransactionId(issuer).ToAsync()
    from status in GetCustomerStatus(id).ToAsync()
    // Next line changed, along with the first lambda to Match
    select (issuer, status.ToString())
  )
  .Match((CardIssuer issuer, string status) => {
    return Encrypt(status, issuer.Key);
  },
  error => {
    // What do I do here?
  });

This assumes an Encrypt method...

static string Encrypt(string s, string key) =>
  // Do some clever encryption with the key
  "";

However, I don't know how to get the encryption key into the Left lambda.

I know I can do this by modifying each of the Either-returning methods to return a tuple containing the type they currently return, along with a string for the key, however, this means that a significant number of currently reusable (and reused) methods would now have to take in and pass out data that has nothing to do with them.

Is there a better way of passing data to the Left lambda?

Avrohom Yisroel
  • 8,555
  • 8
  • 50
  • 106
  • Can you not perform the match inside of the query expression? If not, please post a [minimal, reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). – Mark Seemann Jan 09 '23 at 17:26
  • @MarkSeemann Not sure what you mean, but I edited my question to include an MRE, so please take a look and see what you think. Thanks – Avrohom Yisroel Jan 09 '23 at 18:05
  • In the left case, you may not have a `CardIssuer`. You've declared `GetCardIssuer` so that it doesn't contain a `CardIssuer` in the left case... (The *implementation* only returns right cases, but the type system doesn't know that.) The left case carries no `CardIssuer`, so you *can't* get that value in that case. – Mark Seemann Jan 09 '23 at 19:01
  • AFAICT none of the functions in the MRE return left values. Is this a result of the simplification, or do the real functions look like that as well? If the latter, then why return `Either` values at all? – Mark Seemann Jan 09 '23 at 19:03
  • @MarkSeemann No, that's part of the simplification. I didn't want to complicate the code with unnecessary details. As you can see, the `Either`-returning methods shown above all return hard-coded values, whereas the real methods return values based on a database. – Avrohom Yisroel Jan 09 '23 at 20:08
  • Somewhat tongue in cheek, but consider if you wouldn't be happier programming in F#. Note that that is still .NET and you can combine assemblies compiled in either language. C# is only mildly functional, and trying to apply functional programming ideas directly to sequential code that would normally be written imperatively (local variables, `try .. catch`) rarely leads to more maintainable code. – Jeroen Mostert Jan 09 '23 at 20:14
  • @JeroenMostert I have looked at F#, and would like to use it more, but this is a fairly large mature code base, and the thought of rewriting bits in F# wouldn't go down too well with the people who pay my wage! I have found writing functional code in C# to be quite good though, as I can move bits of code over in stages, as I learn FP more. – Avrohom Yisroel Jan 09 '23 at 20:16
  • Where does the `Either` type and `Match` method come from? – NetMage Jan 09 '23 at 21:01
  • @NetMage `Either` is one of the monads in the rather excellent [Language-Ext](https://github.com/louthy/language-ext/) Nuget package, and `Match` is a standard method that all the monads implement. – Avrohom Yisroel Jan 09 '23 at 21:06
  • And now I want to ask if there is a reason the left lambda is on the right and the right lambda is on the left in `Match` :) – NetMage Jan 09 '23 at 21:10
  • @NetMage I have wondered that myself many times! My guess is that this is the way it's done in Haskell, as I think Language-Ext is largely modelled on the way Haskell does things, but the only way to get a definitive answer would be to ask the package author. – Avrohom Yisroel Jan 09 '23 at 21:12
  • 1
    @NetMage You spurred me on to asking, and it seems it was [just a bad decision](https://github.com/louthy/language-ext/discussions/1167). – Avrohom Yisroel Jan 09 '23 at 22:51

0 Answers0