我有一个简单的Either
类型:
public class Left<L, R> : Either<L, R>
{
public override string ToString()
{
return Left.ToString();
}
public Left(L left)
{
Left = left;
IsLeft = true;
}
}
public class Right<L, R> : Either<L, R>
{
public override string ToString()
{
return Right.ToString();
}
public Right(R right)
{
Right = right;
IsRight = true;
}
}
public class Either<L, R>
{
public bool IsLeft {get; protected set;}
public bool IsRight {get; protected set;}
public L Left {get; protected set;}
public R Right {get; protected set;}
public Either(L left)
{
Left = left;
IsLeft = true;
}
public Either(R right)
{
Right = right;
IsRight = true;
}
public Either()
{
}
}
我想在LINQ查询语法中使用这个类型。我可以实现Select
,但是我不能很好地理解所需的SelectMany
方法签名。更准确地说,它是这样工作的:
var l = new Either<string, int>("no, this is a left");
var r = new Either<string, int>(2);
var ret5 =
from x in l
from y in r
select y;
// returns ret5 as a Left, with the expected message
如果我使用下面的实现:
public static Either<L, RR> SelectMany<L, R, RR>(
this Either<L, R> either, // the source "collection"
Func<R, Either<L, R>> g, // why R here? why not L, R. can only be 1 argument. Func<TSource, IEnumerable<TCollection>> collectionSelector,
Func<L, R, RR> f // a transform that takes the contained items and converts to the transformed right
)
{
if (either.IsLeft) return new Left<L, RR>(either.Left);
var inner = g(either.Right);
if (inner.IsLeft) return new Left<L, RR>(inner.Left);
return new Right<L, RR>(f(inner.Left, inner.Right));
}
但我无法将其与记录的签名(此处来自Jon Skeet's Edulinq post)联系起来
public static IEnumerable<TResult> SelectMany<TSource, TCollection, TResult>(
this IEnumerable<TSource> source,
Func<TSource, IEnumerable<TCollection>> collectionSelector,
Func<TSource, TCollection, TResult> resultSelector)
对于具有多个泛型参数的类,有没有一种通用的方法来理解SelectMany
方法的签名应该是什么?
编辑:LaYumba library对于Transition
示例类有一个类似的签名,所以..完整的SelectMany
重载集是否在某个地方被文档化了--包括那些用于多参数类型的重载?
EDIT:非常延迟的编辑,但是reference source详细描述了IEnumerable<>.SelectMany
的签名,但是没有告诉你为什么需要它们。你可以通过编译器代码来实现,但是这是一个痛苦的旅程。
2条答案
按热度按时间q8l4jmvw1#
建议的
SelectMany
重载只会编译,因为唯一的测试用例不会更改类型。l
和r
具有相同的类型:Either<string, int>
。因此,尽管SelectMany
重载的约束太大,但实际上已经足够让代码编译了。然而,在不同类型的Either之间进行Map时,它不够灵活。TL;DR;
SelectMany
的正确类型为:暂时忘记这个特定的重载可能会有所帮助,因为它是C#特有的奇怪之处,在我所知道的其他语言(F#和Haskell)中没有等价物。
绑定
C#中的标准
SelectMany
方法对应于通常所知的 monadic bind。对于OP中的Either
类型,它看起来如下所示:这个重载处理起来要容易得多,因为它不需要另一个重载所采用的奇怪的额外步骤函数。
但是,为了清楚起见,需要使用另一个重载来使查询语法变得简单,所以我将在稍后再讨论这个问题。
如果你可以实现一个
SelectMany
方法,那么这个类型就形成了一个单子。注意,它只Map了右边的元素。如果我们也添加一个Select
方法,这可能会更容易看到。函式
所有的单子也都是functors。如果你有一个合法的
SelectMany
(单子绑定),你 * 总是 * 可以实现Select
,并且实现是完全自动化的:注意,
Select
只MapR
到RR
,而L
保持不变。Either 实际上不仅仅是一个函子,而是一个函子族-每个L
对应一个函子。例如,Either<string, R>
产生的函子与Either<int, R>
不同。然而,Either * 是bifunctor。只有一种类型需要Map:
R
.查询语法
如果你只需要方法调用语法,这两个方法就足够了,但是如果你还需要查询语法,你就需要另一个
SelectMany
重载。就我所知,如果你已经有了Select
和SelectMany
,你可以随时(?)用相同的嵌套lambda表达式实现另一个重载:我实际上是从一个
SelectMany
方法中复制粘贴了一个完全不同monad(State)的表达式。再次注意
L
是固定的。它不参与任何Map,所以它就像不存在一样。“intermediate”类型在这里被称为T
。单态
编译OP中建议的方法的原因是C#编译器实际上是相当宽容的。如果你使泛型类型不那么泛型,它仍然可以编译。
例如,令我非常惊讶的是,去年我发现还可以使用约束更严格的
Select
方法来实现monomorphic functors。封装
尽管如此,您还是应该考虑为所讨论的类型添加一些封装。就代码而言,任何人都可以添加一个继承自
Either<L, R>
的新类,并且行为完全不稳定。例如,您可以考虑Church-encoded Either。
cfh9epnr2#
Func〈R,Either〈L,R〉〉g,//为什么R在这里?为什么不是L,R。只能是1个参数。
因为在您的实现中,您需要:
g(任.右);
您可以简单地将其写成:
并传入:
当然,它的行为是非常不同的,所以这只是一个你想要什么行为的问题,你希望你的
Either
有这样的行为,“如果外部的either的值是左的,就使用它,甚至不要构造内部的either,如果外部的either的值是右的,得到内部函数的值并使用它(这是函数g
)* 外部的either根据定义是右either*,如果不是,你已经返回了左值 *,甚至没有调用g
*。(我想借此机会指出,在这里使用有意义的变量名是问题的一部分。当你调用一个变量
g
* 时,没有人知道这意味着什么 *。如果你给它一个有意义的名称,你可能会更清楚什么时候使用它,以及为什么在这种情况下从来没有一个左值传递给它。)但是,您可以使用
SelectMany
编写一个行为不同的不同类型。本质上,问题归结为 * 您希望
y
在此代码中的类型是什么 *:在本例中,您希望
y
是l
的右值。但您也可以编写其他一些实现,传入 * 任何它想要的 *。您可以传入Tuple<L,R>
,并让x
在该查询中表示一对 both 值。现在,对于特定的Either
,已知左值没有被填充,因此这实际上并不是一个好主意,但是其他类型的一对值的单子可能需要 both 值才能从前一个值生成新值。