为什么 Haskell 中不需要工厂模式? Haskell 中 OOP 中的模式解决的需求是如何解决的?

Why is the Factory pattern not needed in Haskell? And how are the needs that pattern addresses in OOP addressed in Haskell?

我读过 关于 抽象工厂模式 ,但唯一的答案是 模拟 Haskell OOP 语言中的内容(虽然前言是你不需要它在Haskell)。

另一方面,我的意图并不是像 Haskell 那样在函数式语言上强制使用特定于 OOP 的模式。恰恰相反,我想了解 如何 Haskell 解决 OOP 中通过工厂模式 解决的需求。

我觉得即使这些 需求 一开始在 Haskell 中可能没有意义,但我无法更好地提出问题。

我对工厂模式结构的理解(基于this video 看起来很清楚)是

  1. 有很多不同的产品,都实现了一个通用接口
  2. 有很多不同的创建者类,都实现了一个通用接口

这一切(或部分)如何适用于 Haskell?

有几个不同的产品 类 实现一个通用的产品接口是 Haskell 的事情,接口是一种类型 class,产品是类型(datas/newtypes/existing 类型)。例如,参考链接视频中的宇宙飞船和小行星示例,我们可以有一个类型 class 来定义 Obstacles 任何提供 sizespeed 的东西和 position,

class Obstacle a where
  size :: a -> Double
  speed :: a -> Int
  position :: a -> (Int,Int)

AsteroidPlanet可能是以某种方式实现这个接口的两个具体类型,

data Asteroid = Asteroid { eqside :: Double, pos :: (Int,Int) } deriving Show
instance Obstacle Asteroid where
  size a = eqside a ^ 3 -- yeah, cubic asteroids
  speed _ = 100
  position = pos

data Planet = Planet { radius :: Double, center :: (Int,Int) } deriving Show
instance Obstacle Planet where
  size a = k * radius a ^ 3
    where k = 4.0/3.0*3.14
  speed _ = 10
  position = center

到目前为止,我没有发现我在 Haskell 或 OOP 语言中所做的事情有任何真正的区别。但它接下来会出现。

此时,按照链接视频中的示例,客户端代码可以是一个游戏,它会遍历一些关卡,并根据关卡的数量生成不同的障碍物;它可能是这样的:

clientCode :: [Int] -> IO ()
clientCode levels = do
  mapM_ (print . makeObstacle) levels

其中 makeObstacle 应该是创建者函数,或者是几个函数之一,给定类型 Int 的输入应用逻辑来选择它是否必须创建 AsteroidPlanet.

但是我不明白我怎么能有一个returns输出不同类型的函数,Asteroid vs Planet(事实上他们实现了相同的Obstacle interface 似乎没有帮助),基于所有相同类型的不同值,[Int],更不用说理解“工厂”功能及其通用接口应该是什么了。

Having several different product classes implementing a common product interface is a thing in Haskell, the interface being a typeclass

不完全是。确实 typeclasses 可以表达接口在 OO 语言中的作用,但这并不总是有意义。具体来说,class 没有任何意义,其中所有方法都具有 a -> Fubar.

形式的类型签名

为什么?好吧,您不需要 class - 只需将其设为具体数据类型即可!

data Obstacle = Obstace
  { size :: Double
  , speed :: Int      -- BTW, why would speed be integral??
  , position :: (Int,Int) }

记录字段也可以是函数、IO 动作等等 – 这足以模拟 OO class 的方法可以做什么。普通数据唯一不能表达的是继承——但即使在 OO 中,也有一些关于组合优先于继承的口头禅,就是这样。

或者,您可以将 Obstacle 设为求和类型

data Obstacle = Asteroid ...
              | Planet ...

这取决于应用程序哪个更好。无论哪种方式,它仍然是一个具体的数据类型,不需要 class。

由于 Obstacle 是一种简单的数据类型,因此它的创建者无需“抽象”。相反,您可以简单地使用各种函数 -> Obstacle 来创建恰好代表小行星、行星或其他任何东西的障碍。

你也可以将你的那种“面向对象接口实例”包装成一个数据类型,一个existential

{-# LANGUAGE GADTs #-}

class Obstacle a where ...

data AnObstacle where
  AnObstance :: Obstacle a => a -> AnObstacle

但是 don't do that 除非你确切地知道这就是你想要的。

当我需要创建在运行时之前未知的类型的值时,我通常会使用 OOP 中的工厂。

传统示例是动态 UI:我阅读了 UI 布局的描述,并创建了一些 UIComponent 基础 class 的各种子 class =76=] 相应地——LabelFieldButton,依此类推。 UIComponent 将提供一些通用接口用于渲染、响应事件等。

然后,抽象工厂将只是一个间接级别:针对不同平台 (Windows/Mac/Linux)、呈现格式 (GUI/text/HTML) 等拥有不同类型的工厂。

看来您是从几个常见的以 OOP 为中心的关于如何建模的假设开始的:

  • 每个 UI 组件都作为单独的类型实现

  • 具有通用接口或基础的类型 class 应该成为类型的实例class

遵循这条推理线将引导您实现问题中描述的实现,这被称为“存在反模式”:

-- There are some component types:

data Label = …
data TextField = …
data Button = …
data SubmitButton = …
…

-- There’s a common interface for components:

class Component c where
  render :: c -> IO ()
  handleEvent :: c -> Event -> IO ()
  getBounds :: c -> (Word, Word)
  …

-- Each component type implements that interface:

instance Component Label where
  render = renderLabel
  handleEvent _ _ = pure ()
  getBounds (Label text) = computeTextDimensions text
  …

instance Component TextField where …

…

-- Abstract components are created dynamically:

data SomeComponent where
  SomeComponent :: (Component c) => c -> SomeComponent

-- A UI is a collection of abstract components:

type UI = [SomeComponent]

为了通过解析 UI 描述来处理动态类型,您需要包装器 SomeComponent,它是一对抽象(“存在”)类型的值 c,及其 Component 的实例。这类似于 OOP vtable。但是由于 only 你可以用这个值做的事情是将 Component 的方法应用到它,它 exactly 相当于只是一个功能记录:

-- A component is described by just its operations:

data Component = Component
  { render :: IO ()
  , handleEvent :: Event -> IO ()
  , getBounds :: (Word, Word)
  …
  }

-- There are no separate component types, only functions
-- that construct different ‘Component’ values. Fields
-- are just captured variables.

label :: String -> Component
label text = Component
  { render = renderLabel text
  , handleEvent = \ _ _ -> pure ()
  , getBounds = computeTextDimensions text
  …
  }

textField :: … -> Component
button :: … -> Component
…

-- A UI is a collection of components of uniform type:

type UI = [Component]

这是抽象工厂的直接类比:具体 Component 工厂只是动态构建 Component 的函数,而抽象工厂是动态构建具体工厂的函数.

-- A common dynamic description of the constructor
-- argument values of a UI element.
data ComponentDescription
  = Label String
  | TextField …
  | Button …
  …

-- Parse such a description from JSON.
instance FromJSON ComponentDescription where …

-- Construct a component from its constructor values.
type ComponentFactory = ComponentDescription -> Component

-- A dynamic description of a platform.
data Platform = Windows | Mac | Linux

-- Construct a component factory for a platform theme.
type AbstractComponentFactory = Platform -> ComponentFactory

设计模式几乎消失了。它仍然存在,但只是函数的不同用途。在某种程度上,这也让人想起“实体组件系统”架构,其中对象被描述为表示行为组合的数据。 (不同之处在于这里没有“系统”。)

当您希望组件集打开时,此公式主要有用;但更常见的是,我们默认在 Haskell 中使用 closed 数据建模和求和类型。在那种情况下,我们将直接使用像上面 ComponentDescription 类型的总和类型,并带有合适的字段,作为组件的表示。

在组件上添加新操作很容易:只需在 ComponentDescription 上进行模式匹配即可。添加一种新类型的组件需要更新所有现有函数(除非它们具有通配符匹配),但在实践中通常 需要 编译器告诉您需要更新的所有内容。

操作和组件类型的可扩展性也是可以实现的,但是一个好的描述超出了这个答案的范围。想了解更多,搜索“表达式问题”;特别是,无标签最终样式 在Haskell 中被认为是一个很好的传统解决方案,它以不同的方式使用类型classes。