Replacing Exceptions with // Maybe / Option

I ran into this deadlock while trying to replace exceptions with a monad in C #. This makes me think maybe this is not only a language specific issue but also a missing feature related to technique.

Let me try to explain it more globally:

Given:

  • I have a third party function (a function imported into my code and I have no access) that receives a lazy list (C # IEnumerable, f # Seq ...) and consumes it

I want to:

  • To apply a function (LINQ select, map ...) in the argument of the lazy list method and take each element of the list (lazily) and perform computations that might fail (throwing an exception or returning an error / Either).

  • A list that will only be consumed "inside" the third party function, I don't want to iterate over each item more than once.

With Exceptions / Side Effects, this can be easily achieved by throwing an exception from the select functions, displaying the map, if an error is found it will stop execution "inside" the third party function. Then I could handle the exception outside of it (without the third party "knowing" about my error handling), leaving it to myself to handle the error.

Whereas with Libo it doesn't seem to be possible to get the same behavior without changing the third party function. Intuitively I tried to convert a list from the Eithers list to any of the lists, but this can only be done by using a list with functions. For example, aggregate or decrease (does the Haskell sequence function act the same?).

This all leads me to the question: Maybes / Eithers or Error as a return type, skipping this behavior? Is there any other way to do the same with them?

+3


source to share


2 answers


As far as I can tell, Haskell Either

is isomorphic to C # / Java-style exceptions, which means that translation from Either

non-constrained code to exception-based code and vice versa. However, I am not entirely sure about this, as there may be some edge cases that I am not aware of.

On the other hand, I'm pretty sure I'm isomorphic , so in the next one, I'll stick and ignore . Either () a

Maybe a

Either

Maybe

What you can do with exceptions in C #, you can also do with Either

. By default, there is no error handling 1 in C # :

public IEnumerable<TResult> NoCatch<TResult, T>(
    IEnumerable<T> source, Func<T, TResult> selector)
{
    return source.Select(selector);
}

      

This will jump to source

until an exception occurs. If no exception is thrown, it will return IEnumerable<TResult>

, but if an exception is thrown selector

, the whole method will throw an exception as well. However, if the items source

were processed before the exception was thrown and there were side effects, this work remains.

You can do the same in Haskell using sequence

:

noCatch :: (Traversable t, Monad m) => (a -> m b) -> t a -> m (t b)
noCatch f = sequence . fmap f

      

If f

is a function that returns Either

, then it behaves the same:

*Answer> noCatch (\i -> if i < 10 then Right i else Left i) [1, 3, 5, 2]
Right [1,3,5,2]
*Answer> noCatch (\i -> if i < 10 then Right i else Left i) [1, 3, 5, 11, 2, 12]
Left 11

      

As you can see, if a value is Left

ever returned, you get a response Right

back with all items displayed. If only one case is returned Left

, you will receive this and no further processing will be done.

You can also assume that you have a C # method that suppresses individual exceptions:

public IEnumerable<TResult> Suppress<TResult, T>(
    IEnumerable<T> source, Func<T, TResult> selector)
{
    foreach (var x in source)
        try { yield selector(x) } catch {}
}

      

In Haskell, you can do this with Either

:

filterRight :: (a -> Either e b) -> [a] -> [b]
filterRight f = rights . fmap f

      



This returns all values Right

and ignores values Left

:

*Answer> filterRight (\i -> if i < 10 then Right i else Left i) [1, 3, 5, 11, 2, 12]
[1,3,5,2]

      

You can also write a method that processes the input until the first exception is thrown (if any):

public IEnumerable<TResult> ProcessUntilException<TResult, T>(
    IEnumerable<T> source, Func<T, TResult> selector)
{
    var exceptionHappened = false;
    foreach (var x in source)
    {
        if (!exceptionHappened)
            try { yield selector(x) } catch { exceptionHappened = true }
    }
}

      

Again, you can achieve the same effect with Haskell:

takeWhileRight :: (a -> Either e b) -> [a] -> [Either e b]
takeWhileRight f = takeWhile isRight . fmap f

      

Examples:

*Answer> takeWhileRight (\i -> if i < 10 then Right i else Left i) [1, 3, 5, 11, 2, 12]
[Right 1,Right 3,Right 5]
*Answer> takeWhileRight (\i -> if i < 10 then Right i else Left i) [1, 3, 5, 2]
[Right 1,Right 3,Right 5,Right 2]

      

As you can see, however, both the C # and Haskell examples should be familiar with error handling style. While you can translate between the two styles, you cannot use it with a method / function that expects the other.

If you have a third party C # method that expects exception handling to be as it is done, you cannot pass a sequence of values ​​to it Either

and hope that it can handle it. You will need to change the method.

The converse is not entirely true, because exception handling is built into C # (and, in fact, Haskell); you cannot opt ​​out of handling exceptions in such languages. Imagine, however, a language that doesn't have exception handling built in (perhaps PureScript?) And that would be true too.


1 C # code cannot compile.

+9


source


I don't have a compiler, but you can check my ext project language . It is a functional base class library for C #.

For your needs, it has:

  • Seq<A>

    which is a downside, like a lazy enum that will only evaluate once
  • Try<A>

    which is a delegate-based monad that allows you to catch exceptions from third-party code.
  • Other common monads error handling: Option<A>

    , Either<L, R>

    etc.
  • Bonus versions of these monads: OptionAsync<A>

    , TryOption<A>

    , TryAsync<A>

    ,TryOptionAsync<A>

  • The ability to easily convert between these types: ToOption()

    , ToEither()

    etc.

To apply a function (LINQ select, map ...) in the lazy list argument of the method and will take each element of the list (lazily) and perform computations that may fail (throwing an exception or returning an Error / Lither), a List that will only be consumed " inside the "third party function, I don't want to iterate over each element more than once."

It's a little unclear what the purpose is. In ext-language, you can do this:

using LanguageExt;
using static LanguageExt.Prelude;

// Dummy lazy enumerable
IEnumerable<int> Values()
{
    for(int i = 0; i < 100; i++)
    {
        yield return UnreliableExternalFunc(i);
    }
}

// Convert to a lazy sequence
Seq<int> seq = Seq(Values());

// Invoke external function that takes an IEnumerable
ExternalFunction(seq);

// Calling it again won't evaluate it twice
ExternalFunction(seq);

      

But if the function Values()

threw an exception, then it would stop assigning it and return. So you should ideally do this:

// Dummy lazy enumerable
IEnumerable<Try<int>> Values()
{
    for(int i = 0; i < 100; i++)
    {
        yield return Try(() => UnreliableExternalFunc(i));
    }
}

      

Try

is a constructor function for a monad Try

. So your result will be a sequence of Try

thunks. If you don't care about the exception, you can convert it toOption

// Dummy lazy enumerable
IEnumerable<Option<int>> Values()
{
    for(int i = 0; i < 100; i++)
    {
        yield return Try(() => UnreliableExternalFunc(i)).ToOption();
    }
}

      

You can access all successes through:

var validValues = Values().Somes();

      

Or you can use instead Either

:

// Dummy lazy enumerable
IEnumerable<Either<Exception, A>> Values()
{
    for(int i = 0; i < 100; i++)
    {
        yield return Try(() => UnreliableExternalFunc(i)).ToEither();
    }
}

      



Then you can get reliable results:

var seq = Seq(Values());

var validValues = seq.Rights();

      

And errors:

var errors = seq.Lefts();

      

I converted it to Seq

so it doesn't evaluate twice.

Either way, if you want to catch an exception that occurs during lazy evaluation of an enum, then you will need to wrap each value. If an exception might be thrown from using a lazy value, but inside a function, then your only hope is to surround it Try

:

// Convert to a lazy sequence
Seq<int> seq = Seq(Values());  // Values is back to returning IEnumerable<int>

// Invoke external function that takes an IEnumerable
var res = Try(() => ExternalFunction(seq)).IfFail(Seq<int>.Empty);

// Calling it again won't evaluate it twice
ExternalFunction(seq);

      

Intuitively I tried to convert a list from the Eithers list to any of the list, but this can only be done by using a list with functions. For example, aggregate or decrease (does the Haskell sequence function act the same?).

You can do it in ext language like so:

IEnumerable<Either<L, R>> listOfEithers = ...;

Either<L, IEnumerable<R>> eitherList = listOfEithers.Sequence();

      

Traverse

also supported:

Either<L, IEnumerable<R>> eitherList = listOfEithers.Traverse(x => map(x));

      

All combinations of monads support Sequence()

and Traverse

; so you can do it with Seq<Either<L, R>>

to get Either<L, Seq<R>>

, which would ensure that the lazy sequence is not called multiple times. Either a Seq<Try<A>>

to get Try<Seq<A>>

, or any of the asynchronization options for parallel sequencing and traversal.

I'm not sure if this covers something you are asking about, the question is a bit broad. A more specific example would be helpful.

+1


source







All Articles