9

作为 FP 设计关于可重用库的关键区别之一的前提(对于我正在学习的内容),这些库比相应的 OO(通常)更以数据为中心。

TFD (类型优先开发)等新兴技术似乎也证实了这一点,Tomas Petricek 在这篇博文中对此进行了很好的解释。

如今的语言是多范式的,同一 Petricek 在其书中解释了可从 C# 使用的各种功能技术。

我在这里感兴趣,因此问题是如何正确分区代码

所以我定义了库数据结构,使用等效的可区分联合(如 Petricek 书中所示),并且我计划根据我的需求的域逻辑将它们与不可变列表和/或元组一起使用。

我在哪里放置作用于数据结构的操作(方法...函数)?

如果我想定义一个使用标准委托中体现的函数值的高阶函数,Func<T1...TResult>我应该把它放在哪里?

常识告诉我将这些方法分组到静态类中,但我想得到那些已经用 C# 编写过函数库的人的确认。

假设这是正确的并且我有一个像这样的高阶函数:

static class AnimalTopology {
  IEnumerable<Animal> ListVertebrated(Func<Skeleton, bool> selector) {
    // remainder omitted
  }
}

如果选择脊椎动物有N个我想在图书馆展示的特殊案例,那么更正确的展示方式是什么。

static class VertebratedSelectorsA {
  // this is compatible with "Func<Skeleton, bool> selector"
  static bool Algorithm1(Skeleton s) {
    //...
  } 
}

或者

static class VertebratedSelectorsB {
  // this method creates the function for later application
  static Func<Skeleton, bool> CreateAlgorithm1Selector(Skeleton s) {
     // ...
  } 
}

任何指示将不胜感激。

编辑:

我想引用 Mads Torgersen 的真实世界函数式编程前言T. Petricek 的两句话:

[...] 您可以在 C# 中使用函数式编程技术获得极大的好处,尽管在 F# 中这样做更容易、更自然。[...] 函数式编程是一种心态。[...]

编辑-2:

  • 我觉得有必要进一步澄清这个问题。标题中提到的函数与函数式编程严格相关;我不是在问更实用的分组方法的方式,更逻辑的方式或更有意义的方式。

  • 这意味着实现将尝试尽可能多地遵循NOOO宣言总结的 FP 的创始概念,并在此处引用以方便和清晰:

  1. 类上的函数和类型
  2. 纯度高于可变性
  3. 组合优于继承
  4. 方法分派之上的高阶函数
  5. 空值选项

问题是围绕如何布局遵循 FP 概念编写的 C# 库,因此(例如)绝对不是将方法放入数据结构中的选项;因为这是一个创始的面向对象范式。

编辑-3:

此外,如果问题得到回应(和各种评论),我不想给人一种错误的印象,即有人说一种编程范式优于另一种。和之前一样,我将在其书 Expert F# 3.0(第 20 章 - 设计 F# 库 - pg.565)中提到 FP 的权威人士 Don Syme:

[...] 函数式和 OO 编程方法相互竞争是一个常见的误解。事实上,它们在很大程度上是正交的。[...]

4

2 回答 2

3

注意:如果您想要一个更简短、更中肯的答案,请参阅我的另一个答案。我知道这里的这个似乎是漫无边际的,永远继续下去,谈论你的问题,但也许它会给你一些想法。

Animal如果不知道 和 之间的确切关系,很难回答您的问题Skeleton。我将在回答的后半部分对这种关系提出建议,但在此之前,我将简单地按照我在您的帖子中看到的内容进行。

首先,我将尝试从您的代码中推断出一些事情:

static class AnimalTopology
{
    // Note: I made this function `static`... or did you omit the keyword on purpose?
    static IEnumerable<Animal> ListVertebrated(Func<Skeleton, bool> selector)
    {
        …
    }
}
  1. 如果您根据功能原则设计了该功能,则它应该没有副作用。也就是说,它的输出只依赖于它的论点。(在半面向对象的设置中,可能在 ; 的其他静态成员上AnimalTopology,但由于您没有显示任何内容,所以让我们忽略这种可能性。)

  2. 如果函数确实没有副作用(并且不访问 的静态成员AnimalTopology),那么函数的类型签名表明可以Animal从 a派生 an Skeleton,因为它接受作用于Skeletons 并返回Animals 的东西。

  3. 如果这也是正确的,那么为了能够给出答案,让我假设以下内容:

    class Skeleton
    {
        …
        public Animal Animal { get { … } } // Skeletons have animals!? We'll get to that.
    }
    

现在很明显,您的函数不可能实现,因为它可以从 s 派生Animals Skeleton,但它根本没有收到任何东西Skeleton;它只接收作用于 a 的谓词函数Skeleton。(您可以通过添加第二个 type 参数来解决此问题Func<IEnumerable<Skeleton>> getSkeletons,但是...)

在我看来,类似以下的内容会更有意义:

static IEnumerable<Animal> GetVertebrates(this IEnumerable<Skeleton> skeletons,
                                          Func<Skeleton, bool> isVertebrate)
{
    return skeletons
           .Where(isVertebrate)
           .Select(s => s.Animal);
}

现在,人们可能想知道为什么您要从骨骼中猜测动物;并且bool“是脊椎动物”的属性不是动物(或骨骼)的固有属性吗?真的有几种方法可以决定这个吗?

我建议如下:

class Animal
{
     Skeleton Skeleton { get; } // not only vertebrates have skeletons! 
}

class Vertebrate : Animal { … } // vertebrates are a kind of animal 

static class AnimalsExtensions
{
    static IEnumerable<Vertebrate> ThatAreVertebrates(this IEnumerable<Animal> animals)
    {
        return animals.OfType<Vertebrate>();
    }
}

请注意上述扩展方法的使用。这是一个如何使用它的示例:

List<Animal> animals = …;
IEnumerable<Vertebrate> vertebrates = animals.ThatAreVertebrates();

现在假设您的扩展方法做了更复杂的工作。在这种情况下,将它放在自己指定的“算法类型”中可能是个好主意:

interface IVertebrateSelectionAlgorithm
{
    IEnumerable<Vertebrate> GetVertebrates(IEnumerable<Animal> animals);
}

这具有可以设置/参数化的优点,例如通过类构造函数;您可以将算法拆分为多个方法,这些方法都位于同一个类中(但private除了GetVertebrates.)

当然,您可以使用函数闭包进行相同类型的参数化,但根据我的经验,这在 C# 设置中很快就会变得混乱。在这里,类是将一组函数组合为一个逻辑实体的好方法。

于 2013-03-29T10:01:27.530 回答
2

我在哪里放置作用于数据结构的操作(方法...函数)?

我看到了四种常见的方法(没有特别的顺序):

  • 将函数放入数据结构中。(这是面向对象的“方法”方法。它适用于函数仅作用于该类型的实例的情况。它可能不太合适,例如,当一个函数“绘制”多个不同类型的对象并吐出一个另一种类型的对象。在这种情况下,我会......)

  • 将这些函数放在它们自己指定的“算法类”中。(当函数做大量或复杂的工作,或者需要参数化/配置,或者您可能希望将算法拆分为多个函数时,这似乎是合理的,然后您可以通过将它们放入类类型中来逻辑地“分组”在一起。 )

  • 将函数转换为lambda(也称为匿名委托、闭包等)。(如果它们很小并且您只需要在一个特定的地方使用它们,这很有效;代码不会轻易地在不同的地方重用。)

  • 将函数放在静态类中,并使其成为扩展方法。(这就是 LINQ to Objects 的工作原理。它是一种混合函数式和面向对象的方法。需要格外小心才能正确解决可发现性/命名空间问题。很多人会认为这种方法在走得太远时会破坏“封装”。对于反论点,阅读优秀的 C++ 文章“非成员函数如何改进封装”;用“扩展方法”代替“非成员朋友函数”。)

注意:如果人们愿意,我可以更详细地介绍其中的每一个,但在我这样做之前,我会等着看这个答案会收到什么样的反馈。

于 2013-03-29T10:10:36.053 回答