【问题标题】:Trouble Converting Lambda Expression to Delegate Because of "some return types"由于“某些返回类型”,无法将 Lambda 表达式转换为委托
【发布时间】:2017-07-21 12:00:30
【问题描述】:

我正在用 C# 编写一个链接列表程序,因为我想测试我对这门语言的感受,但我遇到了一些严重的困难。我正在尝试实现一个 Map 方法,该方法的功能类似于 Haskell 映射函数(两者的代码如下)。但是,我收到了错误消息:

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();}
}

虽然 Assign、DoR(Do 和 Return 的缩写)和 Do 看起来像是“代码味道”,但它们是我试图不写的原因

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

type 语句(我习惯于模式匹配)。如果有人有更好的想法,我很想知道他们,但主要是我关心突出的问题。

【问题讨论】:

  • 看起来您正在尝试将 C# 用作函数式语言。虽然委托等使这成为可能,但这并不是 C# 的主要使用方式。如果你想要一种函数式编程 .Net 语言,你看过 F# 吗?
  • 你需要让Join返回结果LinkedList。或者更改您传递给DoR 的lambda,以在变量中捕获LinkedList,然后在其上调用Join,最后返回它。
  • 在我看来,您所做的事情“没有抓住 C# 的重点”。如果存在 C# 的 List 对象,这些都不是必需的。
  • @itsme86 更多的是我不喜欢一遍又一遍地输入相同的东西(如果你在谈论静态方法)。虽然我确实从函数式语言中学到了这些技术,但我不相信这个程序是函数式的,事实上它对我来说似乎非常不实用(因为有很多副作用)。有什么可以代替 if(x!=null) {f();} else {g();} 的推荐吗?
  • 为什么LinkedList&lt;T&gt; 中的Tclass 约束?看来Map可以简单写成return new LinkedList&lt;U&gt;(f(first)) { rest = this.rest == null ? null : this.rest.Map(f) }

标签: c# lambda functional-programming


【解决方案1】:

从您的直接问题开始:这里的基本问题是您正在混合和匹配具有 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&lt;U&gt;() 方法中使用语句体 lambda,将新列表保存到变量中,然后返回该变量。但这比仅仅更改 Join() 方法增加了很多代码,所以它似乎不太可取。

也就是说,您似乎在这里有点滥用 C#。就像用函数式语言编写代码一样,人们应该真正努力以该语言惯用的方式编写真正的函数式代码,在编写 C# 代码时也应该努力编写真正的命令式代码,以 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&lt;U&gt;() 方法的意义。为什么要那样限制它?)

现在,说了这么多,在我看来,如果您确实想要在 C# 中实现函数式链接列表,那么将其设为不可变列表是有意义的。我对 Haskell 不熟悉,但从我对函数式语言的有限使用来看,我的印象是,如果不是 100% 强制执行(例如 XSL),不变性是函数式语言数据类型中的一个常见特征。因此,如果尝试在 C# 中重新实现函数式语言结构,为什么不遵循该范式呢?

例如,请参阅 Eric Lippert 在Efficient implementation of immutable (double) LinkedList 中的回答。或者他关于 C# 中不可变性的优秀系列文章(您可以从这里开始:Immutability in C# Part One: Kinds of Immutability),您可以从中获得有关如何创建各种不可变集合类型的想法。

在浏览 Stack Overflow 的相关帖子时,我发现了一些虽然不直接适用于您的问题,但可能仍然很有趣(我知道我发现它们非常有趣):

how can I create a truly immutable doubly linked list in C#?
Immutable or not immutable?
Doubly Linked List in a Purely Functional Programming Language
Why does the same algorithm work in Scala much slower than in C#? And how to make it faster? Converting C# code to F# (if statement)

我喜欢最后一个,主要是因为它在问题本身的呈现和回复(答案和 cmets)中都有助于很好地说明为什么避免试图从一种语言音译到另一种语言如此重要,而是真正尝试熟悉一种语言的设计使用方式,以及常用的数据结构和算法如何以惯用的方式在给定的语言中表示。

附录:

受 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 的略有不同。您枚举它以访问元素。实现方式也不同;使用二叉树是我启用Join() 方法的方式。

请注意,在实现接口IEnumerable&lt;T&gt; 后,实现Map&lt;U&gt;() 方法的一种方法是根本不这样做,而只需使用内置的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 对象。

可能还有其他方法可以做到这一点(就此而言,我至少知道另一种方法),但以上是从概念上最有意义的方法。

【讨论】:

    猜你喜欢
    • 2016-04-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多