如何在函数式语言中实现面向对象的多态性?

How do you implement Object-Oriented polymorphism in a functional language?

假设您在面向对象的应用程序中有这个:

module Talker
  def talk(word)
    puts word
  end
end

module Swimmer
  def swim(distance)
    puts "swimming #{distance}"
  end
end

class Organism
  def initialize
    rise
  end
  
  def rise
    puts "hello world"
  end
end

class Animal extends Organism
  def think(something)
    puts "think #{something}"
  end
end

class Bird extends Animal
  include Talker
end

class Fish extends Animal
  include Swimmer
end

bird = new Bird
fish = new Fish

在此,您可以调用每个方法都是唯一的:

bird.talk("hello")
fish.swim(50)

但是你也可以调用相同的方法:

bird.think("fly")
fish.think("swim")

如果我有一个接受动物的函数,我可以调用 think 函数:

def experience(animal)
  animal.think("one")
  animal.think("two")
  animal.think("one")
end

在伪函数式语言中,你基本上可以这样做:

function experience(animal) {
  think(animal)
  think(animal)
  think(animal)
}

但不是真的,你必须检查类型:

function think(genericObject) {
  if (genericObject is Animal) {
    animalThink(genericObject)
  } else if (genericObject is SomethingElse) {
    somethingElseThink(genericObject)
  }
}

那是因为,在实现你的“体验”功能的时候,你不只是想要体验动物,你还想要体验石头、树木等其他东西,只是它们的体验功能不同而已。

function experience(thing) {
  move(thing)
  move(thing)
  move(thing)
}

function move(thing) {
  case thing {
    match Animal then animalMove(thing)
    match Plant then plantMove(thing)
    match Rock then rockMove(thing)
  }
}

通过这种方式,您无法拥有完全可重用的函数,您的函数必须知道它将在某处接收的特定类型

有什么方法可以避免这种情况并使其更像函数式语言中的 OO 多态性?

如果是这样,在高层次上,如果可以用函数式语言解决它,它是如何工作的?

函数式编程语言有多种实现多态性的方法。我将对比 Java(我最了解的 OOP 语言)和 Haskell(我最了解的函数式语言)。

方式一:“参数多态”

有了参数多态性,您根本不需要了解基础类型。例如,如果我有一个包含 T 类型元素的单链表,实际上我不需要知道任何有关 T 类型的信息就可以找到列表的长度。我会写类似

的东西
length :: forall a . [a] -> Integer
length [] = 0
length (x:xs) = 1 + length xs

in Haskell(显然我想在实践中使用更好的算法,但你明白了)。请注意,列表元素的类型是什么并不重要;获取长度的代码是相同的。第一行是“类型签名”。它说对于每个类型 a,length 将采用 a 的列表并输出一个整数。

这不能用于太多“严重的多态性”,但绝对是一个强有力的开始。它大致对应于 Java 的泛型。

方式二:类型class式多态

即使像检查相等性这样良性的事情实际上也需要多态性。不同的类型需要不同的代码来检查相等性,并且对于某些类型(通常是函数),由于停机问题,检查相等性实际上是不可能的。因此,我们使用“type classes”。

假设我定义了一个包含 2 个元素的新类型,Bob 和 Larry。在 Haskell 中,这看起来像

data VeggieTalesStars = Bob | Larry

我希望能够比较 VeggieTalesStars 类型的两个元素是否相等。为此,我需要实现一个 Eq 实例。

instance Eq VeggieTalesStars where
    Bob == Bob     = True
    Larry == Larry = True
    Bob == Larry   = False
    Larry == Bob   = False

请注意函数 (==) 具有类型签名

(==) :: forall b . Eq b => b -> b -> Bool

这意味着对于每个类型 b,如果 b 有一个 Eq 实例,那么 (==) 可以接受两个类型 b 的参数和 return 一个 Bool。

你可能不难猜到不等于函数 (/=) 也有类型签名

(/=) :: forall b . Eq b => b -> b -> Bool

因为 (/=) 由

定义
x /= y = not (x == y)

当我们调用(/=)函数时,该函数会根据参数的类型部署正确版本的(==)函数。如果参数具有不同的类型,您将无法使用 (/=).

来比较它们

Typeclass-style 多态性允许您执行以下操作:

class Animal b where
    think :: b -> String -> String
    -- we provide the default implementation
    think b string = "think " ++ string

data Fish = Fish
data Bird = Bird

instance Animal Fish where
instance Animal Bird where

Fish 和 Bird 都实现了“Animal”类型class,因此我们可以在两者上调用 think 函数。也就是说,

>>> think Bird "thought"
"think thought"
>>> think Fish "thought"
"think thought"

此用例大致对应于 Java 接口 - 类型可以根据需要实现任意数量的类型 class。但是类型 classes 比接口强大得多。

方式 3:函数

如果你的对象只有一个方法,它也可能只是一个函数。这是避免继承层次结构的一种非常常见的方法 - 处理函数而不是 1-method 基础的继承者 class.

因此可以定义

type Animal = String -> String
    
basicAnimal :: Animal
basicAnimal thought = "think " ++ thought

“动物”实际上只是一种获取一根弦并产生另一根弦的方式。这将对应于 Java 代码

class Animal {
    public String think(String thought) {
        return "think " + thought;
    }
}

假设在Java中,我们决定按如下方式实现动物的子class:

class ThoughtfulPerson extends Animal {
    private final String thought;
    public ThoughtfulPerson(final String thought) {
        this.thought = thought;
    }

    @Override
    public String think(String thought) {
        System.out.println("I normally think " + this.thought ", but I'm currently thinking" + thought + ".");
    }
}

在 Haskell 中,我们将其实现为

thoughtfulPerson :: String -> Animal
thoughtfulPerson originalThought newThought = "I normally think " ++ originalThought ", but I'm currently thinking" ++ newThought ++ "."

Java代码的“依赖注入”是通过Haskell的高阶函数实现的

方式四:组合优于继承+函数

假设我们有一个抽象基础 class 有两个方法的东西:

abstract class Thing {
    public abstract String name();
    public abstract void makeLightBlink(int duration);
}

我正在使用 Java 风格的语法,但希望它不会太混乱。

从根本上说,使用这个抽象基 class 的唯一方法是调用它的两个方法。因此,一个Thing实际上应该被认为是一个由字符串和函数组成的有序对

在像Haskell这样的函数式语言中,我们会写

data Thing = Thing { name :: String, makeLightsBlink :: Int -> IO () }

换句话说,一个“Thing”由两部分组成:一个名称,它是一个字符串,一个函数makeLightsBlink,它接受一个I​​nt并输出一个“IO action”。这是 Haskell 处理 IO 的方式 - 通过类型系统。

不是定义 Thing 的子classes,Haskell 只是让您定义输出 Thing 的函数(或直接定义 Thing 本身)。因此,如果在 Java 中,您可以定义

class ConcreteThing extends Thing {
    @Override
    public String name() {
        return "ConcreteThing";
    }

    @Override
    public void makeLightsBlink(int duration) {
        for (int i = 0; i < duration; i++) {
            System.out.println("Lights are blinking!");
        }
    }
}

在 Haskell 中,您可以改为定义

concreteThing :: Thing
concreteThing = Thing { name = "ConcreteThing", makeLightsBlink = blinkFunction } where
    blinkFunction duration = for_ [1..duration] . const $ putStrLn "Lights are blinking!"

无需做任何花哨的事情。您可以使用组合和函数来实现您想要的任何行为。

方法 5 - 完全避免多态性

这与面向对象编程中的“开放与封闭原则”相对应。

有时候,正确的做法实际上是完全避免多态性。例如,考虑如何在 Java.

中实现单链表
abstract class List<T> {
    public abstract bool is_empty();
    public abstract T head();
    public abstract List<T> tail();

    public int length() {
        return empty() ? 0 : 1 + tail().length();
    }
}

class EmptyList<T> {
    @Override
    public bool is_empty() { 
        return true; 
    }

    @Override
    public T head() { 
        throw new IllegalArgumentException("can't take head of empty list"); 
    }

    @Override
    public List<T> tail() { 
        throw new IllegalArgumentException("can't take tail of empty list"); 
    }
}

class NonEmptyList<T> {
    private final T head;
    private final List<T> tail;

    public NonEmptyList(T head, List<T> tail) {
        this.head = head;
        this.tail = tail;
    }

    @Override
    public bool is_empty() { 
        return false; 
    }

    @Override
    public T head() { 
        return self.head; 
    }

    @Override
    public List<T> tail() { 
        return self.tail; 
    }
}

然而,这实际上不是一个好的模型,因为您希望只有两种构建列表的方法——空方法和非空方法. Haskell 让你可以很简单地做到这一点。类似的 Haskell 代码是

data List t = EmptyList | NonEmptyList t (List t)

empty :: List t -> Bool
empty EmptyList = True
empty (NonEmptyList t listT) = False

head :: List t -> t
head EmptyList = error "can't take head of empty list"
head (NonEmptyList t listT) = t

tail :: List t -> List t
tail EmptyList = error "can't take tail of empty list"
tail (NonEmptyList t listT) = listT

length list = if empty list then 0 else 1 + length (tail list)

当然,在 Haskell 中,我们尽量避免“部分”函数 - 我们尽量确保每个函数始终 return 是一个值。因此,您不会看到很多 Haskell 用户正是出于这个原因实际使用“head”和“tail”函数——他们有时会出错。您会看到

定义的长度
length EmptyList = 0
length (NonEmptyList t listT) = 1 + length listT

使用模式匹配。

函数式编程语言的这一特性称为“代数数据类型”。非常有用。

希望我已经让您相信,函数式编程不仅可以让您实现许多面向对象的设计模式,而且实际上还可以让您以更简洁明了的形式表达相同的想法。

我在您的示例中添加了一些糖分,因为很难用您的函数证明以对象为中心的实现是合理的。

请注意,我写得不多Haskell,但我认为它是进行比较的正确语言。

我不建议直接比较纯 OO 语言和纯 FP 语言,因为这是浪费时间。如果您选择一门 FP 语言并学习如何从功能上思考,您将不会错过任何 OO 功能。

-- We define and create data of type Fish and Bird

data Fish = Fish String
nemo = Fish "Nemo";

data Bird = Bird String
tweety = Bird "Tweety"


-- We define how they can be displayed with the function `show`

instance Show Fish where
    show (Fish name) = name ++ " the fish"

instance Show Bird where
    show (Bird name) = name ++ " the bird"


{- We define how animals can think with the function `think`.
   Both Fish and Bird will be Animals.
   Notice how `show` dispatches to the correct implementation.
   We need to add to the type signature the constraint that
   animals are showable in order to use `show`.
-}

class Show a => Animal a where
    think :: a -> String -> String
    think animal thought =
        show animal ++ " is thinking about " ++ thought

instance Animal Fish
instance Animal Bird


-- Same thing for Swimmer, only with Fish

class Show s => Swimmer s where
    swim :: s -> String -> String
    swim swimmer length =
        show swimmer ++ " is swimming " ++ length

instance Swimmer Fish


-- Same thing for Singer, only with Bird

class Show s => Singer s where
    sing :: s -> String
    sing singer = show singer ++ " is singing"

instance Singer Bird


{- We define a function which applies to any animal.
   The compiler can figure out that it takes any type
   of the class Animal because we are using `think`.
-}

goToCollege animal = think animal "quantum physics"


-- we're printing the values to the console

main = do
    -- prints "Nemo the fish is thinking about quantum physics"
    print $ goToCollege nemo

    -- prints "Nemo the fish is swimming 4 meters"
    print $ swim nemo "4 meters"

    -- prints "Tweety the bird is thinking about quantum physics"
    print $ goToCollege tweety

    -- prints "Tweety the bird is singing"
    print $ sing tweety

我想知道它在 Clojure 中会是什么样子。它并不令人满意,因为 defprotocol 不提供默认实现,但话又说回来:我们不是将一种风格强加于一种不是为它设计的语言吗?

(defprotocol Show
    (show [showable]))

(defprotocol Animal
    (think [animal thought]))

(defn animal-think [animal thought]
    (str (show animal) " is thinking about " thought))

(defprotocol Swimmer
    (swim [swimmer length]))

(defprotocol Singer
    (sing [singer]))

(defrecord Fish [name]
    Show
    (show [fish] (str (:name fish) " the fish"))
    Animal
    (think [a b] (animal-think a b))
    Swimmer
    (swim [swimmer length] (str (show swimmer) " is swimming " length)))

(defrecord Bird [name]
    Show
    (show [fish] (str (:name fish) " the bird"))
    Animal
    (think [a b] (animal-think a b))
    Singer
    (sing [singer] (str (show singer) " is singing")))

(defn goToCollege [animal]
    (think animal "quantum physics"))

(def nemo (Fish. "Nemo"))

(def tweety (Bird. "Tweety"))

(println (goToCollege nemo))
(println (swim nemo "4 meters"))
(println (goToCollege tweety))
(println (sing tweety))

问题是你想要什么样的多态性。如果您只需要在编译时使用一些多态性,Haskell 的 typeclass 对于大多数情况来说几乎是完美的。

如果你想拥有 运行 时间的多态性(即根据 运行 时间类型动态切换行为),许多函数式编程语言不鼓励使用这种编程模式,因为它具有强大的泛型和类型类, 动态多态性并不总是必要的。

简而言之,如果语言支持子类型,你可以选择动态多态性,而在没有完整子类型的严格函数式语言中,你应该始终以函数式方式编程。最后,如果你仍然想要两者(动态多态性和强大的类型类),你可以尝试具有 traits 的语言,如 ScalaRust.