【问题标题】:Replacing Exceptions With Either/Maybe/Option用 Either/Maybe/Option 替换异常
【发布时间】:2017-11-23 18:13:19
【问题描述】:

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

让我尝试更全面地重新解释它:

给定:

  • 我有一个第 3 方函数(一个导入到我的代码中但我无权访问的函数),它接收一个惰性列表(c# IEnumerable,f# Seq...)并使用它

我想要:

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

  • 仅在“内部”第 3 方函数中使用的列表,我不想对每个元素重复一次以上。

有了异常/副作用,如果发现错误,可以通过从 select、map 函数中抛出异常来轻松实现,这将停止“内部”第 3 方函数的执行。然后我可以在它之外处理异常(没有第三方“意识到”我的错误处理),将错误处理的责任留给我。

虽然使用 Either 似乎不可能在不改变 3rd 方功能的情况下获得相同的行为。直观地说,我试图将列表从 Eithers 列表转换为 Either 的列表,但这只能通过使用带有函数的列表来完成。 Like、aggregated 或 reduce(Haskell 的 Sequence 函数作用相同吗?)。

所有这一切都让我想到了返回类型是 Maybes/Eithers 还是 Error,错过了这种行为?有没有其他方法可以与他们实现同样的目标?

【问题讨论】:

  • 这在不改变第三方功能的情况下是不可能的。而且我认为这是唯一合理的行为:如果可能的话,您将失去参考透明度。异常(可以被捕获)就像null,因为就类型系统而言,它们是(方便的)“黑客”。 null 基本上将所有内容烘焙到 Maybe monad 中,例外情况在其上添加 Either。使用您详述的异常的方法类似于使用 null 来表示特殊/额外的单例值,而不仅仅是使用选项类型。

标签: c# haskell exception functional-programming either


【解决方案1】:

据我所知,Haskell Either 与 C#/Java 样式的异常同构,这意味着存在从基于 Either 的代码到基于异常的代码的转换,反之亦然。不过,我对此不太确定,因为可能存在一些我不知道的极端情况。

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

您可以在 C# 中处理异常,也可以使用 Either。 C# 中的默认设置是不进行错误处理1:

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

这将遍历source,直到发生异常。如果没有抛出异常,它会返回IEnumerable&lt;TResult&gt;,但是如果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 案例,其中包含所有映射的元素。如果只有一个 Left case 返回,你会得到它,并且不会进行进一步的处理。

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

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

在 Haskell 中,您可以使用 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]

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

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

同样,你可以用 Haskell 实现同样的效果:

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

例子:

*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 示例都需要了解错误处理的风格。虽然您可以在两种样式之间进行转换,但您不能将其中一种与期望另一种的方法/函数一起使用。

如果你有一个第三方 C# 方法期望异常处理是事情的完成方式,你不能传递一个 Either 值序列并希望它可以处理它。 您必须修改方法。

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


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

【讨论】:

  • sequence . fmap f = traverse f
  • 在 C# 中:在你的函数、ProcessUntilException 和抛出异常之间仍然存在很小的差距,即使发现错误,你的函数也会继续消耗列表,如果在 if 中添加 break 语句应该给出相同的结果行为,如果你想留在“monad word”中,这个函数可以使用 IEnumerable
  • @CountOren 当我写那个 C# 代码 sn-p; 时我很懒惰。我手边没有 C# 编译器,所以我不想让它变得比我必须做的更复杂,但我相信你知道如何修改它,以便在 exceptionHappenedtrue 时停止迭代.
  • @CountOren 但可以肯定的是:您应该能够编写在一种范式和另一种范式之间转换的函数。如果我对等空现象的看法是正确的,那么这应该总是可行的。
  • 如果你在 GHCi 中尝试这个,请确保import Data.Either
【解决方案2】:

我手头没有编译器,但您可能想查看我的language-ext project。它是 C# 的函数式基类库。

满足您的需要:

  • Seq&lt;A&gt; 这是一个缺点,比如惰性可枚举,它只会评估一次
  • Try&lt;A&gt; 这是一个基于委托的 monad,它允许您从第三方代码中捕获异常
  • 其他常见错误处理 monad:Option&lt;A&gt;Either&lt;L, R&gt;
  • 这些单子的额外变体:OptionAsync&lt;A&gt;TryOption&lt;A&gt;TryAsync&lt;A&gt;TryOptionAsync&lt;A&gt;
  • 能够在这些类型之间轻松转换:ToOption()ToEither() 等。

在方法的惰性列表参数上应用一个函数(LINQ select,map...),并将获取列表中的每个元素(惰性)并执行可能失败的计算(抛出异常或返回错误/要么) . 仅在第 3 方函数“内部”使用的列表,我不想对每个元素重复一次以上。

这有点不清楚实际目标。在语言扩展中,您可以这样做:

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 thunk。如果您不关心异常,可以将其转换为 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 列表转换为 Either 的列表,但这只能通过使用带有函数的列表来完成。 Like、aggregated 或 reduce(Haskell 的 Sequence 函数作用相同吗?)。

您可以像这样在语言扩展中执行此操作:

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&lt;Either&lt;L, R&gt;&gt; 来获得Either&lt;L, Seq&lt;R&gt;&gt;,这将保证惰性序列不会被多次调用。或Seq&lt;Try&lt;A&gt;&gt; 以获取Try&lt;Seq&lt;A&gt;&gt;,或任何用于并发排序和遍历的异步变体。

我不确定这是否涵盖了您要问的内容,这个问题有点宽泛。一个更具体的例子会很有用。

【讨论】:

  • 看看这个问题的更具体的版本和我的答案:stackoverflow.com/questions/44578286/…
  • 另外,我使用的是可选包,因为我正在运行语言扩展不支持的 .net 4。
  • @louthster Language-ext 很棒。除了github.com/louthy/language-ext/wiki,还有其他关于它的使用信息吗?我最近还阅读了“c#(enrico buonanno)中的函数式编程”,这让我找到了它。
猜你喜欢
  • 2018-06-23
  • 2020-06-18
  • 1970-01-01
  • 2016-04-14
  • 2011-11-22
  • 2012-10-11
  • 1970-01-01
  • 1970-01-01
  • 2020-11-16
相关资源
最近更新 更多