Language-ext: Try<> Delegate -- Unexpected Repeated Calls

Created on 16 Jul 2018  路  2Comments  路  Source: louthy/language-ext

Background: I have a factory method that constructs a complex object or fails with an exception. I want to call this method repeatedly for a sequence of inputs, and capture either the object or the exception. My first attempt used curry() and Either<> and worked well. Then I realised Try<> offered more succinct code.

Here's a stripped down demo of what I tried.

```c#
class ObjFactory
{
internal int NCalls = 0;

internal object Create(int i)
{
    // record invocations
    ++NCalls;

    // fail intermittently
    return 0 == i % 2 ? new object() : throw new Exception("Odd!");
}

}

class Program
{
static void Main(string[] args)
{
var objFactory = new ObjFactory();
Try Create(int i) => () => objFactory.Create(i);
var attempts = Enumerable.Range(0, 3).Map(Create);
var (failures, successes) = partition(attempts);
var objs = successes.ToArr();
var exceptions = failures.ToArr();
Console.WriteLine($"NCalls = {objFactory.NCalls}");
Console.ReadKey();
}
}

The output is:

```c#
NCalls = 9

This is falling foul of sequences being re-enumerated and somehow mucking up the memoization. If Create and attempts are replaced with this:

c# var Create = fun<int, Try<object>>(i => Try(() => objFactory.Create(i))); var attempts = Enumerable.Range(0, 3).Map(Create).ToArr();

then NCalls is the desired 3.

Clearly, for an expensive operation, I don't want the extra (unnecessary?) calls.

Should Try<> come with a health warning? Duffers like me need to be saved from themselves.

examples / documentation

Most helpful comment

Thanks. Seq() was the missing part I needed here. I read the version 2 release notes twice but still failed to appreciate the import of this bit:

The primary benefits are:
...
* If you construct a Seq with an IEnumerable then it maintains its laziness, but
  also guarantees that each item in the original IEnumerable is only ever enumerated 
  once.
...

I guess a third reading is in order or else risk shooting myself in the other foot.

All 2 comments

and somehow mucking up the memoization

There's no memoization here because the Try<object> is generated every time the enumerable is evaluated.

The partition function looks like this:

```c#
public static (IEnumerable Lefts, IEnumerable Rights) partition(IEnumerable ma)
where CHOICE : struct, Choice =>
(lefts(ma), rights(ma));

Which as you can see will run the enumerable twice.  I could probably update it to:
```c#
public static (IEnumerable<A> Lefts, IEnumerable<B> Rights) partition<CHOICE, CH, A, B>(IEnumerable<CH> ma)
    where CHOICE : struct, Choice<CH, A, B>
{
   var sa = Seq(ma);
   return (lefts<CHOICE, CH, A, B>(sa), rights<CHOICE, CH, A, B>(sa));
}

But that could give unexpected results for large streams (memory usage).

Your second example doesn't call partition and so it gives the expected number of attempts.

Really, this is expected behviour from Enumerable, if your code was pure then it would be correct. And so, I'm slightly unwilling to put in any fix here.

There is a Seq version of partition, so if you did this (below) I would expect a consistent result:
```c#
var objFactory = new ObjFactory();
Try Create(int i) => () => objFactory.Create(i);

var attempts = Enumerable.Range(0, 3).Map(Create);
var (failures, successes) = partition(Seq(attempts));

var objs = successes.ToArr();
var exceptions = failures.ToArr();

Console.WriteLine($"NCalls = {objFactory.NCalls}");

```

Thanks. Seq() was the missing part I needed here. I read the version 2 release notes twice but still failed to appreciate the import of this bit:

The primary benefits are:
...
* If you construct a Seq with an IEnumerable then it maintains its laziness, but
  also guarantees that each item in the original IEnumerable is only ever enumerated 
  once.
...

I guess a third reading is in order or else risk shooting myself in the other foot.

Was this page helpful?
0 / 5 - 0 ratings