为什么 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 看起来很清楚)是
- 有很多不同的产品,都实现了一个通用接口
- 有很多不同的创建者类,都实现了一个通用接口
- 2 中的每个 类 都隐藏了创建 1 中那些产品的逻辑
- (如果我理解正确的话,对象和创建者之间没有必要存在一对一的映射,因为创建者可以隐藏不同的逻辑,根据用户对创建哪个特定对象做出不同的选择input/time/conditions/whatever.)
- 客户端代码将有创作者供其使用,每次使用其中之一(创作者,而不是客户端代码)将知道如何包装产品以及具体产品。
这一切(或部分)如何适用于 Haskell?
有几个不同的产品 类 实现一个通用的产品接口是 Haskell 的事情,接口是一种类型 class
,产品是类型(data
s/newtype
s/existing 类型)。例如,参考链接视频中的宇宙飞船和小行星示例,我们可以有一个类型 class
来定义 Obstacles
任何提供 size
、speed
的东西和 position
,
class Obstacle a where
size :: a -> Double
speed :: a -> Int
position :: a -> (Int,Int)
和Asteroid
和Planet
可能是以某种方式实现这个接口的两个具体类型,
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
的输入应用逻辑来选择它是否必须创建 Asteroid
或Planet
.
但是我不明白我怎么能有一个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=] 相应地——Label
、Field
、Button
,依此类推。 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。
我读过
另一方面,我的意图并不是像 Haskell 那样在函数式语言上强制使用特定于 OOP 的模式。恰恰相反,我想了解 如何 Haskell 解决 OOP 中通过工厂模式 解决的需求。
我觉得即使这些 需求 一开始在 Haskell 中可能没有意义,但我无法更好地提出问题。
我对工厂模式结构的理解(基于this video 看起来很清楚)是
- 有很多不同的产品,都实现了一个通用接口
- 有很多不同的创建者类,都实现了一个通用接口
- 2 中的每个 类 都隐藏了创建 1 中那些产品的逻辑
- (如果我理解正确的话,对象和创建者之间没有必要存在一对一的映射,因为创建者可以隐藏不同的逻辑,根据用户对创建哪个特定对象做出不同的选择input/time/conditions/whatever.)
- 客户端代码将有创作者供其使用,每次使用其中之一(创作者,而不是客户端代码)将知道如何包装产品以及具体产品。
这一切(或部分)如何适用于 Haskell?
有几个不同的产品 类 实现一个通用的产品接口是 Haskell 的事情,接口是一种类型 class
,产品是类型(data
s/newtype
s/existing 类型)。例如,参考链接视频中的宇宙飞船和小行星示例,我们可以有一个类型 class
来定义 Obstacles
任何提供 size
、speed
的东西和 position
,
class Obstacle a where
size :: a -> Double
speed :: a -> Int
position :: a -> (Int,Int)
和Asteroid
和Planet
可能是以某种方式实现这个接口的两个具体类型,
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
的输入应用逻辑来选择它是否必须创建 Asteroid
或Planet
.
但是我不明白我怎么能有一个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=] 相应地——Label
、Field
、Button
,依此类推。 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。