我正在用C#编写一个Linked List程序,因为我想测试一下我对这种语言的看法,我遇到了一些严重的困难。我正在尝试实现一个像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();}
}
虽然分配,DoR(执行和返回的简称),并且似乎它们是“代码味道”,但它们是我想要写的
if (x != null) {f();}
else {g();}
类型语句(我习惯于模式匹配)。如果有人有更好的想法,我很想知道它们,但大多数时候我都很关注突出的问题。
答案 0 :(得分:2)
从您的直接问题开始:这里的基本问题是您正在混合和匹配具有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#。就像在函数式语言中编写代码一样,人们应该真正努力以习惯于该语言的方式编写真正的功能代码,因此在编写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<U>()
方法上看到通用类型约束的重点。为什么要这样限制?)
现在,所有这一切,在我看来,如果你想在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)
我喜欢最后一个主要是因为在问题本身的呈现和回复(答案和评论)中有助于说明为什么避免尝试从一种语言到一个语言的音译是如此重要另一方面,而是真正尝试熟悉语言的设计方式,以及如何在特定语言中用惯用语法表示常见的数据结构和算法。
<强>附录:强>
受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<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
对象。
可能有其他方法可以做到这一点(就此而言,我至少知道另外一种方法),但以上是概念上最有意义的方法。