如何在Haskell中建模mixins /多个接口? [英] How to model mixins / multiple interfaces in Haskell?
问题描述
我遇到了关于在Haskell中建模继承的这个问题,它提醒我,我有一个更复杂一点的版本一样的问题。我会从那里采用这个例子,因为它比我自己想象的容易。
假设您的程序包含多种类型:
数据Camera = Camera ...
data Light = SpotLight ... | DirectionalLight ...
data Object = Monster ... |玩家... | NPC ...
现在你想实现一些基本的物理,所以你希望它们都有一个位置和速度,比如说某种类型的 Vec3
。
一种方法是声明一个物理
带有 pos 和
vel
函数的类型类型,并使所有类型它的实例。但是这意味着你必须修改所有类型以包含两个 Vec3
s,如果你已经定义了很多好的类型并且你只想粘一点位顶部的功能位。 Chris Taylor建议的基于镜头的解决方案具有相同的问题。
一个让我感觉更加整洁的解决方案是声明一个新类型的构造函数,
data物理a =物理a Vec3 Vec3
然后你只需要实现 pos
, vel
和一个 Functor
实例,您可以保留所有现有的类型声明。
然而......这不是很好。如果您现在想要将对象绘制为蓝色或绿色或紫色,则可以使用颜色来完成相同的操作:
data a a color a a color a b $ b b
$ b现在如果你有一个彩色物理相机您必须 fmap
不同的次数,这取决于您是要查看其颜色还是其位置或焦距。并且彩色物理相机
应该与物理彩色相机
相同,但事实并非如此。所以这不是一个优雅的解决方案。
有没有一种很好的方法可以将不同的功能集合到Haskell中的类型中?一个简单的解决方案,可以在没有语言扩展或者很多样板的情况下工作在普通的老式Haskell中,但是我也乐于学习任何与镜头相关的库,如果这真的是解决这个问题的最好方法。 >
(这个关于mixins风格的代码重用的旧问题似乎相关,但恐怕我不能完全理解这个问题或被接受的解决方案。)
解决方案也许我们可以从未评估的 mtl
软件包,并将先前建议的两种方法:声明两个类型的构造函数(并使它们成为函子)并声明相应的类型类/实例。
但是这里有个诀窍:我们将使用 Data.Functor.Compose
from 变形金刚
,然后定义附加的传递实例以使内层中的方法可用于外层。就像 mtl
为monad变形金刚做的一样!
首先,一些预备:
{ - #LANGUAGE DeriveFunctor# - }
{ - #LANGUAGE FlexibleInstances# - }
import Data.Functor.Compose
数据Camera =相机
数据Light = SpotLight | DirectionalLight
data Object = Monster |玩家| NPC
data Vec3 = Vec3C - 虚拟类型
data Color = ColourC - 虚拟类型
data
定义:
数据物理a =物理a Vec3 Vec3派生Functor
data彩色a =彩色a派生函子
相应的类型类:
class Functor g => FunctorPhysical g其中
vecs :: g a - > (Vec3,Vec3)
class Functor g => FunctorColoured g其中
color :: g a - >颜色
基本实例:
实例FunctorPhysical Physical其中
vecs(Physical _ v1 v2)=(v1,v2)
实例FunctorColoured彩色其中
颜色(彩色_ c )= c
现在 mtl
- 启发技巧。 Passthrough实例!
实例Functor f => FunctorPhysical(Compose Physical f)其中
vecs(Compose f)= vecs f
实例Functor f => FunctorColoured(Compose Colored f)其中
color(Compose f)= color f
实例FunctorPhysical f => FunctorPhysical(Compose Colored f)其中
vecs(Compose(Colored a _))= vecs a
实例FunctorColoured f => FunctorColoured(Compose Physical f)其中
color(Compose(Physical a _ _))= color a
示例值:
exampleLight ::撰写物理彩色光源
exampleLight =撰写(物理(彩色SpotLight ColourC)Vec3C Vec3C)
您应该可以同时使用 vecs 和 color
加上上述值。
编辑:上述解决方案存在访问原始包装值麻烦的问题。这是一个使用comonads的替代版本,它允许您使用 extract
来获取包装值。
import Control.Comonad
import Control.Comonad.Trans.Class
import Control.Comonad.Trans.Env
import Data.Functor.Identity
data PhysicalT wa = PhysicalT {unPhy :: EnvT(Vec3,Vec3)wa}
instance Functor w => Functor(PhysicalT w)其中
fmap g(PhysicalT wa)= PhysicalT(fmap g wa)
instance Comonad w => Comonad(PhysicalT w)其中
duplicate(PhysicalT wa)= PhysicalT(extend PhysicalT wa)
extract(PhysicalT wa)= extract wa
instance ComonadTrans PhysicalT其中
较低=较低。 unPhy
-
data ColouredT w a = ColouredT {unCol :: EnvT Color w a}
实例Functor w => Functor(ColouredT w)其中
fmap g(ColouredT wa)= ColouredT(fmap g wa)
实例Comonad w => Comonad(ColouredT w)其中
重复(ColouredT wa)= ColouredT(延长ColouredT wa)
提取(ColouredT wa)=提取wa
实例ComonadTrans ColouredT其中
较低=较低。 unCol
class Functor g => FunctorPhysical g其中
vecs :: g a - > (Vec3,Vec3)
class Functor g => FunctorColoured g其中
color :: g a - >颜色
实例Comonad c => FunctorPhysical(PhysicalT c)其中
vecs =问。 unPhy
实例Comonad c => FunctorColoured(ColouredT c)其中
color = ask。 unCol
- passthrough实例
实例(Comonad c,FunctorPhysical c)=> FunctorPhysical(ColouredT c)其中
vecs = vecs。较低
实例(Comonad c,FunctorColoured c)=> FunctorColoured(PhysicalT c)其中
color = color。低
- 示例值
exampleLight :: PhysicalT(ColouredT Identity)Light
exampleLight = PhysicalT。 EnvT(Vec3C,Vec3C)$
ColouredT。 EnvT ColourC $身份SpotLight
不幸的是,它需要更多的样板。就个人而言,我只会使用嵌套的 EnvT
变形金刚,代价是不太统一的访问。
I came across this question on modeling inheritance in Haskell and it reminded me that I have a little more complicated version of the same problem. I'll adopt the example from there because it's easier than thinking up my own.
Suppose your program contains a number of types:
data Camera = Camera ...
data Light = SpotLight ... | DirectionalLight ...
data Object = Monster ... | Player ... | NPC ...
Now you want to implement some basic physics, so you want them all to have a position and a velocity, say of some type Vec3
.
One way to do this is to declare a Physical
typeclass with pos
and vel
functions, and make all your types instances of it. But that means you have to modify all the types to contain two Vec3
s, which is annoying if you have a lot of nice types defined already and you just want to glue a little bit of functionality on top. The lens-based solution suggested by Chris Taylor has the same issue.
A solution that feels neater to me is to declare a new type constructor,
data Physical a = Physical a Vec3 Vec3
Then you only have to implement pos
, vel
, and a Functor
instance once, and you get to keep all your existing type declarations.
However... this doesn't compose very well. If you now want to have the ability to paint your objects blue or green or purple, you might want to do the same thing with colours:
data Coloured a = Coloured a Colour
But now if you have a Coloured Physical Camera
you have to fmap
a different number of times depending on whether you want to look at its colour or its position or its focal length. And a Coloured Physical Camera
ought to be the same thing as a Physical Coloured Camera
, but it's not. So this is not an elegant solution.
Is there a nice way to mix in different sets of functionality to types in Haskell? A simple solution that works in plain old Haskell without language extensions or a lot of boilerplate would be ideal, but I'm also open to learning any of the lens-related libraries if that's really the best way to approach the problem.
(This older question on mixins-style code reuse seems related, but I'm afraid I don't fully understand the question or the accepted solution.)
解决方案 Perhaps we could take a cue from the underappreciated mtl
package, and combine the two previously suggested approaches: declare two type constructors (and make them functors) and declare corresponding typeclasses/instances.
But here's the trick: we will compose the functors using Data.Functor.Compose
from transformers
, and then define additional "pass-through" instances to make methods from the inner layers available in the outer layer. Just like mtl
does for monad transformers!
First, some preliminaries:
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleInstances #-}
import Data.Functor.Compose
data Camera = Camera
data Light = SpotLight | DirectionalLight
data Object = Monster | Player | NPC
data Vec3 = Vec3C -- dummy type
data Colour = ColourC -- dummy type
The data
definitions:
data Physical a = Physical a Vec3 Vec3 deriving Functor
data Coloured a = Coloured a Colour deriving Functor
The corresponding typeclasses:
class Functor g => FunctorPhysical g where
vecs :: g a -> (Vec3,Vec3)
class Functor g => FunctorColoured g where
colour :: g a -> Colour
The base instances:
instance FunctorPhysical Physical where
vecs (Physical _ v1 v2) = (v1,v2)
instance FunctorColoured Coloured where
colour (Coloured _ c) = c
And now the mtl
-inspired trick. Passthrough instances!
instance Functor f => FunctorPhysical (Compose Physical f) where
vecs (Compose f) = vecs f
instance Functor f => FunctorColoured (Compose Coloured f) where
colour (Compose f) = colour f
instance FunctorPhysical f => FunctorPhysical (Compose Coloured f) where
vecs (Compose (Coloured a _)) = vecs a
instance FunctorColoured f => FunctorColoured (Compose Physical f) where
colour (Compose (Physical a _ _)) = colour a
An example value:
exampleLight :: Compose Physical Coloured Light
exampleLight = Compose (Physical (Coloured SpotLight ColourC) Vec3C Vec3C)
You should be able to use both vecs
and colour
with the above value.
EDIT: The above solution has the problem that accessing the original wrapped value is cumbersome. Here is an alternate version using comonads that lets you use extract
to get the wrapped value back.
import Control.Comonad
import Control.Comonad.Trans.Class
import Control.Comonad.Trans.Env
import Data.Functor.Identity
data PhysicalT w a = PhysicalT { unPhy :: EnvT (Vec3,Vec3) w a }
instance Functor w => Functor (PhysicalT w) where
fmap g (PhysicalT wa) = PhysicalT (fmap g wa)
instance Comonad w => Comonad (PhysicalT w) where
duplicate (PhysicalT wa) = PhysicalT (extend PhysicalT wa)
extract (PhysicalT wa) = extract wa
instance ComonadTrans PhysicalT where
lower = lower . unPhy
--
data ColouredT w a = ColouredT { unCol :: EnvT Colour w a }
instance Functor w => Functor (ColouredT w) where
fmap g (ColouredT wa) = ColouredT (fmap g wa)
instance Comonad w => Comonad (ColouredT w) where
duplicate (ColouredT wa) = ColouredT (extend ColouredT wa)
extract (ColouredT wa) = extract wa
instance ComonadTrans ColouredT where
lower = lower . unCol
class Functor g => FunctorPhysical g where
vecs :: g a -> (Vec3,Vec3)
class Functor g => FunctorColoured g where
colour :: g a -> Colour
instance Comonad c => FunctorPhysical (PhysicalT c) where
vecs = ask . unPhy
instance Comonad c => FunctorColoured (ColouredT c) where
colour = ask . unCol
-- passthrough instances
instance (Comonad c, FunctorPhysical c) => FunctorPhysical (ColouredT c) where
vecs = vecs . lower
instance (Comonad c, FunctorColoured c) => FunctorColoured (PhysicalT c) where
colour = colour . lower
-- example value
exampleLight :: PhysicalT (ColouredT Identity) Light
exampleLight = PhysicalT . EnvT (Vec3C,Vec3C) $
ColouredT . EnvT ColourC $ Identity SpotLight
Sadly, it requires even more boilerplate. Personally, I would just use nested EnvT
transformers at the cost of less uniform access.
这篇关于如何在Haskell中建模mixins /多个接口?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!