用 Either/Maybe/Option 替换异常
Replacing Exceptions With Either/Maybe/Option
我在尝试用 c# 中的任一 monad 替换异常时遇到了这个 。
这让我觉得这可能不仅是语言特定的问题,还有更多与技术相关的缺失功能。
让我尝试更全面地重新解释它:
鉴于:
- 我有一个第 3 方函数(一个导入到我的代码中但我无权访问的函数),它接收一个惰性列表(c# IEnumerable、f# Seq...)并使用它
我要:
在方法的惰性列表参数上应用函数 (LINQ select,map...) 并将获取列表中的每个元素(惰性地)并进行计算失败(抛出异常或 returning Error/Either)。
仅要使用的列表 "inside" 第 3 方函数,我不想对每个元素进行多次迭代。
使用 Exceptions/side 效果可以通过从 select 抛出异常轻松实现,如果发现错误,地图函数将停止执行 "inside" 第 3 方函数。然后我可以在它之外处理异常(没有第 3 方是我的错误处理的 "aware"),将错误处理的责任留给我。
虽然使用 Either 似乎不可能在不改变第 3 方功能的情况下获得相同的行为。凭直觉,我试图将列表从 Eithers 列表转换为 Either of list,但这只能通过使用带有函数的列表来完成。喜欢,聚合或减少(Haskell 的 Sequence 函数作用相同吗?)。
所有这些让我想到的问题是 Maybes/Eithers 或 Error as return 类型,缺少此行为?有没有其他方法可以实现同样的目的?
据我所知,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
直到发生异常。如果没有抛出异常,它会 return 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
是 return 的函数 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
值从未被 returned,您会得到一个 Right
后盖,其中包含所有映射元素。如果 只有一个 Left
案例被 returned,你就明白了,没有进一步的处理。
您还可以想象您有一个 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
这 return 包含所有 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 示例 都需要了解 错误处理的风格。虽然您可以在这两种样式之间进行转换,但您不能将一种样式与预期另一种样式的 method/function 一起使用。
如果您有一个第三方 C# 方法期望异常处理是完成事情的方式,您不能向它传递一系列 Either
值并希望它能够处理它。 你必须修改方法。
反之则不然,因为异常处理内置于 C# 中(事实上 Haskell 也是如此);您不能真正选择退出此类语言的异常处理。然而,想象一下没有内置异常处理的语言(也许是 PureScript?),这也是正确的。
1 C# 代码可能无法编译。
我手边没有编译器,但您可能想查看我的 language-ext project。它是 C# 的功能基础 class 库。
根据您的需要,它有:
Seq<A>
这是一个像 lazy enumerable 这样的缺点,它只会评估一次
Try<A>
这是一个基于委托的 monad,允许您从第三方代码中捕获异常
- 其他常见错误处理 monad:
Option<A>
、Either<L, R>
等
- 这些 monad 的额外变体:
OptionAsync<A>
、TryOption<A>
、TryAsync<A>
、TryOptionAsync<A>
- 能够在这些类型之间轻松转换:
ToOption()
、ToEither()
等
To apply a function (LINQ select,map...) on the method's lazy list argument and will take each element of the list (lazily) and will do computation that might fail (throwing an exception or returning Error/Either).
The list to be consumed only "inside" the 3rd party function, I don't want to have to iterate over each element more then once.
这有点不清楚实际目标。在 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()
函数抛出异常,那么它会结束它的屈服并会 return。所以你最好有这个:
// Dummy lazy enumerable
IEnumerable<Try<int>> Values()
{
for(int i = 0; i < 100; i++)
{
yield return Try(() => UnreliableExternalFunc(i));
}
}
Try
是 Try
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);
Intuitively I was trying to convert the list from the list of Eithers to Either of list, but this can be done only by consuming the list with functions. Like, aggregate or reduce (does Haskell's Sequence function act the same?).
您可以像这样在 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));
monads的所有组合都支持Sequence()
和Traverse
;所以你可以用 Seq<Either<L, R>>
来获得 Either<L, Seq<R>>
,这将保证惰性序列不会被多次调用。或 Seq<Try<A>>
得到 Try<Seq<A>>
,或任何用于并发排序和遍历的异步变体。
我不确定这是否涵盖了您的问题,这个问题有点宽泛。一个更具体的例子会很有用。
我在尝试用 c# 中的任一 monad 替换异常时遇到了这个
让我尝试更全面地重新解释它:
鉴于:
- 我有一个第 3 方函数(一个导入到我的代码中但我无权访问的函数),它接收一个惰性列表(c# IEnumerable、f# Seq...)并使用它
我要:
在方法的惰性列表参数上应用函数 (LINQ select,map...) 并将获取列表中的每个元素(惰性地)并进行计算失败(抛出异常或 returning Error/Either)。
仅要使用的列表 "inside" 第 3 方函数,我不想对每个元素进行多次迭代。
使用 Exceptions/side 效果可以通过从 select 抛出异常轻松实现,如果发现错误,地图函数将停止执行 "inside" 第 3 方函数。然后我可以在它之外处理异常(没有第 3 方是我的错误处理的 "aware"),将错误处理的责任留给我。
虽然使用 Either 似乎不可能在不改变第 3 方功能的情况下获得相同的行为。凭直觉,我试图将列表从 Eithers 列表转换为 Either of list,但这只能通过使用带有函数的列表来完成。喜欢,聚合或减少(Haskell 的 Sequence 函数作用相同吗?)。
所有这些让我想到的问题是 Maybes/Eithers 或 Error as return 类型,缺少此行为?有没有其他方法可以实现同样的目的?
据我所知,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
直到发生异常。如果没有抛出异常,它会 return 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
是 return 的函数 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
值从未被 returned,您会得到一个 Right
后盖,其中包含所有映射元素。如果 只有一个 Left
案例被 returned,你就明白了,没有进一步的处理。
您还可以想象您有一个 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
这 return 包含所有 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 示例 都需要了解 错误处理的风格。虽然您可以在这两种样式之间进行转换,但您不能将一种样式与预期另一种样式的 method/function 一起使用。
如果您有一个第三方 C# 方法期望异常处理是完成事情的方式,您不能向它传递一系列 Either
值并希望它能够处理它。 你必须修改方法。
反之则不然,因为异常处理内置于 C# 中(事实上 Haskell 也是如此);您不能真正选择退出此类语言的异常处理。然而,想象一下没有内置异常处理的语言(也许是 PureScript?),这也是正确的。
1 C# 代码可能无法编译。
我手边没有编译器,但您可能想查看我的 language-ext project。它是 C# 的功能基础 class 库。
根据您的需要,它有:
Seq<A>
这是一个像 lazy enumerable 这样的缺点,它只会评估一次Try<A>
这是一个基于委托的 monad,允许您从第三方代码中捕获异常- 其他常见错误处理 monad:
Option<A>
、Either<L, R>
等 - 这些 monad 的额外变体:
OptionAsync<A>
、TryOption<A>
、TryAsync<A>
、TryOptionAsync<A>
- 能够在这些类型之间轻松转换:
ToOption()
、ToEither()
等
To apply a function (LINQ select,map...) on the method's lazy list argument and will take each element of the list (lazily) and will do computation that might fail (throwing an exception or returning Error/Either). The list to be consumed only "inside" the 3rd party function, I don't want to have to iterate over each element more then once.
这有点不清楚实际目标。在 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()
函数抛出异常,那么它会结束它的屈服并会 return。所以你最好有这个:
// Dummy lazy enumerable
IEnumerable<Try<int>> Values()
{
for(int i = 0; i < 100; i++)
{
yield return Try(() => UnreliableExternalFunc(i));
}
}
Try
是 Try
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);
Intuitively I was trying to convert the list from the list of Eithers to Either of list, but this can be done only by consuming the list with functions. Like, aggregate or reduce (does Haskell's Sequence function act the same?).
您可以像这样在 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));
monads的所有组合都支持Sequence()
和Traverse
;所以你可以用 Seq<Either<L, R>>
来获得 Either<L, Seq<R>>
,这将保证惰性序列不会被多次调用。或 Seq<Try<A>>
得到 Try<Seq<A>>
,或任何用于并发排序和遍历的异步变体。
我不确定这是否涵盖了您的问题,这个问题有点宽泛。一个更具体的例子会很有用。