使用/可能/选项替换例外

时间:2017-06-21 02:36:24

标签: c# haskell exception functional-programming either

我尝试用c#中的monad替换异常时遇到了这个dead end。 这让我想到可能不仅是语言特定的问题和更多与技术相关的缺失特征。

让我试着在全球范围内重新解释一下:

假设:

  • 我有第三方功能(导入我的代码并且我无法访问的功能),它接收一个惰性列表(c#IEnumerable,f#Seq ...)并使用它

我想要:

  • 在方法的lazy list参数上应用一个函数(LINQ select,map ...)并将获取列表中的每个元素(懒惰)并执行可能失败的计算(抛出一个异常或返回错误/要么)。

  • 仅供消费的清单"内部"第三方功能,我不想再多次迭代每个元素。

使用异常/副作用这可以通过从select,map函数抛出异常轻松实现,如果发现错误,这将停止执行" inside"第三方功能。然后我可以在它之外处理异常(没有第三方是"意识到"我的错误处理),将错误处理的责任交给我。

虽然在不改变第三方功能的情况下似乎无法获得相同的行为。直觉上我试图将列表从Eithers列表转换为列表中的任何一个,但这只能通过使用函数列表来完成。喜欢,聚合或减少(Haskell的序列函数是否相同?)。

所有这些导致我的问题是Maybes / Eithers或Error作为返回类型,错过了这种行为?还有另一种方法可以与他们达成同样的目的吗?

2 个答案:

答案 0 :(得分:9)

据我所知,Haskell Either与C#/ Java风格的异常同构,这意味着从基于Either的代码到基于异常的代码的转换,反之亦然。不过,我对此并不十分肯定,因为可能会有一些我不知道的边缘情况。

另一方面,我确定的是Either () a is isomorphic to Maybe a,所以在下文中,我将坚持Either并忽略{{1 }}

您可以使用C#中的例外做什么,也可以使用Maybe。 C#中的默认值是不执行错误处理 1

Either

这将迭代public IEnumerable<TResult> NoCatch<TResult, T>( IEnumerable<T> source, Func<T, TResult> selector) { return source.Select(selector); } ,直到发生异常。如果没有抛出异常,它将返回source,但如果IEnumerable<TResult>抛出异常,则整个方法也会抛出异常。但是,如果在抛出异常之前处理了selector的元素,并且存在副作用,那么这项工作仍然有效。

您可以使用source在Haskell中执行相同操作:

sequence

如果noCatch :: (Traversable t, Monad m) => (a -> m b) -> t a -> m (t b) noCatch f = sequence . fmap f 是一个返回f的函数,那么它的行为方式相同:

Either

如您所见,如果没有返回*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 值,则会返回Left个案例,其中包含所有映射元素。如果只返回一个 Right个案,那么就可以了,并且没有进一步处理。

您还可以想象您有一个C#方法可以抑制个别异常:

Left

在Haskell中,您可以使用public IEnumerable<TResult> Suppress<TResult, T>( IEnumerable<T> source, Func<T, TResult> selector) { foreach (var x in source) try { yield selector(x) } catch {} } 执行此操作:

Either

这将返回所有filterRight :: (a -> Either e b) -> [a] -> [b] filterRight f = rights . fmap f 值,并忽略Right值:

Left

您还可以编写一个处理输入的方法,直到抛出第一个异常(如果有):

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

同样,您可以使用Haskell实现相同的效果:

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 }
    }
}

示例:

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

然而,正如您所看到的,C#示例和Haskell示例需要了解错误处理的风格。虽然您可以在两种样式之间进行转换,但是不能使用具有期望另一种样式的方法/函数。

如果你有一个第三方C#方法,希望异常处理成为事情的方式,你就不能传递一系列*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] 值,并希望它可以处理它。 您必须修改此方法。

但是,反过来说并不完全正确,因为异常处理内置于C#(事实上也是Haskell);您无法真正选择退出此类语言的异常处理。但是,想象一下,这种语言没有内置的异常处理(也许是PureScript?),这也是正确的。

1 C#代码可能无法编译。

答案 1 :(得分:2)

我没有编译器,但您可能想检查我的language-ext project。它是C#的功能基类库。

根据您的需要,它有:

  • Seq<A>这是一个像懒惰的枚举,只会评估一次
  • Try<A>这是一个基于委托的monad,允许您从第三方代码中捕获异常
  • 其他常见错误处理monad:Option<A>Either<L, R>
  • 这些monad的奖励变体:OptionAsync<A>TryOption<A>TryAsync<A>TryOptionAsync<A>
  • 能够轻松转换这些类型:ToOption()ToEither()
  

在方法的lazy list参数上应用函数(LINQ select,map ...)并将获取列表中的每个元素(懒惰)并执行可能失败的计算(抛出异常或返回Error / Either) 。   仅在第三方函数“内部”使用的列表,我不想再遍历每个元素一次。

这有点不清楚实际目标。在language-ext中你可以这样做:

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);

但是如果Values()函数抛出一个异常,它就会结束它的屈服并返回。所以你最好有这个:

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

TryTry monad的构造函数。所以你的结果将是一系列Try thunks。如果您不关心异常,可以将其转换为Option

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

您可以通过以下方式访问所有成功案例:

var validValues = Values().Somes();

或者您可以使用Either

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

然后你可以得到有效的结果:

var seq = Seq(Values());

var validValues = seq.Rights();

错误:

var errors = seq.Lefts();

我将其转换为Seq,因此不会评估两次。

无论如何,如果要捕获在对可枚举的惰性求值期间发生的异常,那么您将需要包装每个值。如果使用惰性值可以发生异常,但是在函数内,那么你唯一的希望就是用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);
  

直觉上我试图将列表从Eithers列表转换为列表中的任何一个,但这只能通过使用函数列表来完成。比如,聚合或减少(Haskell的序列函数是否相同?)。

你可以在language-ext中这样做:

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

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

Traverse也受支持:

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

monad的所有组合都支持Sequence()Traverse;所以你可以使用Seq<Either<L, R>>来获得Either<L, Seq<R>>,这可以保证不会多次调用延迟序列。或Seq<Try<A>>获取Try<Seq<A>>或任何异步变体以进行并发排序和遍历。

我不确定这是否涵盖了你所提出的问题,这个问题有点宽泛。更具体的例子很有用。