linq 我的类型的SelectMany方法应具有什么签名?

xoefb8l8  于 2022-12-06  发布在  其他
关注(0)|答案(2)|浏览(134)

我有一个简单的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的签名,但是没有告诉你为什么需要它们。你可以通过编译器代码来实现,但是这是一个痛苦的旅程。

q8l4jmvw

q8l4jmvw1#

建议的SelectMany重载只会编译,因为唯一的测试用例不会更改类型。

var ret5 =
    from x in l
    from y in r
    select y;

lr具有相同的类型:Either<string, int>。因此,尽管SelectMany重载的约束太大,但实际上已经足够让代码编译了。然而,在不同类型的Either之间进行Map时,它不够灵活。

TL;DR;

SelectMany的正确类型为:

public static Either<L, RR> SelectMany<L, R, T, RR>(
    this Either<L, R> source,
    Func<R, Either<L, T>> k,
    Func<R, T, RR> s)

暂时忘记这个特定的重载可能会有所帮助,因为它是C#特有的奇怪之处,在我所知道的其他语言(F#和Haskell)中没有等价物。

绑定

C#中的标准SelectMany方法对应于通常所知的 monadic bind。对于OP中的Either类型,它看起来如下所示:

public static Either<L, RR> SelectMany<L, R, RR>(
    this Either<L, R> source,
    Func<R, Either<L, RR>> selector)
{
    if (source.IsLeft)
        return new Left<L, RR>(source.Left);

    return selector(source.Right);
}

这个重载处理起来要容易得多,因为它不需要另一个重载所采用的奇怪的额外步骤函数。
但是,为了清楚起见,需要使用另一个重载来使查询语法变得简单,所以我将在稍后再讨论这个问题。
如果你可以实现一个SelectMany方法,那么这个类型就形成了一个单子。注意,它只Map了右边的元素。如果我们也添加一个Select方法,这可能会更容易看到。

函式

所有的单子也都是functors。如果你有一个合法的SelectMany(单子绑定),你 * 总是 * 可以实现Select,并且实现是完全自动化的:

public static Either<L, RR> Select<L, R, RR>(
    this Either<L, R> source,
    Func<R, RR> selector)
{
    return source.SelectMany(r => new Right<L, RR>(selector(r)));
}

注意,Select只MapRRR,而L保持不变。Either 实际上不仅仅是一个函子,而是一个函子族-每个L对应一个函子。例如,Either<string, R>产生的函子与Either<int, R>不同。然而,Either * 是bifunctor
只有一种类型需要Map:R .

查询语法

如果你只需要方法调用语法,这两个方法就足够了,但是如果你还需要查询语法,你就需要另一个SelectMany重载。就我所知,如果你已经有了SelectSelectMany,你可以随时(?)用相同的嵌套lambda表达式实现另一个重载:

public static Either<L, RR> SelectMany<L, R, T, RR>(
    this Either<L, R> source,
    Func<R, Either<L, T>> k,
    Func<R, T, RR> s)
{
    return source.SelectMany(x => k(x).Select(y => s(x, y)));
}

我实际上是从一个SelectMany方法中复制粘贴了一个完全不同monad(State)的表达式。
再次注意L是固定的。它不参与任何Map,所以它就像不存在一样。“intermediate”类型在这里被称为T

单态

编译OP中建议的方法的原因是C#编译器实际上是相当宽容的。如果你使泛型类型不那么泛型,它仍然可以编译。
例如,令我非常惊讶的是,去年我发现还可以使用约束更严格的Select方法来实现monomorphic functors

封装

尽管如此,您还是应该考虑为所讨论的类型添加一些封装。就代码而言,任何人都可以添加一个继承自Either<L, R>的新类,并且行为完全不稳定。
例如,您可以考虑Church-encoded Either

cfh9epnr

cfh9epnr2#

Func〈R,Either〈L,R〉〉g,//为什么R在这里?为什么不是L,R。只能是1个参数。
因为在您的实现中,您需要:
g(任.右);
您可以简单地将其写成:

Func<L, Either<L, R>> g,

并传入:

g(either.Left);

当然,它的行为是非常不同的,所以这只是一个你想要什么行为的问题,你希望你的Either有这样的行为,“如果外部的either的值是左的,就使用它,甚至不要构造内部的either,如果外部的either的值是右的,得到内部函数的值并使用它(这是函数g)* 外部的either根据定义是右either*,如果不是,你已经返回了左值 *,甚至没有调用g *。
(我想借此机会指出,在这里使用有意义的变量名是问题的一部分。当你调用一个变量g * 时,没有人知道这意味着什么 *。如果你给它一个有意义的名称,你可能会更清楚什么时候使用它,以及为什么在这种情况下从来没有一个左值传递给它。)
但是,您可以使用SelectMany编写一个行为不同的不同类型。
本质上,问题归结为 * 您希望y在此代码中的类型是什么 *:

from x in l
from y in r

在本例中,您希望yl的右值。但您也可以编写其他一些实现,传入 * 任何它想要的 *。您可以传入Tuple<L,R>,并让x在该查询中表示一对 both 值。现在,对于特定的Either,已知左值没有被填充,因此这实际上并不是一个好主意,但是其他类型的一对值的单子可能需要 both 值才能从前一个值生成新值。

相关问题