问题描述
假设您在面向对象的应用程序中有这个:
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")
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 多态性吗?
如果是这样,在高层次上,如果这可以用函数式语言解决,它是如何工作的?
- Achieving polymorphism in functional programming
- https://www.quora.com/How-is-polymorphism-used-in-functional-programming-languages
- https://wiki.haskell.org/OOP_vs_type_classes
解决方法
函数式编程语言有多种实现多态的方法。我将比较 Java(我最了解的 OOP 语言)和 Haskell(我最了解的函数式语言)。
方式一:“参数多态”
使用参数多态性,您根本不需要了解有关底层类型的任何信息。例如,如果我有一个包含 T 类型元素的单向链表,我实际上不需要知道任何关于 T 类型的信息就可以找到列表的长度。我会写一些类似的东西
length :: forall a . [a] -> Integer
length [] = 0
length (x:xs) = 1 + length xs
在 Haskell 中(显然我想在实践中使用更好的算法,但你明白了)。请注意,列表元素的类型无关紧要;获取长度的代码是一样的。第一行是“类型签名”。它表示对于每个类型 a,length 将采用 a 的列表并输出一个整数。
这不能用于太多“严重的多态性”,但这绝对是一个强大的开始。它大致对应于 Java 的泛型。
方式二:类型类风格的多态
即使是像检查相等性这样温和的事情,实际上也需要多态性。不同的类型需要不同的代码来检查相等性,并且对于某些类型(通常是函数),由于停机问题,检查相等性实际上是不可能的。因此,我们使用“类型类”。
假设我定义了一个具有 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 类型的参数并返回一个 Bool。
你可能不难猜到不等于函数 (/=) 也有类型签名
(/=) :: forall b . Eq b => b -> b -> Bool
因为 (/=) 被定义为
x /= y = not (x == y)
当我们调用 (/=) 函数时,该函数将根据参数的类型部署正确版本的 (==) 函数。如果参数具有不同的类型,您将无法使用 (/=) 来比较它们。
类型类样式的多态性允许您执行以下操作:
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”类型类,因此我们可以在两者上调用 think 函数。也就是说,
>>> think Bird "thought"
"think thought"
>>> think Fish "thought"
"think thought"
这个用例大致对应于 Java 接口 - 类型可以实现任意数量的类型类。但是类型类比接口强大得多。
方式 3:函数
如果你的对象只有一个方法,它也可能只是一个函数。这是避免继承层次结构的一种非常常见的方法 - 处理函数而不是 1-method 基类的继承者。
因此可以定义
type Animal = String -> String
basicAnimal :: Animal
basicAnimal thought = "think " ++ thought
“动物”实际上只是获取一个字符串并生成另一个字符串的一种方式。这将对应于 Java 代码
class Animal {
public String think(String thought) {
return "think " + thought;
}
}
假设在 Java 中,我们决定实现一个动物的子类,如下所示:
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 的高阶函数实现的。
方式 4:组合优于继承 + 函数
假设我们有一个带有两个方法的抽象基类 Thing:
abstract class Thing {
public abstract String name();
public abstract void makeLightBlink(int duration);
}
我使用的是 Java 风格的语法,但希望不会太混乱。
从根本上说,使用这个抽象基类的唯一方法是调用它的两个方法。因此,Thing 实际上应该被认为是一个由字符串和函数组成的有序对。
在像 Haskell 这样的函数式语言中,我们会写
data Thing = Thing { name :: String,makeLightsBlink :: Int -> IO () }
换句话说,一个“Thing”由两部分组成:一个名称,它是一个字符串,一个函数 makeLightsBlink,它接受一个 Int 并输出一个“IO 动作”。这是 Haskell 处理 IO 的方式——通过类型系统。
Haskell 不需要定义 Thing 的子类,而是让您定义输出 Thing 的函数(或直接定义 Things 本身)。所以如果在 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 中,我们尽量避免使用“部分”函数——我们尽量确保每个函数总是返回一个值。所以你不会看到很多 Haskeller 正是因为这个原因而实际使用“头”和“尾”函数——他们有时会出错。你会看到由
定义的长度length EmptyList = 0
length (NonEmptyList t listT) = 1 + length listT
使用模式匹配。
函数式编程语言的这一特性被称为“代数数据类型”。这非常有用。
希望我已经让您确信,函数式编程不仅可以让您实现许多面向对象的设计模式,而且实际上可以让您以更简洁明了的形式表达相同的想法。
,我在您的示例中添加了一些糖,因为很难用您的函数证明以对象为中心的实现是合理的。
请注意,我没有写很多 Haskell,但我认为这是进行比较的正确语言。
我不建议直接比较纯 OO 语言和纯 FP 语言,因为这是浪费时间。如果您学习 FP 语言并学习如何从功能上进行思考,您将不会错过任何面向对象的功能。
-- 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 的语言,如 Scala
或 Rust
。 >