由于"some return types",将 lambda 表达式转换为委托时遇到问题



我正在编写C#中的链接列表程序,因为我想测试我对语言的感觉,并且遇到了一些严重的困难。我正在尝试实现一种诸如Haskell Map函数(下面的代码)之类的地图方法。但是,我收到错误消息:

main.cs(43,66): error CS0029: Cannot implicitly convert type `void' to `MainClass.LinkedList<U>'
main.cs(43,33): error CS1662: Cannot convert `lambda expression' to delegate type `System.Func<MainClass.LinkedList<U>>' because some of the return types in the block are not implicitly convertible to the delegate return type

相关代码:理想的Haskell代码:

map :: [a] -> (a -> b) -> [b]
map (x:[]) f = (f x) : []
map (x:xs) f = (f x) : (map xs f)

c#代码:

public class LinkedList<T> where T: class
   {
     public T first;
     public LinkedList<T> rest;
     public LinkedList(T x) {this.first = x;}
     public void Join(LinkedList<T> xs)
     {
       Do(this.rest, ()=>this.rest.Join(xs), ()=>Assign(ref this.rest, xs));  
     }
     public LinkedList<U> Map<U>(Func<T, U> f) where U: class
     {
       return DoR(this.rest, ()=>new LinkedList<U>(f(this.first)).Join(this.rest.Map(f)), ()=>new LinkedList<U>(f(this.first)));
     }
public static void Assign<T>(ref T a, T b)
{
  a = b;
}
public static U DoR<T, U>(T x, Func<U> f, Func<U> g)
{
  if (x!=null) {return f();}
  else {return g();}
}
public static void Do<T>(T x, Action f, Action g)
{
  if (x != null) {f();}
  else {g();}
}

分配时(做和返回的简短),看起来像是"代码气味",它们是我想不写

的想法
if (x != null) {f();}
else {g();}

类型语句(我习惯于模式磨练)。如果有人有更好的想法,我很想认识他们,但是大多数情况下,我关注着突出的问题。

从您的直接问题开始:这里的基本问题是您要混合和匹配具有void返回类型或实际返回类型的Lambda表达式。这可以通过更改您的Join()方法来解决,以便它返回用于调用Join()的列表:

public LinkedList<T> Join(LinkedList<T> xs)
{
    Do(this.rest, () => this.rest.Join(xs), () => Assign(ref this.rest, xs));
    return this;
}

另一种方法是在Map<U>()方法中拥有一个语句主体lambda,该方法将新列表保存到变量,然后返回。但这添加了更多的代码,而不仅仅是更改Join()方法,因此似乎不太优选。

也就是说,您似乎在这里滥用C#。就像用功能语言编写代码时一样,人们应该真正努力编写 real 功能代码,以这种语言的惯用方式,因此在编写C#代码写作时也应该努力 real 命令代码,以C#的惯用方式。

是的,C#具有一些功能性的功能,但是它们通常没有与实际功能性语言中的功能相同的功能,并且它们旨在允许C#程序员获得低悬而未决的水果代码的功能风格无需切换语言。还要注意的一件事是,lambda表达式生成的代码比普通C#命令代码多得多。

坚持使用更多惯用的C#代码,您要实现的数据结构可以更简洁地编写,并且以创建更有效的代码的方式。看起来像这样:

class LinkedList<T>
{
    public T first;
    public LinkedList<T> rest;
    public LinkedList(T x) { first = x; }
    public void Join(LinkedList<T> xs)
    {
        if (rest != null) rest.Join(xs);
        else rest = xs;
    }
    public LinkedList<U> Map<U>(Func<T, U> f) where U : class
    {
        LinkedList<U> result = new LinkedList<U>(f(first));
        if (rest != null) result.Join(rest.Map(f));
        return result;
    }
}

(对于您的Map<U>()方法,我看不到通用类型约束的点。为什么会这样限制它?)

现在,在我看来,在我看来,如果您确实想要C#中的功能性链接列表实现,那么将其作为不可变的列表是有意义的。我不熟悉Haskell,但是由于我通常使用功能语言的使用有限,我的印象是,不变性是功能语言数据类型的常见功能,即使没有执行100%(例如XSL)。因此,如果尝试在C#中重新实现功能语言构造,为什么不遵循该范式?

参见,例如,埃里克·利普特(Eric Lippert)在有效实施不变的(双重)linkedlist时的答案。或他关于C#不变性的一系列出色的文章(您可以从这里开始:C#中的不变性:一种不变性),您可以在其中了解如何创建各种不变的收藏类型。

在浏览相关帖子的浏览堆栈溢出中,我发现一些不直接适用于您的问题,但仍然很感兴趣(我知道我发现它们很有趣):

如何在C#中创建一个真正不变的双重链接列表?
不可变或不变?
纯粹的功能编程语言中的双重链接列表
为什么相同的算法在Scala中起作用比C#慢得多?以及如何使其更快?将C#代码转换为F#(如果语句)

我喜欢最后一个,主要是因为在问题本身的介绍和答复(答案和评论)中都很好地说明了为什么要避免尝试从一种语言到另一种语言而如此重要,而是如此重要为了真正尝试熟悉一种语言的使用方式,以及如何以给定语言表示通用的数据结构和算法。

附录:

受埃里克·利普特(Eric Lippert)的不变列表类型的粗略草稿的启发,我写了一个不同的版本,其中包括 Join()方法,以及在列表的正面和结尾添加元素的能力:

abstract class ImmutableList<T> : IEnumerable<T>
{
    public static readonly ImmutableList<T> Empty = new EmptyList();
    public abstract IEnumerator<T> GetEnumerator();
    public abstract ImmutableList<T> AddLast(T t);
    public abstract ImmutableList<T> InsertFirst(T t);
    public ImmutableList<T> Join(ImmutableList<T> tail)
    {
        return new List(this, tail);
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
    class EmptyList : ImmutableList<T>
    {
        public override ImmutableList<T> AddLast(T t)
        {
            return new LeafList(t);
        }
        public override IEnumerator<T> GetEnumerator()
        {
            yield break;
        }
        public override ImmutableList<T> InsertFirst(T t)
        {
            return AddLast(t);
        }
    }
    abstract class NonEmptyList : ImmutableList<T>
    {
        public override ImmutableList<T> AddLast(T t)
        {
            return new List(this, new LeafList(t));
        }
        public override ImmutableList<T> InsertFirst(T t)
        {
            return new List(new LeafList(t), this);
        }
    }
    class LeafList : NonEmptyList
    {
        private readonly T _value;
        public LeafList(T t)
        {
            _value = t;
        }
        public override IEnumerator<T> GetEnumerator()
        {
            yield return _value;
        }
    }
    class List : NonEmptyList
    {
        private readonly ImmutableList<T> _head;
        private readonly ImmutableList<T> _tail;
        public List(ImmutableList<T> head, ImmutableList<T> tail)
        {
            _head = head;
            _tail = tail;
        }
        public override IEnumerator<T> GetEnumerator()
        {
            return _head.Concat(_tail).GetEnumerator();
        }
    }
}

公共API与Eric的API有些不同。您将其列举以访问元素。实施也不同。使用二进制树是我启用Join()方法的方式。

请注意,实现接口IEnumerable<T>,实现Map<U>()方法的一种方法是完全不执行此操作,而只是使用内置的Enumerable.Select()

ImmutableList<T> list = ...; // whatever your list is
Func<T, U> map = ...; // whatever your projection is
IEnumerable<U> mapped = list.Select(map);

只要map功能相对便宜,那就可以了。每当枚举mapped时,它都会重新数量list,并应用map功能。mapped枚举保持不变,因为它基于不变的list对象。

可能还有其他方法可以做(我知道至少有一个),但是以上是概念上最有意义的。

最新更新